ManageIQ/manageiq-api

View on GitHub
app/controllers/api/base_controller/renderer.rb

Summary

Maintainability
C
7 hrs
Test Coverage
A
100%
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
end
end