module JSONAPI::Serializer

Constants

VERSION

Public Class Methods

activemodel_errors(raw_errors) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 377
def self.activemodel_errors(raw_errors)
  raw_errors.to_hash(full_messages: true).inject([]) do |result, (attribute, messages)|
    result += messages.map { |message| single_error(attribute.to_s, message) }
  end
end
find_serializer(object, options) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 251
def self.find_serializer(object, options)
  find_serializer_class(object, options).new(object, options)
end
find_serializer_class(object, options) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 246
def self.find_serializer_class(object, options)
  class_name = find_serializer_class_name(object, options)
  class_name.constantize
end
find_serializer_class_name(object, options) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 236
def self.find_serializer_class_name(object, options)
  if options[:namespace]
    return "#{options[:namespace]}::#{object.class.name}Serializer"
  end
  if object.respond_to?(:jsonapi_serializer_class_name)
    return object.jsonapi_serializer_class_name.to_s
  end
  "#{object.class.name}Serializer"
end
included(target) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 6
def self.included(target)
  target.send(:include, InstanceMethods)
  target.extend ClassMethods
  target.class_eval do
    include JSONAPI::Attributes
  end
end
is_activemodel_errors?(raw_errors) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 383
def self.is_activemodel_errors?(raw_errors)
  raw_errors.respond_to?(:to_hash) && raw_errors.respond_to?(:full_messages)
end
serialize(objects, options = {}) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 255
def self.serialize(objects, options = {})
  # Normalize option strings to symbols.
  options[:is_collection] = options.delete('is_collection') || options[:is_collection] || false
  options[:include] = options.delete('include') || options[:include]
  options[:serializer] = options.delete('serializer') || options[:serializer]
  options[:namespace] = options.delete('namespace') || options[:namespace]
  options[:context] = options.delete('context') || options[:context] || {}
  options[:skip_collection_check] = options.delete('skip_collection_check') || options[:skip_collection_check] || false
  options[:base_url] = options.delete('base_url') || options[:base_url]
  options[:jsonapi] = options.delete('jsonapi') || options[:jsonapi]
  options[:meta] = options.delete('meta') || options[:meta]
  options[:links] = options.delete('links') || options[:links]
  options[:fields] = options.delete('fields') || options[:fields] || {}

  # Deprecated: use serialize_errors method instead
  options[:errors] = options.delete('errors') || options[:errors]

  # Normalize includes.
  includes = options[:include]
  includes = (includes.is_a?(String) ? includes.split(',') : includes).uniq if includes

  # Transforms input so that the comma-separated fields are separate symbols in array
  # and keys are stringified
  # Example:
  # {posts: 'title,author,long_comments'} => {'posts' => [:title, :author, :long_comments]}
  # {posts: ['title', 'author', 'long_comments'} => {'posts' => [:title, :author, :long_comments]}
  #
  fields = {}
  # Normalize fields to accept a comma-separated string or an array of strings.
  options[:fields].map do |type, whitelisted_fields|
    whitelisted_fields = [whitelisted_fields] if whitelisted_fields.is_a?(Symbol)
    whitelisted_fields = whitelisted_fields.split(',') if whitelisted_fields.is_a?(String)
    fields[type.to_s] = whitelisted_fields.map(&:to_sym)
  end

  # An internal-only structure that is passed through serializers as they are created.
  passthrough_options = {
    context: options[:context],
    serializer: options[:serializer],
    namespace: options[:namespace],
    include: includes,
    fields: fields,
    base_url: options[:base_url]
  }

  if !options[:skip_collection_check] && options[:is_collection] && !objects.respond_to?(:each)
    raise JSONAPI::Serializer::AmbiguousCollectionError.new(
      'Attempted to serialize a single object as a collection.')
  end

  # Automatically include linkage data for any relation that is also included.
  if includes
    include_linkages = includes.map { |key| key.to_s.split('.').first }
    passthrough_options[:include_linkages] = include_linkages
  end

  # Spec: Primary data MUST be either:
  # - a single resource object or null, for requests that target single resources.
  # - an array of resource objects or an empty array ([]), for resource collections.
  # http://jsonapi.org/format/#document-structure-top-level
  if options[:is_collection] && !objects.any?
    primary_data = []
  elsif !options[:is_collection] && objects.nil?
    primary_data = nil
  elsif options[:is_collection]
    # Have object collection.
    primary_data = serialize_primary_multi(objects, passthrough_options)
  else
    # Duck-typing check for a collection being passed without is_collection true.
    # We always must be told if serializing a collection because the JSON:API spec distinguishes
    # how to serialize null single resources vs. empty collections.
    if !options[:skip_collection_check] && objects.respond_to?(:each)
      raise JSONAPI::Serializer::AmbiguousCollectionError.new(
        'Must provide `is_collection: true` to `serialize` when serializing collections.')
    end
    # Have single object.
    primary_data = serialize_primary(objects, passthrough_options)
  end
  result = {
    'data' => primary_data,
  }
  result['jsonapi'] = options[:jsonapi] if options[:jsonapi]
  result['meta'] = options[:meta] if options[:meta]
  result['links'] = options[:links] if options[:links]
  result['errors'] = options[:errors] if options[:errors]

  # If 'include' relationships are given, recursively find and include each object.
  if includes
    relationship_data = {}
    inclusion_tree = parse_relationship_paths(includes)

    # Given all the primary objects (either the single root object or collection of objects),
    # recursively search and find related associations that were specified as includes.
    objects = options[:is_collection] ? objects.to_a : [objects]
    objects.compact.each do |obj|
      # Use the mutability of relationship_data as the return datastructure to take advantage
      # of the internal special merging logic.
      find_recursive_relationships(obj, inclusion_tree, relationship_data, passthrough_options)
    end

    result['included'] = relationship_data.map do |_, data|
      included_passthrough_options = {}
      included_passthrough_options[:base_url] = passthrough_options[:base_url]
      included_passthrough_options[:context] = passthrough_options[:context]
      included_passthrough_options[:fields] = passthrough_options[:fields]
      included_passthrough_options[:serializer] = find_serializer_class(data[:object], options)
      included_passthrough_options[:namespace] = passthrough_options[:namespace]
      included_passthrough_options[:include_linkages] = data[:include_linkages]
      serialize_primary(data[:object], included_passthrough_options)
    end
  end
  result
end
serialize_errors(raw_errors) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 369
def self.serialize_errors(raw_errors)
  if is_activemodel_errors?(raw_errors)
    {'errors' => activemodel_errors(raw_errors)}
  else
    {'errors' => raw_errors}
  end
end
single_error(attribute, message) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 387
def self.single_error(attribute, message)
  {
    'source' => {
      'pointer' => "/data/attributes/#{attribute.dasherize}"
    },
    'detail' => message
  }
end

Protected Class Methods

find_recursive_relationships(root_object, root_inclusion_tree, results, options) click to toggle source

Recursively find object relationships and returns a tree of related objects. Example return: {

['comments', '1'] => {object: <Comment>, include_linkages: ['author']},
['users', '1'] => {object: <User>, include_linkages: []},
['users', '2'] => {object: <User>, include_linkages: []},

}

# File lib/jsonapi-serializers/serializer.rb, line 449
def self.find_recursive_relationships(root_object, root_inclusion_tree, results, options)
  root_inclusion_tree.each do |attribute_name, child_inclusion_tree|
    # Skip the sentinal value, but we need to preserve it for siblings.
    next if attribute_name == :_include

    serializer = JSONAPI::Serializer.find_serializer(root_object, options)
    unformatted_attr_name = serializer.unformat_name(attribute_name).to_sym

    # We know the name of this relationship, but we don't know where it is stored internally.
    # Check if it is a has_one or has_many relationship.
    object = nil
    is_collection = false
    is_valid_attr = false
    if serializer.has_one_relationships.has_key?(unformatted_attr_name)
      is_valid_attr = true
      attr_data = serializer.has_one_relationships[unformatted_attr_name]
      object = serializer.has_one_relationship(unformatted_attr_name, attr_data)
    elsif serializer.has_many_relationships.has_key?(unformatted_attr_name)
      is_valid_attr = true
      is_collection = true
      attr_data = serializer.has_many_relationships[unformatted_attr_name]
      object = serializer.has_many_relationship(unformatted_attr_name, attr_data)
    end

    if !is_valid_attr
      raise JSONAPI::Serializer::InvalidIncludeError.new(
        "'#{attribute_name}' is not a valid include.")
    end

    if attribute_name != serializer.format_name(attribute_name)
      expected_name = serializer.format_name(attribute_name)

      raise JSONAPI::Serializer::InvalidIncludeError.new(
        "'#{attribute_name}' is not a valid include.  Did you mean '#{expected_name}' ?"
      )
    end

    # We're finding relationships for compound documents, so skip anything that doesn't exist.
    next if object.nil?

    # Full linkage: a request for comments.author MUST automatically include comments
    # in the response.
    objects = is_collection ? object : [object]
    if child_inclusion_tree[:_include] == true
      # Include the current level objects if the _include attribute exists.
      # If it is not set, that indicates that this is an inner path and not a leaf and will
      # be followed by the recursion below.
      objects.each do |obj|
        obj_serializer = JSONAPI::Serializer.find_serializer(obj, options)
        # Use keys of ['posts', '1'] for the results to enforce uniqueness.
        # Spec: A compound document MUST NOT include more than one resource object for each
        # type and id pair.
        # http://jsonapi.org/format/#document-structure-compound-documents
        key = [obj_serializer.type, obj_serializer.id]

        # This is special: we know at this level if a child of this parent will also been
        # included in the compound document, so we can compute exactly what linkages should
        # be included by the object at this level. This satisfies this part of the spec:
        #
        # Spec: Resource linkage in a compound document allows a client to link together
        # all of the included resource objects without having to GET any relationship URLs.
        # http://jsonapi.org/format/#document-structure-resource-relationships
        current_child_includes = []
        inclusion_names = child_inclusion_tree.keys.reject { |k| k == :_include }
        inclusion_names.each do |inclusion_name|
          if child_inclusion_tree[inclusion_name][:_include]
            current_child_includes << inclusion_name
          end
        end

        # Special merge: we might see this object multiple times in the course of recursion,
        # so merge the include_linkages each time we see it to load all the relevant linkages.
        current_child_includes += results[key] && results[key][:include_linkages] || []
        current_child_includes.uniq!
        results[key] = {object: obj, include_linkages: current_child_includes}
      end
    end

    # Recurse deeper!
    if !child_inclusion_tree.empty?
      # For each object we just loaded, find all deeper recursive relationships.
      objects.each do |obj|
        find_recursive_relationships(obj, child_inclusion_tree, results, options)
      end
    end
  end
  nil
end
merge_relationship_path(path, data) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 556
def self.merge_relationship_path(path, data)
  parts = path.split('.', 2)
  current_level = parts[0].strip
  data[current_level] ||= {_include: true}

  if parts.length == 2
    # Need to recurse more.
    merge_relationship_path(parts[1], data[current_level])
  end
end
parse_relationship_paths(paths) click to toggle source

Takes a list of relationship paths and returns a hash as deep as the given paths. The _include: true is a sentinal value that specifies whether the parent level should be included.

Example:

Given: ['author', 'comments', 'comments.user']
Returns: {
  'author' => {_include: true},
  'comments' => {_include: true, 'user' => {_include: true}},
}
# File lib/jsonapi-serializers/serializer.rb, line 549
def self.parse_relationship_paths(paths)
  relationships = {}
  paths.each { |path| merge_relationship_path(path, relationships) }
  relationships
end
serialize_primary(object, options = {}) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 396
def self.serialize_primary(object, options = {})
  serializer_class = options[:serializer] || find_serializer_class(object, options)

  # Spec: Primary data MUST be either:
  # - a single resource object or null, for requests that target single resources.
  # http://jsonapi.org/format/#document-structure-top-level
  return if object.nil?

  serializer = serializer_class.new(object, options)
  data = {
    'type' => serializer.type.to_s,
  }

  # "The id member is not required when the resource object originates at the client
  #  and represents a new resource to be created on the server."
  # http://jsonapi.org/format/#document-resource-objects
  # We'll assume that if the id is blank, it means the resource is to be created.
  data['id'] = serializer.id.to_s if serializer.id && !serializer.id.empty?

  # Merge in optional top-level members if they are non-nil.
  # http://jsonapi.org/format/#document-structure-resource-objects
  # Call the methods once now to avoid calling them twice when evaluating the if's below.
  attributes = serializer.attributes
  links = serializer.links
  relationships = serializer.relationships
  jsonapi = serializer.jsonapi
  meta = serializer.meta
  data['attributes'] = attributes if !attributes.empty?
  data['links'] = links if !links.empty?
  data['relationships'] = relationships if !relationships.empty?
  data['jsonapi'] = jsonapi if !jsonapi.nil?
  data['meta'] = meta if !meta.nil?
  data
end
serialize_primary_multi(objects, options = {}) click to toggle source
# File lib/jsonapi-serializers/serializer.rb, line 432
def self.serialize_primary_multi(objects, options = {})
  # Spec: Primary data MUST be either:
  # - an array of resource objects or an empty array ([]), for resource collections.
  # http://jsonapi.org/format/#document-structure-top-level
  return [] if !objects.any?

  objects.map { |obj| serialize_primary(obj, options) }
end