app/controllers/api/base_controller/renderer.rb
require 'jbuilder' module Api class BaseController module Renderer # # Helper proc to render a collection # def render_collection(type, resources, opts = {}) render :json => collection_to_jbuilder(type, gen_reftype(type, opts), resources, opts).target! end # # Helper proc to render a single resource # def render_resource(type, resource, opts = {}) render :json => resource_to_jbuilder(type, gen_reftype(type, opts), resource, opts).target!, :status => status_from_resource(resource) end # # We want reftype to reflect subcollection if targeting as such. # def gen_reftype(type, opts) opts[:is_subcollection] ? "#{@req.collection}/#{@req.collection_id}/#{type}" : type end # Methods for Serialization as Jbuilder Objects. # # Given a resource, return its serialized flavor using Jbuilder #Method `collection_to_jbuilder` has a Cognitive Complexity of 19 (exceeds 11 allowed). Consider refactoring. def collection_to_jbuilder(type, reftype, resources, opts = {}) link_builder = Api::LinksBuilder.new(params, @req.url, opts[:counts]) Jbuilder.new do |json| json.set! 'name', opts[:name] if opts[:name] if opts[:counts] opts[:counts].counts.each do |count, value| json.set! count, value end end json.set! 'pages', link_builder.pages if link_builder.links? unless @req.hide?("resources") || collection_option?(:hide_resources) key_id = collection_config.resource_identifier(type) json.resources resources.collect do |resource| if opts[:expand_resources] add_hash json, resource_to_jbuilder(type, reftype, resource, opts).attributes! else json.href normalize_href(reftype, resource[key_id]) end end end cspec = collection_config[type] aspecs = gen_action_spec_for_collections(type, cspec, opts[:is_subcollection], reftype) if cspec add_actions(json, aspecs, reftype) if link_builder.links? json.links do link_builder.links.each do |link_name, link_href| json.set! link_name, link_href end end end end end def resource_to_jbuilder(type, reftype, resource, opts = {}) normalize_options = {} reftype = get_reftype(type, reftype, resource, opts) json = Jbuilder.new physical_attrs, virtual_attrs = validate_attr_selection(resource) normalize_options[:render_attributes] = physical_attrs if physical_attrs.present? add_hash json, normalize_hash(reftype, resource, normalize_options) expand_virtual_attributes(json, type, resource, virtual_attrs) unless virtual_attrs.empty? expand_subcollections(json, type, resource) if resource.respond_to?(:attributes) json.set!('href_slug', "#{type}/#{resource.id}") if virtual_attrs.include?('href_slug') expand_actions(resource, json, type, opts, physical_attrs) if opts[:expand_actions] expand_resource_custom_actions(resource, json, type, physical_attrs) if opts[:expand_custom_actions] json end def get_reftype(type, reftype, resource, _opts = {}) # sometimes we are returning different objects than the posted resource, i.e. request for an order. return reftype unless resource.respond_to?(:attributes) rclass = resource.class collection_class = collection_class(type) # Ensures hrefs are consistent with those of the collection they were requested from return reftype if collection_class == rclass || collection_class.descendants.include?(rclass) collection_config.name_for_klass(rclass) || collection_config.name_for_subclass(rclass) end # # Common proc for adding a child element to the Jbuilder # def add_child(json, hash) json.child! { |js| hash.each { |attr, value| js.set! attr, value } } unless hash.blank? end # # Common proc for adding a hash directly to the Jbuilder # def add_hash(json, hash) return if hash.blank? hash.each do |attr, value| json.set! attr, value end end # # Method name for optional accessor of virtual attributes # def virtual_attribute_accessor(type, attr) method = "fetch_#{type}_#{attr}" respond_to?(method) ? method : nil end private def resource_search(id, type, klass = nil, key_id = nil) klass ||= collection_class(type) key_id ||= collection_config.resource_identifier(type) validate_id(id, key_id, klass) target = if respond_to?("find_#{type}") public_send("find_#{type}", id) else find_resource(klass, key_id, id) end raise NotFoundError, "Couldn't find #{klass} with '#{key_id}'=#{id}" unless target filter_resource(target, type, klass) end def find_resource(klass, key_id, id) key_id == "id" ? klass.find(id) : klass.find_by(key_id => id) end def filter_resource(target, type, klass) res = Rbac.filtered_object(target, :user => User.current_user, :class => klass) raise ForbiddenError, "Access to the resource #{type}/#{target.id} is forbidden" unless res res end def collection_search(is_subcollection, type, klass) res = if is_subcollection send("#{type}_query_resource", parent_resource_obj) elsif by_tag_param klass.find_tagged_with(:all => by_tag_param, :ns => TAG_NAMESPACE, :separator => ',') else find_collection(klass) end res = res.where(public_send("#{type}_search_conditions")) if respond_to?("#{type}_search_conditions") collection_filterer(res, type, klass, is_subcollection) end def find_collection(klass) klass.all end Method `collection_filterer` has a Cognitive Complexity of 12 (exceeds 11 allowed). Consider refactoring. def collection_filterer(res, type, klass, is_subcollection = false) miq_expression = filter_param(klass) if miq_expression if is_subcollection && !res.respond_to?(:where) raise BadRequestError, "Filtering is not supported on #{type} subcollection" end sql, _, attrs = miq_expression.to_sql res = res.where(sql) if attrs[:supported_by_sql] end sort_options = sort_params(klass) if res.respond_to?(:reorder) res = res.reorder(sort_options) if sort_options.present? options = {:user => User.current_user} options[:order] = sort_options if sort_options.present? options[:filter] = miq_expression if miq_expression options[:offset] = params['offset'] if params['offset'] options[:limit] = params['limit'] if params['limit'] options[:extra_cols] = determine_extra_cols(klass) options[:include_for_find] = determine_include_for_find(klass) filter_results(miq_expression, res, options) end def filter_results(miq_expression, res, options) if miq_expression.present? && options.key?(:limit) && options.key?(:offset) subquery_res = Rbac.filtered(res, options.except(:offset, :limit, :extra_cols)) [Rbac.filtered(res, options), subquery_res.count] else [Rbac.filtered(res, options)] end end Method `virtual_attribute_search` has a Cognitive Complexity of 15 (exceeds 11 allowed). Consider refactoring. def virtual_attribute_search(resource, attribute) if resource.class < ApplicationRecord rbac = Rbac::Filterer.new # is relation in 'attribute' variable plural in the model class (from 'resource.class') ? if [:has_many, :has_and_belongs_to_many].include?(resource.class.reflection_with_virtual(attribute).try(:macro)) resource_attr = resource.public_send(attribute) klass = resource_attr.kind_of?(ActiveRecord::Relation) ? resource_attr.klass : resource_attr.try(:first).class return resource_attr unless rbac.send(:apply_rbac_directly?, klass) Rbac.filtered(resource_attr) # Don't re-do an Rbac query if it has already been done elsif collection_class(@req.subject) != resource.class.base_model && rbac.send(:apply_rbac_directly?, resource.class) Rbac.filtered_object(resource).try(:public_send, attribute) else resource.public_send(attribute) end else resource.public_send(attribute) end end # # Let's expand subcollections for objects if asked for # def expand_subcollections(json, type, resource) collection_config.subcollections(type).each do |sc| target = "#{sc}_query_resource" next unless expand_subcollection?(sc, target) if Array(attribute_selection).include?(sc.to_s) raise BadRequestError, "Cannot expand subcollection #{sc} by name and virtual attribute" end expand_subcollection(json, sc, "#{type}/#{resource.id}/#{sc}", send(target, resource)) end end def expand_subcollection?(sc, target) return false unless respond_to?(target) # If there's no query method, no need to go any further expand_resources?(sc) || expand_action_resource?(sc) || resource_requested?(sc) end # Expand if: expand='resources' && no attributes specified && subcollection is configured def expand_resources?(sc) collection_config.show?(sc) && (@req.collection_id || @req.expand?('resources')) && @req.attributes.empty? end # Expand if: resource is being returned and subcollection is configured # IE an update to /service_catalogs expects service_templates as part of its resource def expand_action_resource?(sc) @req.method != :get && collection_config.show?(sc) end # Expand if: explicitly requested def resource_requested?(sc) @req.expand?(sc) end # # Let's expand virtual attributes and related objects if asked for # Supporting [<related_object>]*.<virtual_attribute> # def expand_virtual_attributes(json, type, resource, virtual_attrs) result = {} object_hash = {} virtual_attrs.each do |vattr| next if vattr == 'href_slug' attr_name, attr_base = split_virtual_attribute(vattr)Useless assignment to variable - `value`. Use `_` or `_value` as a variable name to indicate that it won't be used. value, value_result = if attr_base.blank? fetch_direct_virtual_attribute(type, resource, attr_name) else fetch_indirect_virtual_attribute(type, resource, attr_base, attr_name, object_hash) end result = result.deep_merge(Hash(value_result)) end add_hash json, result end def fetch_direct_virtual_attribute(type, resource, attr) return unless attr_accessible?(resource, attr) virtattr_accessor = virtual_attribute_accessor(type, attr) value = virtattr_accessor ? send(virtattr_accessor, resource) : virtual_attribute_search(resource, attr) value = add_custom_action_hrefs(value) if attr == "custom_actions" result = {attr => normalize_attr(attr, value)} # set nil vtype above to "#{type}/#{resource.id}/#{attr}" to support id normalization [value, result] end # # HACK: Because custom actions are represented as a plain hash # in the model, we lose all context about the type of object we # must add an href to in the normalization process. Refactoring # all of normalization to get the proper context will be # necessary in order to fix this correctly. Instead, we # intercept the result here, adding the correct hrefs, which # will not be overwritten later. # def add_custom_action_hrefs(value) return if value.nil? result = value.dup result[:buttons].each do |button| button["href"] = normalize_href(:custom_buttons, button["id"]) end result[:button_groups].each do |group| group["href"] = normalize_href(:custom_button_sets, group["id"]) group[:buttons].each do |button| button["href"] = normalize_href(:custom_buttons, button["id"]) end end result end def fetch_indirect_virtual_attribute(_type, resource, base, attr, object_hash) query_related_objects(base, resource, object_hash) return unless attr_accessible?(object_hash[base], attr) value = virtual_attribute_search(object_hash[base], attr) result = {attr => normalize_attr(attr, value)} # set nil vtype above to "#{type}/#{resource.id}/#{base.tr('.', '/')}/#{attr}" to support id normalization base.split(".").reverse_each { |level| result = {level => result} } [value, result] end # # Accesing and hashing <resource>[.<related_object>]+ in object_hash # def query_related_objects(object_path, resource, object_hash) return if object_hash[object_path].present? related_resource = resource related_objects = [] object_path.split(".").each do |related_object| related_objects << related_object if attr_accessible?(related_resource, related_object) related_resource = related_resource.public_send(related_object) object_hash[related_objects.join(".")] = related_resource if related_resource end end end def split_virtual_attribute(attr) attr_parts = attr_split(attr) return [attr_parts.first, ""] if attr_parts.length == 1 [attr_parts.last, attr_parts[0..-2].join(".")] end def attr_accessible?(object, attr) return true if object && object.respond_to?(attr) object.class.try(:has_attribute?, attr) || object.class.try(:reflect_on_association, attr) || object.class.try(:virtual_attribute?, attr) || object.class.try(:virtual_reflection?, attr) end def attr_virtual?(object, attr) return false if ID_ATTRS.include?(attr) primary = attr_split(attr).first klass = object klass = object.class if object.kind_of?(ActiveRecord::Base) (klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(primary)) || (klass.respond_to?(:virtual_attribute?) && klass.virtual_attribute?(primary)) || (klass.respond_to?(:virtual_reflection?) && klass.virtual_reflection?(primary)) end def attr_physical?(object, attr) return true if ID_ATTRS.include?(attr) klass = object klass = object.class if object.kind_of?(ActiveRecord::Base) (klass.respond_to?(:has_attribute?) && klass.has_attribute?(attr)) && !(klass.respond_to?(:virtual_attribute?) && klass.virtual_attribute?(attr)) end def attr_split(attr) attr.tr("/", ".").split(".") end # # Let's expand actions # def expand_actions(resource, json, type, opts, physical_attrs) return unless render_actions(physical_attrs) href = json.attributes!["href"] cspec = collection_config[type] aspecs = gen_action_spec_for_resources(cspec, opts[:is_subcollection], href, resource) if cspec add_actions(json, aspecs, type) end def add_actions(json, aspecs, type) if aspecs && aspecs.any? json.actions do |js| aspecs.each { |action_spec| add_child js, normalize_hash(type, action_spec) } end end end def expand_resource_custom_actions(resource, json, type, physical_attrs) return unless render_actions(physical_attrs) && collection_config.custom_actions?(type) href = @req.subcollection.present? ? normalize_url("#{@req.subcollection}/#{resource.id}") : json.attributes!["href"] json.actions do |js| resource_custom_action_names(resource).each do |action| add_child js, "name" => action, "method" => :post, "href" => href end end end def resource_custom_action_names(resource) return [] unless resource.respond_to?(:custom_action_buttons)Use `collect { |x| x.name.downcase }` instead of `collect` method chain. Array(resource.custom_action_buttons).collect(&:name).collect(&:downcase) end def validate_attr_selection(resource) physical_attrs, virtual_attrs = [], [] attrs = attribute_selection return [physical_attrs, virtual_attrs] if resource.kind_of?(Hash) || attrs == 'all' return [attrs, virtual_attrs] if (attrs - ID_ATTRS).empty? attrs.each do |attr| if attr_physical?(resource, attr) || attr == 'actions' physical_attrs.push(attr) elsif attr_virtual?(resource, attr) || @additional_attributes.try(:include?, attr) virtual_attrs.push(attr) end end attrs = attrs - physical_attrs - virtual_attrs raise BadRequestError, "Invalid attributes specified: #{attrs.join(',')}" unless attrs.empty? [(physical_attrs - ID_ATTRS).empty? ? [] : physical_attrs, virtual_attrs] end # # Let's expand a subcollection # def expand_subcollection(json, sc, sctype, subresources) if collection_config.show_as_collection?(sc) copts = { :counts => Api::QueryCounts.new(subresources.length), :is_subcollection => true, :expand_resources => @req.expand?(sc) } json.set! sc.to_s, collection_to_jbuilder(sc.to_sym, sctype, subresources, copts) elsif subresources.kind_of?(Hash) json.set!(sc, normalize_hash(sctype, subresources)) else sc_key_id = collection_config.resource_identifier(sctype) json.set! sc.to_s do |js| subresources.each do |scr| if @req.expand?(sc) || scr[sc_key_id].nil? add_child js, normalize_hash(sctype, scr) else js.child! { |jsc| jsc.href normalize_href(sctype, scr[sc_key_id]) } end end end end end Method `gen_action_spec_for_collections` has a Cognitive Complexity of 17 (exceeds 11 allowed). Consider refactoring. def gen_action_spec_for_collections(collection, cspec, is_subcollection, href) if is_subcollection target = :subcollection_actions cspec_target = collection_config.typed_subcollection_actions(@req.collection, collection) || cspec[target] else target = :collection_actions cspec_target = cspec[target] end return [] unless cspec_target cspec_target.each.collect do |method, action_definitions| next unless render_actions_for_method(cspec[:verbs], method) typed_action_definitions = fetch_typed_subcollection_actions(method, is_subcollection) || action_definitions typed_action_definitions.each.collect do |action| if api_user_role_allows?(action[:identifier]) {"name" => action[:name], "method" => method, "href" => (href ? href : collection)} end end end.flatten.compact end Method `gen_action_spec_for_resources` has a Cognitive Complexity of 15 (exceeds 11 allowed). Consider refactoring. def gen_action_spec_for_resources(cspec, is_subcollection, href, resource) if is_subcollection target = :subresource_actions cspec_target = cspec[target] || collection_config.typed_subcollection_actions(@req.collection, @req.subcollection, :subresource) else target = :resource_actions cspec_target = cspec[target] end return [] unless cspec_target cspec_target.each.collect do |method, action_definitions| next unless render_actions_for_method(cspec[:verbs], method) typed_action_definitions = action_definitions || fetch_typed_subcollection_actions(method, is_subcollection) typed_action_definitions.each.collect do |action| next unless api_user_role_allows?(action[:identifier]) && action_validated?(resource, action) build_resource_actions(action, method, href, cspec[:verbs]) end end.flatten.uniq.compact end def build_resource_actions(action, method, href, verbs) actions = [{"name" => action[:name], "method" => method, "href" => href}] if action[:name] == "edit" actions << { 'name' => 'edit', 'method' => :patch, 'href' => href } if verbs.include?(:patch) actions << { 'name' => 'edit', 'method' => :put, 'href' => href } if verbs.include?(:put) end actions end def render_actions_for_method(methods, method) method != :get && methods.include?(method) end def fetch_typed_subcollection_actions(method, is_subcollection) return unless is_subcollection collection_config.typed_subcollection_action(@req.collection, @req.subcollection, method) end def custom_api_user_role_allows_method?(_action_identifier) false end def api_user_role_allows?(action_identifier) return true unless action_identifier return custom_api_user_role_allows?(action_identifier) if custom_api_user_role_allows_method?(action_identifier) @role_allows_cache ||= {} Array(action_identifier).any? do |identifier| unless @role_allows_cache.key?(identifier) @role_allows_cache[identifier] = User.current_user.role_allows?(:identifier => identifier) end @role_allows_cache[identifier] end end def render_actions(physical_attrs) render_attr("actions") || physical_attrs.blank? end def action_validated?(resource, action_spec) if action_spec[:options] && action_spec[:options].include?(:validate_action) validate_method = "validate_#{action_spec[:name]}" return resource.respond_to?(validate_method) && resource.send(validate_method) end true end def render_options(resource, data = {}) klass = collection_class(resource) render :json => OptionsSerializer.new(klass, data).serialize end def render_resource_options(id, action) type = @req.collection.to_sym resource = resource_search(id, type) raise BadRequestError, resource.unsupported_reason(action) unless resource.supports?(action) schema = resource.send("params_for_#{action}".to_sym) render_options(type, :form_schema => schema) end def render_create_resource_options(ems_id) type = @req.collection.to_sym base_klass = collection_class(type) ems = resource_search(ems_id, :providers) klass = ems.class_by_ems(base_klass.name) raise BadRequestError, "No #{type.to_s.titleize} support for - #{ems.name}" unless klass raise BadRequestError, klass.unsupported_reason(:create) unless klass.supports?(:create) schema = klass.method(:params_for_create).arity == 0 ? klass.params_for_create : klass.params_for_create(ems) render_options(type, :form_schema => schema) end # This is a helper method used by both .determine_include_for_find and # .determine_extra_cols to collect and filter virtual_attributes for the # :include_for_find and :extra_cols options that are passed to Rbac. The # intent is to reduce a large amount of shared code between those two # shared methods by combining them into this one. # # The required block used by each of aforementioned methods is used to do # custom filtering that pertains to each of those methods. # def virtual_attributes_for(klass) return nil unless klass.respond_to?(:reflect_on_association) type = @req.subject results = [] validate_attr_selection(klass).last.each do |vattr| next if vattr == "href_slug" attr_name, attr_base = split_virtual_attribute(vattr) filtered_attr = yield type, attr_name, attr_base results << filtered_attr if filtered_attr end results.empty? ? nil : results end def attr_base_uses_rbac?(attr_base) attr_base.split(".").any? do |relation| relation_class = relation.singularize.classify Rbac::Filterer::CLASSES_THAT_PARTICIPATE_IN_RBAC.include?(relation_class) end end Method `determine_include_for_find` has a Cognitive Complexity of 22 (exceeds 11 allowed). Consider refactoring.
Cyclomatic complexity for determine_include_for_find is too high. [12/11] def determine_include_for_find(klass) attrs = virtual_attributes_for(klass) do |type, attr_name, attr_base| if klass.virtual_includes(attr_name) && !klass.attribute_supported_by_sql?(attr_name) && attr_base.blank? attr_name else next if attr_base.blank? next if virtual_attribute_accessor(type, attr_name) next if attr_base_uses_rbac?(attr_base) attr_base end end # Handle nested relationships and convert to a hash if attrs attrs.each_with_object({}) do |key, include_for_find| if (virtual_includes = klass.virtual_includes(key)) ActiveRecord::Base.merge_includes(include_for_find, virtual_includes) else nested = include_for_find key.split(".").each { |k| nested = nested[k] ||= {} } end end end end def determine_extra_cols(klass) virtual_attributes_for(klass) do |type, attr_name, attr_base| next if attr_base.present? next if virtual_attribute_accessor(type, attr_name) next unless klass.attribute_supported_by_sql?(attr_name) attr_name.to_sym end end # given a response to render, determine the proper return code # index pages have a response array of objects or hash that represents an object # show pages have resource as an object, or a hash representing an object # all of these pages just want to return ok # # the create and update pages are the ones we want to give a status # if there are multiple response entries, we just return ok # but if there is a single response, we'll use status to determine the resposne code # # So there are many ways we want to render and only a fraction of them will # use success to determine the return status - that is why so much short circuit logic # # @param [Object,Array[Object],Hash,Hash{String=>Array[Hash]}] resource # @return [Symbol] http status code def status_from_resource(resource) return :ok if @req.bulk? || !resource.kind_of?(Hash) return resource[:success] ? :ok : :bad_request if resource.key?(:success) results = resource["results"] if !results.kind_of?(Array) || !results.first.kind_of?(Hash) || !results.first.key?(:success) || results.first[:success] :ok else :bad_request end end end endend