class ModelApi::Utils

Constants

API_OPERATIONS
CAMELCASE_CONVERSION

Public Class Methods

add_ignored_field(ignored_fields, attr, value, attr_metadata) click to toggle source
# File lib/model-api/utils.rb, line 332
def add_ignored_field(ignored_fields, attr, value, attr_metadata)
  return unless ignored_fields.is_a?(Array)
  attr_metadata ||= {}
  external_attr = ext_attr(attr, attr_metadata)
  return unless external_attr.present?
  ignored_fields << { external_attr => value }
end
api_attrs(obj_or_class) click to toggle source
# File lib/model-api/utils.rb, line 14
def api_attrs(obj_or_class)
  return nil if obj_or_class.nil?
  klass = obj_or_class.is_a?(Class) ? obj_or_class : obj_or_class.class
  return expand_metadata(klass.api_attributes) if klass.respond_to?(:api_attributes)
  Hash[klass.column_names.map(&:to_sym).map { |attr| [attr, { attribute: attr }] }]
end
apply_updates(obj, req_obj, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 340
def apply_updates(obj, req_obj, operation, opts = {})
  opts = opts.merge(object: opts[:object] || obj)
  metadata = filtered_ext_attrs(opts[:api_attr_metadata] ||
      filtered_attrs(obj, operation, opts), operation, opts)
  set_context_attrs(obj, opts)
  req_obj.each do |attr, value|
    attr = attr.to_sym
    attr_metadata = metadata[attr]
    unless attr_metadata.present?
      add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
      next
    end
    update_api_attr(obj, attr, value, opts.merge(attr_metadata: attr_metadata))
  end
end
assoc_opts(assoc, attr_metadata, opts) click to toggle source
# File lib/model-api/utils.rb, line 171
def assoc_opts(assoc, attr_metadata, opts)
  contextual_metadata_opts(attr_metadata, opts.merge(association: assoc))
end
common_http_headers() click to toggle source
# File lib/model-api/utils.rb, line 252
def common_http_headers
  {
      'Cache-Control' => 'no-cache, no-store, max-age=0, must-revalidate',
      'Pragma' => 'no-cache',
      'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT'
  }
end
contextual_metadata_opts(attr_metadata, opts = {}) click to toggle source

Build options to generate metadata for a special context, e.g. for an object nested inside of a parent object.

# File lib/model-api/utils.rb, line 177
def contextual_metadata_opts(attr_metadata, opts = {})
  context_opts = opts
  if (obj_metadata = attr_metadata[:attributes]).present?
    context_opts = context_opts.merge(metadata: obj_metadata)
  end
  except_attrs = attr_metadata[:except_attrs] || []
  if (assoc = opts[:association]).present? &&
      ![:belongs_to, :has_and_belongs_to_many].include?(assoc.macro) &&
      !assoc.through_reflection.present?
    except_attrs << assoc.foreign_key.to_sym
  end
  if except_attrs.present?
    context_opts = context_opts.merge(except: (context_opts[:except] || []) +
        except_attrs.compact.map(&:to_sym).uniq)
  end
  context_opts
end
eval_bool(obj, expr, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 112
def eval_bool(obj, expr, opts = {})
  if expr.is_a?(Hash) && opts.blank?
    obj = nil
    opts = expr
  end
  if expr.respond_to?(:call)
    return invoke_callback(expr, *([obj, opts].compact)) ? true : false
  end
  expr ? true : false
end
eval_can(criteria, context, action_type, controller) click to toggle source
# File lib/model-api/utils.rb, line 106
def eval_can(criteria, context, action_type, controller)
  return false unless criteria.present? && context.present? &&
      action_type.present? && controller.respond_to?(:can?)
  controller.can?(criteria, context)
end
ext_attr(attr, attr_metadata = {}) click to toggle source
# File lib/model-api/utils.rb, line 58
def ext_attr(attr, attr_metadata = {})
  sym = attr.is_a?(Symbol)
  ext_attr = attr_metadata[:alias] || attr
  ext_attr = ext_attr.to_s.camelize(:lower) if CAMELCASE_CONVERSION
  sym ? ext_attr.to_sym : ext_attr.to_s
end
ext_query_attr(attr, attr_metadata = {}) click to toggle source
# File lib/model-api/utils.rb, line 65
def ext_query_attr(attr, attr_metadata = {})
  sym = attr.is_a?(Symbol)
  ext_attr = attr_metadata[:alias] || attr
  ext_attr = ext_attr.to_s.underscore
  sym ? ext_attr.to_sym : ext_attr.to_s
end
ext_value(value, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 72
def ext_value(value, opts = {})
  return value unless CAMELCASE_CONVERSION
  if value.is_a?(Hash)
    ext_hash(value, opts)
  elsif value.respond_to?(:map)
    value.map { |v| ext_value(v, opts) }
  else
    value
  end
end
extract_error_msgs(obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 356
def extract_error_msgs(obj, opts = {})
  object_errors = []
  attr_prefix = opts[:attr_prefix] || ''
  api_metadata = opts[:api_attr_metadata] || api_attrs(obj.class)
  obj.errors.each do |attr, attr_errors|
    attr_errors = [attr_errors] unless attr_errors.is_a?(Array)
    attr_errors.each do |error|
      attr_metadata = api_metadata[attr] || {}
      qualified_attr = "#{attr_prefix}#{ext_attr(attr, attr_metadata)}"
      assoc_errors = nil
      if attr_metadata[:type] == :association
        assoc_errors = extract_assoc_error_msgs(obj, attr, opts.merge(
            attr_metadata: attr_metadata))
      end
      if assoc_errors.present?
        object_errors += assoc_errors
      else
        error_hash = {}
        error_hash[:object] = attr_prefix if attr_prefix.present?
        error_hash[:attribute] = qualified_attr unless attr == :base
        object_errors << error_hash.merge(error: error,
            message: (attr == :base ? error : "#{qualified_attr} #{error}"))
      end
    end
  end
  object_errors
end
filtered_attrs(obj_or_class, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 21
def filtered_attrs(obj_or_class, operation, opts = {})
  return nil if obj_or_class.nil?
  klass = obj_or_class.is_a?(Class) ? obj_or_class : obj_or_class.class
  filtered_metadata(api_attrs(klass), klass, operation, opts)
end
filtered_ext_attrs(metadata, operation = nil, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 27
def filtered_ext_attrs(metadata, operation = nil, opts = {})
  if operation.is_a?(Hash) && opts.blank?
    opts = operation
    operation = opts[:operation] || :show
  end
  if metadata.is_a?(ActiveRecord::Base) || (metadata.is_a?(Class) &&
      metadata < ActiveRecord::Base)
    metadata = filtered_attrs(metadata, operation, opts)
  end
  return metadata unless metadata.is_a?(Hash) && metadata.present?
  if [:filter, :sort].include?(operation)
    return Hash[metadata.map { |a, m| [ext_query_attr(a, m), m] }]
  end
  Hash[metadata.map { |a, m| [ext_attr(a, m), m] }]
end
find_class(obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 465
def find_class(obj, opts = {})
  return nil if obj.nil?
  opts[:class] || (obj.respond_to?(:klass) ? obj.klass : obj.class)
end
format_value(value, attr_metadata, opts) click to toggle source
# File lib/model-api/utils.rb, line 218
def format_value(value, attr_metadata, opts)
  transform_value(value, attr_metadata[:render], opts)
rescue Exception => e
  Rails.logger.warn 'Error encountered formatting API output ' \
        "(\"#{e.message}\") for value: \"#{value}\"" \
        ' ... rendering unformatted value instead.'
  value
end
http_status(status) click to toggle source
# File lib/model-api/utils.rb, line 150
def http_status(status)
  unless status.is_a?(Fixnum)
    status_num = status.to_s.to_i
    if status_num.to_s != status.to_s
      status_sym = status.to_s.to_sym
      unless Rack::Utils::SYMBOL_TO_STATUS_CODE.include?(status_sym)
        fail "Invalid / unrecognized HTTP status: #{status_sym}"
      end
      return status_sym
    end
    status = status_num
  end
  status_string = Rack::Utils::HTTP_STATUS_CODES[status]
  fail "Invalid / unrecognized HTTP status code: #{status}" unless status_string.present?
  status_string.downcase.gsub(/\s|-/, '_').to_sym
end
http_status_code(status) click to toggle source
# File lib/model-api/utils.rb, line 134
def http_status_code(status)
  unless status.is_a?(Symbol)
    status_num = status.to_s.to_i
    if status_num.to_s == status.to_s
      unless Rack::Utils::HTTP_STATUS_CODES.include?(status_num)
        fail "Invalid / unrecognized HTTP status code: #{status_num}"
      end
      return status_num
    end
    status = status.to_s.to_sym
  end
  status_code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status]
  fail "Invalid / unrecognized HTTP status: #{status}" unless status_code.present?
  status_code
end
internal_value(value, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 83
def internal_value(value, opts = {})
  return value unless CAMELCASE_CONVERSION
  if value.is_a?(Hash)
    internal_hash(value, opts)
  elsif value.respond_to?(:map)
    value.map { |v| internal_value(v, opts) }
  else
    value
  end
end
invoke_callback(callback, *params) click to toggle source
# File lib/model-api/utils.rb, line 242
def invoke_callback(callback, *params)
  return nil unless callback.respond_to?(:call)
  callback_param_count = callback.parameters.size
  if params.size >= callback_param_count + 1 && (last_param = params.last).is_a?(Hash)
    # Automatically pass duplicate of final hash param (to prevent data corruption)
    params = params[0..-2] + [last_param.dup]
  end
  callback.send(*(([:call] + params)[0..callback.parameters.size]))
end
model_metadata(klass) click to toggle source
# File lib/model-api/utils.rb, line 208
def model_metadata(klass)
  return klass.api_model if klass.respond_to?(:api_model)
  {}
end
model_name(klass) click to toggle source
# File lib/model-api/utils.rb, line 213
def model_name(klass)
  model_alias = (model_metadata(klass) || {})[:alias]
  ActiveModel::Name.new(klass, nil, model_alias.present? ? model_alias.to_s : nil)
end
not_found_response_body(opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 227
def not_found_response_body(opts = {})
  response =
      {
          successful: false,
          status: :not_found,
          status_code: http_status_code(:not_found),
          errors: [{
              error: opts[:error] || 'No resource found',
              message: opts[:message] || 'No resource found at the path ' \
              'provided or matching the criteria specified'
          }]
      }
  response.to_json(opts)
end
parse_request_body(request) click to toggle source
# File lib/model-api/utils.rb, line 43
def parse_request_body(request)
  request_body = request.body.read.to_s.strip
  parsed_request_body = nil
  if request.env['API_CONTENT_TYPE'] == :xml ||
      request_body.start_with?('<')
    parsed_request_body = Hash.from_xml(request_body) rescue nil
    detected_format = :xml
  end
  unless parsed_request_body.present?
    parsed_request_body = JSON.parse(request_body) rescue nil
    detected_format = :json
  end
  [parsed_request_body, detected_format]
end
process_collection_includes(collection, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 409
def process_collection_includes(collection, opts = {})
  klass = find_class(collection, opts)
  metadata = filtered_ext_attrs(klass, opts[:operation] || :index, opts)
  model_metadata = opts[:model_metadata] || model_metadata(klass)
  includes = []
  if (metadata_includes = model_metadata[:collection_includes]).present?
    meta_includes = [meta_includes] unless meta_includes.is_a?(Array)
    includes += metadata_includes
  end
  metadata.each do |_attr, attr_metadata|
    includes << attr_metadata[:key] if attr_metadata[:type] == :association
  end
  includes = includes.compact.uniq
  includes = remove_excluded_associations(includes, opts)
  collection = collection.includes(includes) if includes.present?
  collection
end
query_by_id_attrs(id_attributes, assoc, assoc_payload, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 312
def query_by_id_attrs(id_attributes, assoc, assoc_payload, opts = {})
  return nil unless id_attributes.present? && opts[:api_context].present?
  assoc_class = assoc.class_name.constantize
  attr_metadata = filtered_attrs(assoc_class, opts[:operation] || :update, opts)
  id_attributes.each do |id_attr_set|
    query = nil
    id_attr_set.each do |id_attr|
      ext_attr = attr_metadata[id_attr].try(:[], :alias) || id_attr
      unless ext_attr.present? && assoc_payload.include?(ext_attr.to_s)
        query = nil
        break
      end
      query = (query || opts[:api_context].api_query(assoc_class, opts))
          .where(id_attr => assoc_payload[ext_attr.to_s])
    end
    return query unless query.nil?
  end
  nil
end
remove_association_from_array(array_collection, association) click to toggle source

If the association is present in array form (in collection_includes) delete it. Then loop over the array elements to remove the association incase of nested associations.

# File lib/model-api/utils.rb, line 441
def remove_association_from_array(array_collection, association)
  array_collection.delete(association)
  array_collection.each_with_index do |collection, i|
    if collection.is_a?(Hash)
      array_collection[i] = remove_association_from_hash(collection, association)
    end
  end
  array_collection
end
remove_association_from_hash(hash_collection, association) click to toggle source

Incase of nested associations (in collection_inlcudes) delete the key with the association name and then loop over the collection to delete associations present as element of the array if any

# File lib/model-api/utils.rb, line 453
def remove_association_from_hash(hash_collection, association)
  if hash_collection.keys.include?(association)
    hash_collection.delete(association)
  else
    hash_collection.each do |key, value|
      hash_collection[key] =
        remove_association_from_array(value, association) if value.is_a?(Array)
    end
  end
  hash_collection
end
remove_excluded_associations(includes, opts) click to toggle source

Does not eager load associations mentioned in the exclude_associations array defined by user.

# File lib/model-api/utils.rb, line 429
def remove_excluded_associations(includes, opts)
  includes_dup = includes.compact.uniq.deep_dup
  exclude_associations = opts[:exclude_associations]
  return includes_dup unless exclude_associations.present?
  exclude_associations.each do |association|
    remove_association_from_array(includes_dup, association.to_sym)
  end
  includes_dup
end
resolve_assoc_obj(parent_obj, assoc, assoc_payload, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 276
def resolve_assoc_obj(parent_obj, assoc, assoc_payload, opts = {})
  klass = parent_obj.class
  assoc = klass.reflect_on_association(assoc) if assoc.is_a?(Symbol) || assoc.is_a?(String)
  fail "Unrecognized association '#{assoc}' on class '#{klass.name}'" if assoc.nil?
  model_metadata = model_metadata(assoc.class_name.constantize)
  do_resolve_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj, nil, opts)
end
response_successful?(response_status) click to toggle source
# File lib/model-api/utils.rb, line 167
def response_successful?(response_status)
  http_status_code(response_status) < 400
end
route_name(request) click to toggle source
# File lib/model-api/utils.rb, line 7
def route_name(request)
  Rails.application.routes.router.recognize(request) do |route, _matches, _parameters|
    return route.name
  end
  nil
end
save_obj(obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 384
def save_obj(obj, opts = {})
  operation = opts[:operation] || (obj.new_record? ? :create : :update)
  model_metadata = opts.delete(:model_metadata) || model_metadata(obj.class)
  before_validate_callbacks(model_metadata, obj, opts)
  validate_operation(obj, operation, opts.merge(model_metadata: model_metadata))
  validate_preserving_existing_errors(obj)
  new_obj = obj.new_record?
  before_save_callbacks(model_metadata, obj, new_obj, opts)
  obj.instance_variable_set(:@readonly, false) if obj.instance_variable_get(:@readonly)
  successful = obj.save unless obj.errors.present?
  after_save_callbacks(model_metadata, obj, new_obj, opts) if successful
  successful
end
set_open_api_type_and_format(properties, type_name) click to toggle source
# File lib/model-api/utils.rb, line 195
def set_open_api_type_and_format(properties, type_name)
  open_api_type, open_api_format = OpenApi::Utils.open_api_type_and_format(type_name)
  if open_api_type.nil?
    open_api_type = :string
    open_api_format = type_name
  end
  if open_api_type.present?
    properties[:type] = open_api_type
    properties[:format] = open_api_format if open_api_format.present?
  end
  properties
end
transform_value(value, transform_method_or_proc, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 123
def transform_value(value, transform_method_or_proc, opts = {})
  return value unless transform_method_or_proc.present?
  if (transform_method_or_proc.is_a?(String) || transform_method_or_proc.is_a?(Symbol)) &&
      value.respond_to?(transform_method_or_proc.to_sym)
    return value.send(transform_method_or_proc.to_sym)
  end
  value = value.symbolize_keys if value.is_a?(Hash)
  return value unless transform_method_or_proc.respond_to?(:call)
  invoke_callback(transform_method_or_proc, value, opts)
end
translate_external_api_filter(controller, opts = {}, &block) click to toggle source

Transforms request and response to match conventions (i.e. using camelcase attrs if configured, and the standard response envelope)

# File lib/model-api/utils.rb, line 262
def translate_external_api_filter(controller, opts = {}, &block)
  request = controller.request
  json, _format = parse_request_body(request)
  json = internal_value(json)
  json.each { |k, v| request.parameters[k] = request.POST[k] = v } if json.is_a?(Hash)

  block.call

  obj = controller.response_body.first if controller.response_body.is_a?(Array)
  obj = (JSON.parse(obj) rescue nil) if obj.present?
  opts = opts.merge(generate_body_only: true)
  controller.response_body = [ModelApi::Renderer.render(controller, ext_value(obj), opts)]
end
update_api_attr(obj, attr, value, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 284
def update_api_attr(obj, attr, value, opts = {})
  attr = attr.to_sym
  attr_metadata = get_attr_metadata(obj, attr, opts)
  begin
    value = transform_value(value, attr_metadata[:parse], opts)
  rescue Exception => e
    Rails.logger.warn "Error encountered parsing API input for attribute \"#{attr}\" " \
            "(\"#{e.message}\"): \"#{value.to_s.first(1000)}\" ... using raw value instead."
  end
  begin
    if attr_metadata[:type] == :association && attr_metadata[:parse].blank?
      attr_metadata = opts[:attr_metadata]
      assoc = attr_metadata[:association]
      if assoc.macro == :has_many
        update_one_to_many_assoc(obj, attr, value, opts)
      elsif [:belongs_to, :has_one].include?(assoc.macro)
        update_one_to_one_assoc(obj, attr, value, opts)
      else
        add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
      end
    else
      set_api_attr(obj, attr, value, opts)
    end
  rescue Exception => e
    handle_api_setter_exception(e, obj, attr_metadata, opts)
  end
end
validate_operation(obj, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 398
def validate_operation(obj, operation, opts = {})
  klass = find_class(obj, opts)
  model_metadata = opts[:model_metadata] || model_metadata(klass)
  return nil unless operation.present?
  if obj.nil?
    invoke_callback(model_metadata[:"validate_#{operation}"], opts)
  else
    invoke_callback(model_metadata[:"validate_#{operation}"], obj, opts)
  end
end

Private Class Methods

action_filter(klass, filter_value, test_value, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 472
def action_filter(klass, filter_value, test_value, opts = {})
  filter_type = opts[:filter_type] || :operation
  if test_value.is_a?(Array)
    return test_value.map(&:to_sym).include?(filter_value)
  elsif test_value.is_a?(Hash)
    test_value = test_value[filter_value]
  end
  if test_value.respond_to?(:call)
    return invoke_callback(test_value, klass, opts.merge(filter_type => filter_value))
  end
  filter_value == test_value
end
after_save_callbacks(model_metadata, obj, new_obj, opts) click to toggle source
# File lib/model-api/utils.rb, line 847
def after_save_callbacks(model_metadata, obj, new_obj, opts)
  invoke_callback(model_metadata[:after_create], obj, opts) if new_obj
  invoke_callback(opts[:after_create], obj, opts) if new_obj
  invoke_callback(model_metadata[:after_save], obj, opts)
  invoke_callback(opts[:after_save], obj, opts)
end
before_save_callbacks(model_metadata, obj, new_obj, opts) click to toggle source
# File lib/model-api/utils.rb, line 840
def before_save_callbacks(model_metadata, obj, new_obj, opts)
  invoke_callback(model_metadata[:before_create], obj, opts) if new_obj
  invoke_callback(opts[:before_create], obj, opts) if new_obj
  invoke_callback(model_metadata[:before_save], obj, opts)
  invoke_callback(opts[:before_save], obj, opts)
end
before_validate_callbacks(model_metadata, obj, opts) click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/model-api/utils.rb, line 834
def before_validate_callbacks(model_metadata, obj, opts)

  invoke_callback(model_metadata[:before_validate], obj, opts)
  invoke_callback(opts[:before_validate], obj, opts)
end
do_resolve_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj, existing_assoc_obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 705
def do_resolve_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj, existing_assoc_obj,
    opts = {})
  return nil unless assoc_payload.present?
  if opts[:resolve].try(:respond_to?, :call)
    assoc_obj = invoke_callback(opts[:resolve], assoc_payload, opts.merge(
        parent: parent_obj, association: assoc, association_metadata: model_metadata))
  elsif (assoc_obj = existing_assoc_obj).blank?
    assoc_obj = query_by_id_attrs(model_metadata[:id_attributes], assoc, assoc_payload, opts)
    assoc_obj = (assoc_obj.count != 1 ? nil : assoc_obj.first) unless assoc_obj.nil?
    assoc_obj ||= assoc.class_name.constantize.new if opts[:auto_create]
  end
  assoc_obj
end
exclude_associations_metadata(metadata, obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 501
def exclude_associations_metadata(metadata, obj, opts = {})
  exclude_associations = opts[:exclude_associations].try(:map, &:to_sym)
  if exclude_associations.present?
    (exclude_associations.include?(obj.table_name) || exclude_associations.include?(metadata[:key]))
  end
end
expand_metadata(metadata) click to toggle source
# File lib/model-api/utils.rb, line 534
def expand_metadata(metadata)
  Hash[metadata.map { |k, v| [k, v.is_a?(Hash) ? v : { value: v, read_only: true }] }]
end
ext_hash(hash, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 538
def ext_hash(hash, opts = {})
  return hash unless CAMELCASE_CONVERSION
  Hash[hash.map do |key, value|
    sym = key.is_a?(Symbol)
    key = key.to_s.camelize(:lower)
    value = ext_value(value, opts)
    [sym ? key.to_sym : key, value]
  end]
end
extract_assoc_error_msgs(obj, attr, opts) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/model-api/utils.rb, line 792
def extract_assoc_error_msgs(obj, attr, opts)
  object_errors = []
  attr_metadata = opts[:attr_metadata] || {}
  processed_assoc_objects = {}
  assoc = attr_metadata[:association]
  external_attr = ext_attr(attr, attr_metadata)
  attr_metadata_create = attr_metadata_update = nil
  if assoc.macro == :has_many
    obj.send(attr).each_with_index do |assoc_obj, index|
      next if processed_assoc_objects[assoc_obj]
      processed_assoc_objects[assoc_obj] = true
      attr_prefix = "#{external_attr}[#{index}]."
      if assoc_obj.new_record?
        attr_metadata_create ||= filtered_attrs(assoc.class_name.constantize, :create, opts)
        object_errors += extract_error_msgs(assoc_obj, opts.merge(
            attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
      else
        attr_metadata_update ||= filtered_attrs(assoc.class_name.constantize, :update, opts)
        object_errors += extract_error_msgs(assoc_obj, opts.merge(
            attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
      end
    end
  else
    assoc_obj = obj.send(attr)
    return object_errors unless assoc_obj.present? && !processed_assoc_objects[assoc_obj]
    processed_assoc_objects[assoc_obj] = true
    attr_prefix = "#{external_attr}->"
    if assoc_obj.new_record?
      attr_metadata_create ||= filtered_attrs(assoc.class_name.constantize, :create, opts)
      object_errors += extract_error_msgs(assoc_obj, opts.merge(
          attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_create))
    else
      attr_metadata_update ||= filtered_attrs(assoc.class_name.constantize, :update, opts)
      object_errors += extract_error_msgs(assoc_obj, opts.merge(
          attr_prefix: attr_prefix, api_attr_metadata: attr_metadata_update))
    end
  end
  object_errors
end
filtered_metadata(metadata, klass, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 485
def filtered_metadata(metadata, klass, operation, opts = {})
  if (only = opts[:only]).present?
    metadata.select! { |k, _v| only.include?(k) }
  end
  if (except = opts[:except]).present?
    metadata.reject! { |k, _v| except.include?(k) }
  end
  if (metadata_overrides = opts[:metadata]).present?
    metadata = merge_metadata_overrides(metadata, metadata_overrides)
  end
  metadata.select! do |_attr, attr_metadata|
    include_item?(attr_metadata, klass, operation, opts)
  end
  metadata
end
get_attr_metadata(obj, attr, opts) click to toggle source
# File lib/model-api/utils.rb, line 757
def get_attr_metadata(obj, attr, opts)
  attr_metadata = opts[:attr_metadata]
  return attr_metadata unless attr_metadata.nil?
  operation = opts[:operation] || :update
  metadata = filtered_ext_attrs(opts[:api_attr_metadata] ||
      filtered_attrs(obj, operation, opts), operation, opts)
  metadata[attr] || {}
end
handle_api_setter_exception(e, obj, attr_metadata, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 736
def handle_api_setter_exception(e, obj, attr_metadata, opts = {})
  return unless attr_metadata.is_a?(Hash)
  on_exception = attr_metadata[:on_exception]
  fail e unless on_exception.present?
  on_exception = { Exception => on_exception } unless on_exception.is_a?(Hash)
  on_exception.each do |klass, handler|
    klass = klass.to_s.constantize rescue nil unless klass.is_a?(Class)
    next unless klass.is_a?(Class) && e.is_a?(klass)
    if handler.respond_to?(:call)
      invoke_callback(handler, obj, e, opts)
    elsif handler.present?
      # Presume handler is an error message in this case
      obj.errors.add(attr_metadata[:key], handler.to_s)
    else
      add_ignored_field(opts[:ignored_fields], nil, opts[:value],
          attr_metadata)
    end
    break
  end
end
include_item?(metadata, obj, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 508
def include_item?(metadata, obj, operation, opts = {})
  return false unless metadata.is_a?(Hash)
  return false unless include_item_meets_admin_criteria?(metadata, obj, operation, opts)
  # Stop here re: filter/sort params, as following checks involve payloads/responses only
  return eval_bool(obj, metadata[:filter], opts) if operation == :filter
  return eval_bool(obj, metadata[:sort], opts) if operation == :sort
  return false unless include_item_meets_read_write_criteria?(metadata, obj, operation, opts)
  return false unless include_item_meets_incl_excl_criteria?(metadata, obj, operation, opts)
  return false if exclude_associations_metadata(metadata, obj, opts)
  true
end
include_item_meets_admin_criteria?(metadata, obj, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 558
def include_item_meets_admin_criteria?(metadata, obj, operation, opts = {})
  if eval_bool(obj, metadata[:admin_only], opts)
    return true if opts[:admin]
    return false unless [:create, :update, :patch].include?(operation)
    return opts[:admin_user] ? true : false
  end
  return false if eval_bool(obj, metadata[:admin_content], opts) && !opts[:admin_content]
  true
end
include_item_meets_incl_excl_criteria?(metadata, obj, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 577
def include_item_meets_incl_excl_criteria?(metadata, obj, operation, opts = {})
  if (only = metadata[:only]).present?
    return false unless action_filter(obj, operation, only, opts)
  end
  if (except = metadata[:except]).present?
    return false if action_filter(obj, operation, except, opts)
  end
  action = opts[:action]
  if (only = metadata[:only_actions]).present?
    return false unless action_filter(obj, action, only, opts.merge(filter_type: :action))
  end
  if (except = metadata[:except_actions]).present?
    return false if action_filter(obj, action, except, opts.merge(filter_type: :action))
  end
  true
end
include_item_meets_read_write_criteria?(metadata, obj, operation, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 568
def include_item_meets_read_write_criteria?(metadata, obj, operation, opts = {})
  if [:create, :update, :patch].include?(operation)
    return false if eval_bool(obj, metadata[:read_only], opts)
  else
    return false if eval_bool(obj, metadata[:write_only], opts)
  end
  true
end
internal_hash(hash, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 548
def internal_hash(hash, opts = {})
  return hash unless CAMELCASE_CONVERSION
  Hash[hash.map do |key, value|
    sym = key.is_a?(Symbol)
    key = key.to_s.underscore
    value = internal_value(value, opts)
    [sym ? key.to_sym : key, value]
  end]
end
merge_metadata_overrides(metadata, metadata_overrides) click to toggle source
# File lib/model-api/utils.rb, line 520
def merge_metadata_overrides(metadata, metadata_overrides)
  if metadata_overrides.is_a?(Hash)
    Hash[
        expand_metadata(metadata_overrides).map do |key, item_metadata|
          [key, item_metadata.reverse_merge(metadata[key] || {}).reverse_merge(key: key)]
        end
    ]
  elsif metadata_overrides.is_a?(Array)
    metadata.select { |key, _m| metadata_overrides.include?(key) }
  else
    metadata
  end
end
resolve_one_to_many_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 646
def resolve_one_to_many_assoc_obj(model_metadata, assoc, assoc_payload,
    parent_obj, opts = {})
  assoc_obj = do_resolve_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj, nil,
      opts.merge(auto_create: true))
  return [nil, nil, nil] if assoc_obj.nil?
  if assoc_obj.new_record?
    assoc_oper = :create
    opts[:create_opts] ||= opts.merge(api_attr_metadata: filtered_attrs(
        assoc.class_name.constantize, :create, opts))
    assoc_opts = opts[:create_opts]
  else
    assoc_oper = :update
    opts[:update_opts] ||= opts.merge(api_attr_metadata: filtered_attrs(
        assoc.class_name.constantize, :update, opts))

    assoc_opts = opts[:update_opts]
  end
  [assoc_obj, assoc_oper, assoc_opts]
end
resolve_one_to_one_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj, existing_assoc_obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 693
def resolve_one_to_one_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj,
    existing_assoc_obj, opts = {})
  assoc_opts = opts[:ignored_fields].is_a?(Array) ? opts.merge(ignored_fields: []) : opts
  assoc_obj = do_resolve_assoc_obj(model_metadata, assoc, assoc_payload, parent_obj,
      existing_assoc_obj, opts.merge(auto_create: true))
  return [nil, nil, nil] if assoc_obj.nil?
  assoc_oper = assoc_obj.new_record? ? :create : :update
  assoc_opts = assoc_opts.merge(
      api_attr_metadata: filtered_attrs(assoc.class_name.constantize, assoc_oper, opts))
  return [assoc_obj, assoc_oper, assoc_opts]
end
set_api_attr(obj, attr, value, opts) click to toggle source
# File lib/model-api/utils.rb, line 719
def set_api_attr(obj, attr, value, opts)
  attr = attr.to_sym
  attr_metadata = get_attr_metadata(obj, attr, opts)
  internal_field = attr_metadata[:key] || attr
  setter = opts[:setter] || attr_metadata[:setter] || "#{(internal_field)}="
  if setter.respond_to?(:call)
    ModelApi::Utils.invoke_callback(setter, value, opts.merge(parent: obj, attribute: attr))
  elsif !obj.respond_to?(setter)
    Rails.logger.warn "Error encountered assigning API input for attribute \"#{attr}\" " \
            '(setter not found): skipping.'
    add_ignored_field(opts[:ignored_fields], attr, value, attr_metadata)
    return
  else
    obj.send(setter, value)
  end
end
set_context_attrs(obj, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 766
def set_context_attrs(obj, opts = {})
  klass = (obj.class < ActiveRecord::Base ? obj.class : nil)
  (opts[:context] || {}).each do |key, value|
    begin
      setter = "#{key}="
      next unless obj.respond_to?(setter)
      if (column = klass.try(:columns_hash).try(:[], key.to_s)).present?
        case column.type
        when :integer, :primary_key then
          obj.send("#{key}=", value.to_i)
        when :decimal, :float then
          obj.send("#{key}=", value.to_f)
        else
          obj.send(setter, value.to_s)
        end
      else
        obj.send(setter, value.to_s)
      end
    rescue Exception => e
      Rails.logger.warn "Error encountered assigning context parameter #{key} to " \
        "'#{value}' (skipping): \"#{e.message}\")."
    end
  end
end
update_one_to_many_assoc(obj, attr, value, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 594
def update_one_to_many_assoc(obj, attr, value, opts = {})
  attr_metadata = opts[:attr_metadata]
  assoc = attr_metadata[:association]
  model_metadata = model_metadata(assoc.class_name.constantize)
  value_array = value.to_a rescue nil
  unless value_array.is_a?(Array)
    obj.errors.add(attr, 'must be supplied as an array of objects')
    return
  end
  opts = opts.merge(model_metadata: model_metadata)
  opts[:ignored_fields] = [] if opts.include?(:ignored_fields)
  assoc_objs = []
  value_array.each_with_index do |assoc_payload, index|
    opts[:ignored_fields].clear if opts.include?(:ignored_fields)
    assoc_obj = update_one_to_many_assoc_obj(obj, assoc, assoc_payload,
        opts.merge(model_metadata: model_metadata))
    next if assoc_obj.nil?
    assoc_objs << assoc_obj
    if opts[:ignored_fields].present?
      external_attr = ext_attr(attr, attr_metadata)
      opts[:ignored_fields] << { "#{external_attr}[#{index}]" => opts[:ignored_fields] }
    end
  end
  assoc_objs = assoc_objs.each do |assoc_obj|
    if assoc_obj.new_record? || assoc_obj != existing_assoc_obj
      set_api_attr(obj, attr, assoc_obj.attributes, opts.merge(setter: ->(v, _opts) {
        obj.send(assoc.name).build(v)
      }))
    else
      assoc_obj.save
      assoc_obj
    end
  end
end
update_one_to_many_assoc_obj(parent_obj, assoc, assoc_payload, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 629
def update_one_to_many_assoc_obj(parent_obj, assoc, assoc_payload, opts = {})
  model_metadata = opts[:model_metadata] || model_metadata(assoc.class_name.constantize)
  assoc_obj, assoc_oper, assoc_opts = resolve_one_to_many_assoc_obj(model_metadata, assoc,
      assoc_payload, parent_obj, opts)
  return nil if assoc_obj.nil?
  if (inverse_assoc = assoc.options[:inverse_of]).present? &&
      assoc_obj.respond_to?("#{inverse_assoc}=")
    assoc_obj.send("#{inverse_assoc}=", parent_obj)
  elsif !parent_obj.new_record? && assoc_obj.respond_to?("#{assoc.foreign_key}=")
    assoc_obj.send("#{assoc.foreign_key}=", parent_obj.id)
  end
  apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
  invoke_callback(model_metadata[:after_initialize], assoc_obj,
      assoc_opts.merge(operation: assoc_oper))
  assoc_obj
end
update_one_to_one_assoc(parent_obj, attr, assoc_payload, opts = {}) click to toggle source
# File lib/model-api/utils.rb, line 666
def update_one_to_one_assoc(parent_obj, attr, assoc_payload, opts = {})
  unless assoc_payload.is_a?(Hash)
    parent_obj.errors.add(attr, 'must be supplied as an object')
    return
  end
  attr_metadata = opts[:attr_metadata]
  assoc = attr_metadata[:association]
  model_metadata = model_metadata(assoc.class_name.constantize)
  existing_assoc_obj = parent_obj.send(assoc.name) rescue nil
  assoc_obj, assoc_oper, assoc_opts = resolve_one_to_one_assoc_obj(model_metadata, assoc,
      assoc_payload, parent_obj, existing_assoc_obj, opts)
  return if assoc_obj.nil?
  apply_updates(assoc_obj, assoc_payload, assoc_oper, assoc_opts)
  invoke_callback(model_metadata[:after_initialize], assoc_obj,
      opts.merge(operation: assoc_oper))
  if assoc_opts[:ignored_fields].present?
    external_attr = ext_attr(attr, attr_metadata)
    opts[:ignored_fields] << { external_attr.to_s => assoc_opts[:ignored_fields] }
  end
  if assoc_obj.new_record? || assoc_obj != existing_assoc_obj
    set_api_attr(parent_obj, attr, assoc_obj.attributes,
        opts.merge(setter: "build_#{attr_metadata[:key] || attr}"))
  else
    assoc_obj.save
  end
end
validate_preserving_existing_errors(obj) click to toggle source
# File lib/model-api/utils.rb, line 854
def validate_preserving_existing_errors(obj)
  if obj.errors.present?
    errors = obj.errors.messages.dup
    obj.valid?
    errors = obj.errors.messages.merge(errors)
    obj.errors.clear
    errors.each { |field, error| obj.errors.add(field, error) }
  else
    obj.valid?
  end
end