module ModelApi::Renderer

Public Class Methods

render(controller, response_obj, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 6
def render(controller, response_obj, opts = {})
  opts = opts.symbolize_keys
  format = (opts[:format] ||= get_format(controller))
  opts[:action] ||= controller.action_name.to_sym
  if format == :xml
    render_xml_response(response_obj, controller, opts)
  else
    render_json_response(response_obj, controller, opts)
  end
end

Private Class Methods

attrs_by_type(obj, metadata) click to toggle source
# File lib/model-api/renderer.rb, line 33
def attrs_by_type(obj, metadata)
  render_values = []
  render_assoc = []
  metadata.each do |attr, attr_metadata|
    if !(value = attr_metadata[:value]).nil?
      render_values << [attr, value, attr_metadata]
    elsif obj.respond_to?(attr.to_s)
      render_values << [attr, obj.send(attr.to_sym), attr_metadata]
    elsif (assoc = obj.class.reflect_on_association(attr)).present?
      render_assoc << [assoc, attr_metadata]
    elsif obj.is_a?(ActiveRecord::Base)
      fail "Invalid API attribute for #{obj.class.model_name.human} instance: #{attr}"
    else
      fail "Invalid API attribute for #{obj.class.name} instance: #{attr}"
    end
  end
  [render_values, render_assoc]
end
build_hateoas_route_args(controller, route, link_opts, _opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 290
def build_hateoas_route_args(controller, route, link_opts, _opts = {})
  link_opts ||= {}
  format = controller.request.format.symbol rescue nil
  link_opts[:format] ||= format if format.present?
  if route.is_a?(Array)
    if route.size > 1 && (route_opts = route.last).is_a?(Hash)
      route.first(route.size - 1).append(link_opts.merge(route_opts))
    else
      route.append(link_opts)
    end
  else
    [route, link_opts]
  end
end
build_object_hateoas_route_args(obj, controller, route, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 239
def build_object_hateoas_route_args(obj, controller, route, opts = {})
  id_attr = opts[:id_attribute] || :id
  link_opts = (opts[:object_link_options] || {}).dup
  format = controller.request.format.symbol rescue nil
  link_opts[:format] ||= format if format.present?
  if route.is_a?(Array)
    if route.size > 1 && (route_opts = route.last).is_a?(Hash)
      route.first(route.size - 1).append(link_opts.merge(route_opts))
    else
      route + [object.send(id_attr), link_opts]
    end
  else
    id = obj.is_a?(Hash) ? obj[id_attr] : obj.send(id_attr)
    path_params = Rails.application.routes.routes.named_routes[route.to_s].parts rescue []
    path_params.each do |param|
      param = param.to_sym
      next if link_opts.include?(param)
      value = controller.params[param]
      link_opts[param] = value if value.present?
    end
    [route, { (opts[:id_param] || :id) => id }.merge(link_opts)]
  end
end
build_response_obj_json(response_obj, controller, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 442
def build_response_obj_json(response_obj, controller, opts = {})
  if !response_obj.nil?
    if response_obj.is_a?(ActiveRecord::Base)
      root_elem_json = ModelApi::Utils.ext_attr(get_object_root_elem(response_obj, opts)).to_json
      response_json = ",#{root_elem_json}:" +
          hateoas_object(response_obj, controller, opts[:format] || :json, opts)
      links = hateoas_links(opts[:links], opts[:link_opts], controller, opts)
    elsif !response_obj.is_a?(Hash) && response_obj.respond_to?(:map)
      response_json = ',' + hateoas_collection(response_obj, controller,
          opts[:format] || :json, opts)
      links = hateoas_links(opts[:collection_links],
          opts[:collection_link_options], controller, opts)
    else
      root_elem_json = ModelApi::Utils.ext_attr(get_object_root_elem(response_obj, opts)).to_json
      response_json = ",#{root_elem_json}:" + response_obj.to_json(opts)
      links = hateoas_links(opts[:links], opts[:link_opts], controller, opts)
    end
    if links.present?
      response_json += ",\"_links\":" + links.to_json(opts)
    end
    response_json
  else
    ''
  end
end
get_collection_root_elem(collection, opts) click to toggle source
# File lib/model-api/renderer.rb, line 152
def get_collection_root_elem(collection, opts)
  if (root_elem = opts[:root]).present?
    return root_elem
  end
  if collection.respond_to?(:klass)
    item_class = collection.klass
  elsif collection.respond_to?(:first) && (first_obj = collection.first).present?
    item_class = first_obj.class
  else
    item_class = Object
  end
  ModelApi::Utils.model_name(item_class).plural
end
get_format(controller) click to toggle source
# File lib/model-api/renderer.rb, line 132
def get_format(controller)
  format = controller.request.format.symbol rescue :json
  format == :xml ? :xml : :json
end
get_object_root_elem(obj, opts) click to toggle source
# File lib/model-api/renderer.rb, line 137
def get_object_root_elem(obj, opts)
  if (root_elem = opts[:root]).present?
    return root_elem
  end
  if obj.respond_to?(:klass)
    item_class = obj.klass
  elsif obj.nil?
    item_class = Object
  else
    item_class = obj.class
  end
  return 'response' if item_class == Hash
  ModelApi::Utils.model_name(item_class).singular
end
hateoas_collection(collection, controller, format, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 325
def hateoas_collection(collection, controller, format, opts = {})
  opts = (opts || {}).symbolize_keys
  count = opts[:count]
  page = opts[:page]
  page_count = opts[:page_count]
  page_size = opts[:page_size]
  root_tag = ModelApi::Utils.ext_attr(opts[:root] || get_collection_root_elem(collection, opts) ||
      :objects)
  if format == :xml
    children_tag = opts.delete(:children) || root_tag.to_s.singularize
    response_xml = []
    response_xml << "<#{root_tag}>"
    collection.each do |obj|
      response_xml << hateoas_object(obj, controller, format, opts.merge(root: children_tag))
    end
    response_xml << "</#{root_tag}>"
    response_xml << hateoas_pagination_values_xml(count, page, page_count, page_size)
    pretty_xml(response_xml.join)
  else
    "\"#{root_tag}\":[" + collection.map do |obj|
      hateoas_object(obj, controller, format, opts)
    end.join(',') + ']' +
        hateoas_pagination_values_json(count, page, page_count, page_size)
  end
end
hateoas_object(obj, controller, format, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 305
def hateoas_object(obj, controller, format, opts = {})
  object_links = object_hateoas_links(opts[:object_links], obj, controller, opts)
  hash = serializable_object(obj, opts)
  if format == :xml
    root_tag = ModelApi::Utils.ext_attr(opts[:root] || get_object_root_elem(obj, opts) || :obj)
    end_tag = "</#{root_tag}>"
    if object_links.present?
      pretty_xml(hash
          .to_xml(opts.merge(root: root_tag, skip_instruct: true))
          .sub(Regexp.new('(' + Regexp.escape(end_tag) + ')\\w*\\Z'),
              hateoas_link_xml(object_links, opts) + end_tag))
    else
      pretty_xml(hash.to_xml(opts.merge(root: root_tag, skip_instruct: true)))
    end
  else
    hash[:_links] = object_links
    hash.to_json(opts)
  end
end
hateoas_pagination_values_json(count, page, page_count, page_size) click to toggle source
# File lib/model-api/renderer.rb, line 174
def hateoas_pagination_values_json(count, page, page_count, page_size)
  json = []
  if count.present?
    json << ",\"#{ModelApi::Utils.ext_attr(:count)}\":#{[count.to_i, 0].max}"
  end
  json << ",\"#{ModelApi::Utils.ext_attr(:page)}\":#{[page.to_i, 0].max}" if page.present?
  if page_count.present?
    json << ",\"#{ModelApi::Utils.ext_attr(:page_count)}\":#{[page_count.to_i, 0].max}"
  end
  if page_size.present?
    json << ",\"#{ModelApi::Utils.ext_attr(:page_size)}\":#{[page_size.to_i, 0].max}"
  end
  json.join
end
hateoas_pagination_values_xml(count, page, page_count, page_size) click to toggle source
# File lib/model-api/renderer.rb, line 189
def hateoas_pagination_values_xml(count, page, page_count, page_size)
  xml = []
  count_attr = ModelApi::Utils.ext_attr(:count)
  page_attr = ModelApi::Utils.ext_attr(:page)
  page_count_attr = ModelApi::Utils.ext_attr(:page_count)
  page_size_attr = ModelApi::Utils.ext_attr(:page_size)
  xml << "<#{count_attr}>#{[count.to_i, 0].max}</#{count_attr}>" if count.present?
  xml << "<#{page_attr}>#{[page.to_i, 0].max}</#{page_attr}>" if page.present?
  if page_count.present?
    xml << "<#{page_count_attr}>#{[page_count.to_i, 0].max}</#{page_count_attr}>"
  end
  if page_size.present?
    xml << "<#{page_size_attr}>#{[page_size.to_i, 0].max}</#{page_size_attr}>"
  end
  xml.join
end
http_status_and_status_code(controller, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 409
def http_status_and_status_code(controller, opts = {})
  if opts[:status].present?
    return [opts[:status].to_sym, ModelApi::Utils.http_status_code(opts[:status].to_sym)]
  elsif opts[:status_code].present?
    return [ModelApi::Utils.http_status(opts[:status_code].to_i), opts[:status_code].to_i]
  elsif controller.response.status.present? && controller.response.status > 0
    return [ModelApi::Utils.http_status(controller.response.status), controller.response.status]
  else
    return [:ok, ModelApi::Utils.http_status_code(:ok)]
  end
end
pretty_xml(xml, _indent = 2) click to toggle source
# File lib/model-api/renderer.rb, line 483
def pretty_xml(xml, _indent = 2)
  xml_doc = REXML::Document.new(xml) rescue nil
  return xml unless xml_doc.present?
  formatter = REXML::Formatters::Pretty.new
  formatter.compact = true
  out = ''
  formatter.write(xml_doc, out)
  out || xml
end
process_serializable_hash(api_attrs_metadata, hash, opts) click to toggle source
# File lib/model-api/renderer.rb, line 118
def process_serializable_hash(api_attrs_metadata, hash, opts)
  updated_hash_array = (hash.map do |key, value|
    attr_metadata = api_attrs_metadata[key.to_sym] || {}
    value = ModelApi::Utils.format_value(value, attr_metadata, opts)
    next nil if value.nil? && attr_metadata[:hide_when_nil]
    [ModelApi::Utils.ext_attr(key, attr_metadata).to_sym, value]
  end).compact
  api_attrs = api_attrs_metadata.map { |k, m| ModelApi::Utils.ext_attr(k, m).to_sym }
  updated_hash_array.sort_by! do |key, _value|
    api_attrs.find_index(key.to_s.to_sym) || api_attrs.size
  end
  Hash[updated_hash_array]
end
render_json_response(response_obj, controller, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 421
def render_json_response(response_obj, controller, opts = {})
  http_status, http_status_code = http_status_and_status_code(controller, opts)
  successful = ModelApi::Utils.response_successful?(http_status_code)
  response_json = "\"#{ModelApi::Utils.ext_attr(:successful)}\":#{successful ? 'true' : 'false'},"
  response_json += "\"#{ModelApi::Utils.ext_attr(:status)}\":\"#{http_status}\","
  response_json += "\"#{ModelApi::Utils.ext_attr(:status_code)}\":#{http_status_code}"
  if opts[:messages].present?
    response_json += ",\"#{ModelApi::Utils.ext_attr(successful ? :messages : :errors)}\":" +
        opts[:messages].to_json(opts)
  end
  response_json += build_response_obj_json(response_obj, controller, opts)
  if opts[:ignored_fields].present?
    response_json += ",\"#{ModelApi::Utils.ext_attr(:ignored_fields)}\":" +
        opts[:ignored_fields].to_json(opts)
  end
  return "{#{response_json}}" if opts[:generate_body_only]
  set_location_header(response_obj, controller, successful, opts)
  controller.render status: http_status, json: "{#{response_json}}"
  successful
end
render_xml_response(response_obj, controller, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 351
def render_xml_response(response_obj, controller, opts = {})
  http_status, http_status_code = http_status_and_status_code(controller, opts)
  successful = ModelApi::Utils.response_successful?(http_status_code)
  response_xml = render_xml_response_heading(http_status, opts)
  render_xml_response_body(response_xml, response_obj, controller, opts)
  response_xml << "</#{ModelApi::Utils.ext_attr(:response)}>"
  return pretty_xml(response_xml.join) if opts[:generate_body_only]
  set_location_header(response_obj, controller, successful, opts)
  controller.render status: http_status, xml: pretty_xml(response_xml.join)
  successful
end
render_xml_response_body(response_xml, response_obj, controller, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 381
def render_xml_response_body(response_xml, response_obj, controller,
    opts = {})
  collection = false
  if response_obj.is_a?(ActiveRecord::Base)
    response_xml << hateoas_object(response_obj, controller, opts[:format] || :xml, opts)
  elsif !response_obj.is_a?(Hash) && response_obj.respond_to?(:map)
    response_xml << hateoas_collection(response_obj, controller, opts[:format] || :xml, opts)
    collection = true
  elsif response_obj.present?
    root = ModelApi::Utils.ext_attr(opts[:root] || get_object_root_elem(response_obj, opts) ||
        :response)
    response_xml << response_obj.to_xml(opts.merge(skip_instruct: true, root: root)).rstrip
  end
  if opts[:ignored_fields].present?
    response_xml << xml_collection_elem_tags_with_attrs(
        ModelApi::Utils.ext_attr(:ignored_fields), opts[:ignored_fields])
  end
  if collection
    if (links = hateoas_links(opts[:collection_links],
        opts[:collection_link_options], controller, opts)).present?
      response_xml << hateoas_link_xml(links, opts)
    end
  elsif (links = hateoas_links(opts[:links], opts[:link_opts], controller, opts)).present?
    response_xml << hateoas_link_xml(links, opts)
  end
  response_xml
end
render_xml_response_heading(status, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 363
def render_xml_response_heading(status, opts = {})
  http_status_code = ModelApi::Utils.http_status_code(status)
  successful = ModelApi::Utils.response_successful?(http_status_code)
  successful_tag = ModelApi::Utils.ext_attr(:successful)
  status_tag = ModelApi::Utils.ext_attr(:status)
  status_code_tag = ModelApi::Utils.ext_attr(:status_code)
  response_xml = []
  response_xml << "<#{ModelApi::Utils.ext_attr(:response)}>"
  response_xml << "<#{successful_tag}>#{successful ? 'true' : 'false'}</#{successful_tag}>"
  response_xml << "<#{status_tag}>#{status}</#{status_tag}>"
  response_xml << "<#{status_code_tag}>#{http_status_code}</#{status_code_tag}>"
  if opts[:messages].present?
    response_xml << xml_collection_elem_tags_with_attrs(
        ModelApi::Utils.ext_attr(successful ? :messages : :errors), opts[:messages])
  end
  response_xml
end
serializable_object(obj, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 19
def serializable_object(obj, opts = {})
  obj = obj.first if obj.is_a?(ActiveRecord::Relation)
  opts = opts.symbolize_keys
  return nil if obj.nil?
  operation = opts[:operation] || :show
  metadata_opts = opts.merge(ModelApi::Utils.contextual_metadata_opts(opts))
  metadata = ModelApi::Utils.filtered_attrs(obj, operation, metadata_opts)
  render_values, render_assoc = attrs_by_type(obj, metadata)
  hash = {}
  serialize_values(hash, obj, render_values, opts)
  serialize_associations(hash, obj, render_assoc, opts)
  process_serializable_hash(metadata, hash, opts)
end
serialize_associations(hash, obj, associations, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 62
def serialize_associations(hash, obj, associations, opts = {})
  associations.each do |assoc, attr_metadata|
    return nil if assoc.nil?
    assoc_opts = ModelApi::Utils.assoc_opts(assoc, attr_metadata, opts)
    next if assoc_opts.nil?
    attr = assoc.name
    assoc = assoc_opts[:association]
    render_proc ||= ->(o) { serialize_value(o, attr_metadata, assoc_opts) }
    if !assoc.nil? && assoc.collection?
      value = obj.send(attr).map { |o| render_proc.call(o) }
    else
      value = render_proc.call(obj.send(attr))
    end
    hash[attr] = value
  end
end
serialize_value(value, attr_metadata, opts) click to toggle source
# File lib/model-api/renderer.rb, line 79
def serialize_value(value, attr_metadata, opts)
  if (render_method = attr_metadata[:render_method]).present?
    if render_method.respond_to?(:call)
      value = serialize_value_proc(render_method, value)
    else
      value = serialize_value_obj_attr(render_method, value)
    end
  end
  opts = ModelApi::Utils.contextual_metadata_opts(attr_metadata, opts)
  opts[:operation] ||= :show
  if value.respond_to?(:map)
    return value.map do |elem|
      elem.is_a?(ActiveRecord::Base) ? serializable_object(elem, opts) : elem
    end
  elsif value.is_a?(ActiveRecord::Base)
    return serializable_object(value, opts)
  end
  value
end
serialize_value_obj_attr(render_method, value) click to toggle source
# File lib/model-api/renderer.rb, line 107
def serialize_value_obj_attr(render_method, value)
  render_method = render_method.to_s.to_sym
  if value.is_a?(ActiveRecord::Associations::CollectionProxy) || value.is_a?(Array)
    (value.map do |obj|
      obj.respond_to?(render_method) ? obj.send(render_method) : nil
    end).compact
  elsif value.respond_to?(render_method)
    value.send(render_method)
  end
end
serialize_value_proc(render_method, value) click to toggle source
# File lib/model-api/renderer.rb, line 99
def serialize_value_proc(render_method, value)
  if render_method.parameters.count > 1
    render_method.call(value, opts)
  else
    render_method.call(value)
  end
end
serialize_values(hash, obj, value_procs, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 52
def serialize_values(hash, obj, value_procs, opts = {})
  value_procs.each do |attr, value, attr_metadata|
    if value.respond_to?(:call) && value.respond_to?(:parameters)
      proc_opts = opts.merge(attr: attr, attr_metadata: attr_metadata)
      value = value.send(*([:call, obj, proc_opts][0..value.parameters.size]))
    end
    hash[attr] = serialize_value(value, attr_metadata, opts)
  end
end
set_location_header(response_obj, controller, successful, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 493
def set_location_header(response_obj, controller, successful, opts = {})
  return unless successful && opts[:location_header] &&
      response_obj.is_a?(ActiveRecord::Base)
  links = opts[:object_links]
  return unless links.is_a?(Hash) && links.include?(:self)
  link = object_hateoas_links({ self: links[:self] }, response_obj, controller,
      opts.merge(exclude_api_links: true)).find { |l| l[:rel].to_s == 'self' }
  controller.response.header['Location'] = link[:href] if link.include?(:href)
end
xml_collection_elem_tags_with_attrs(element, array, opts = {}) click to toggle source
# File lib/model-api/renderer.rb, line 475
def xml_collection_elem_tags_with_attrs(element, array, opts = {})
  child_element = opts[:children] || element.to_s.singularize
  tags = array.map do |hash|
    xml_elem_tags_with_attrs(child_element, hash)
  end
  "<#{element}>#{tags.join}</#{element}>"
end
xml_elem_tags_with_attrs(element, hash) click to toggle source
# File lib/model-api/renderer.rb, line 468
def xml_elem_tags_with_attrs(element, hash)
  tags = hash.map do |attr, value|
    " #{attr}=\"#{CGI.escapeHTML(value.to_s)}\""
  end
  "<#{element}#{tags.join} />"
end