lib/jsonapi/resource_serializer.rb in jsonapi-resources-0.9.12 vs lib/jsonapi/resource_serializer.rb in jsonapi-resources-0.10.0.beta1
- old
+ new
@@ -1,9 +1,9 @@
module JSONAPI
class ResourceSerializer
- attr_reader :link_builder, :key_formatter, :serialization_options, :primary_class_name,
+ attr_reader :link_builder, :key_formatter, :serialization_options,
:fields, :include_directives, :always_include_to_one_linkage_data,
:always_include_to_many_linkage_data
# initialize
# Options can include
@@ -17,11 +17,10 @@
# key_formatter: KeyFormatter instance to override the default configuration
# serialization_options: additional options that will be passed to resource meta and links lambdas
def initialize(primary_resource_klass, options = {})
@primary_resource_klass = primary_resource_klass
- @primary_class_name = primary_resource_klass._type
@fields = options.fetch(:fields, {})
@include = options.fetch(:include, [])
@include_directives = options[:include_directives]
@include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
@key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
@@ -40,81 +39,90 @@
@_config_keys = {}
@_supplying_attribute_fields = {}
@_supplying_relationship_fields = {}
end
- # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
- def serialize_to_hash(source)
- @top_level_sources = Set.new([source].flatten(1).compact.map {|s| top_level_source_key(s) })
+ # Converts a resource_set to a hash, conforming to the JSONAPI structure
+ def serialize_resource_set_to_hash_single(resource_set)
- is_resource_collection = source.respond_to?(:to_ary)
-
- @included_objects = {}
-
- process_source_objects(source, @include_directives.include_directives)
-
primary_objects = []
+ included_objects = []
- # pull the processed objects corresponding to the source objects. Ensures we preserve order.
- if is_resource_collection
- source.each do |primary|
- if primary.id
- case primary
- when CachedResourceFragment then primary_objects.push(@included_objects[primary.type][primary.id][:object_hash])
- when Resource then primary_objects.push(@included_objects[primary.class._type][primary.id][:object_hash])
- else raise "Unknown source type #{primary.inspect}"
- end
+ resource_set.resource_klasses.each_value do |resource_klass|
+ resource_klass.each_value do |resource|
+ serialized_resource = object_hash(resource[:resource], resource[:relationships])
+
+ if resource[:primary]
+ primary_objects.push(serialized_resource)
+ else
+ included_objects.push(serialized_resource)
end
end
- else
- if source.try(:id)
- case source
- when CachedResourceFragment then primary_objects.push(@included_objects[source.type][source.id][:object_hash])
- when Resource then primary_objects.push(@included_objects[source.class._type][source.id][:object_hash])
- else raise "Unknown source type #{source.inspect}"
- end
- end
end
+ fail "To Many primary objects for show" if (primary_objects.count > 1)
+ primary_hash = { 'data' => primary_objects[0] }
+
+ primary_hash['included'] = included_objects if included_objects.size > 0
+ primary_hash
+ end
+
+ def serialize_resource_set_to_hash_plural(resource_set)
+
+ primary_objects = []
included_objects = []
- @included_objects.each_value do |objects|
- objects.each_value do |object|
- unless object[:primary]
- included_objects.push(object[:object_hash])
+
+ resource_set.resource_klasses.each_value do |resource_klass|
+ resource_klass.each_value do |resource|
+ serialized_resource = object_hash(resource[:resource], resource[:relationships])
+
+ if resource[:primary]
+ primary_objects.push(serialized_resource)
+ else
+ included_objects.push(serialized_resource)
end
end
end
- primary_hash = { data: is_resource_collection ? primary_objects : primary_objects[0] }
+ primary_hash = { 'data' => primary_objects }
- primary_hash[:included] = included_objects if included_objects.size > 0
+ primary_hash['included'] = included_objects if included_objects.size > 0
primary_hash
end
- def serialize_to_relationship_hash(source, requested_relationship)
+ def serialize_related_resource_set_to_hash_plural(resource_set, _source_resource)
+ return serialize_resource_set_to_hash_plural(resource_set)
+ end
+
+ def serialize_to_links_hash(source, requested_relationship, resource_ids)
if requested_relationship.is_a?(JSONAPI::Relationship::ToOne)
- data = to_one_linkage(source, requested_relationship)
+ data = to_one_linkage(resource_ids[0])
else
- data = to_many_linkage(source, requested_relationship)
+ data = to_many_linkage(resource_ids)
end
- rel_hash = { 'data': data }
-
- links = default_relationship_links(source, requested_relationship)
- rel_hash['links'] = links unless links.blank?
-
- rel_hash
+ {
+ 'links' => {
+ 'self' => self_link(source, requested_relationship),
+ 'related' => related_link(source, requested_relationship)
+ },
+ 'data' => data
+ }
end
def query_link(query_params)
link_builder.query_link(query_params)
end
def format_key(key)
@key_formatter.format(key)
end
+ def unformat_key(key)
+ @key_formatter.unformat(key)
+ end
+
def format_value(value, format)
@value_formatter_type_cache.get(format).format(value)
end
def config_key(resource_klass)
@@ -130,34 +138,39 @@
class_name: self.class.name,
seriserialization_options: serialization_options.sort.map(&:as_json),
supplying_attribute_fields: supplying_attribute_fields(resource_klass).sort,
supplying_relationship_fields: supplying_relationship_fields(resource_klass).sort,
link_builder_base_url: link_builder.base_url,
+ route_formatter_class: link_builder.route_formatter.uncached.class.name,
key_formatter_class: key_formatter.uncached.class.name,
always_include_to_one_linkage_data: always_include_to_one_linkage_data,
always_include_to_many_linkage_data: always_include_to_many_linkage_data
}
end
- # Returns a serialized hash for the source model
- def object_hash(source, include_directives = {})
+ def object_hash(source, relationship_data)
obj_hash = {}
- if source.is_a?(JSONAPI::CachedResourceFragment)
- obj_hash['id'] = source.id
+ return obj_hash if source.nil?
+
+ fetchable_fields = Set.new(source.fetchable_fields)
+
+ if source.is_a?(JSONAPI::CachedResponseFragment)
+ id_format = source.resource_klass._attribute_options(:id)[:format]
+
+ id_format = 'id' if id_format == :default
+ obj_hash['id'] = format_value(source.id, id_format)
obj_hash['type'] = source.type
obj_hash['links'] = source.links_json if source.links_json
obj_hash['attributes'] = source.attributes_json if source.attributes_json
- relationships = cached_relationships_hash(source, include_directives)
- obj_hash['relationships'] = relationships unless relationships.blank?
+ relationships = cached_relationships_hash(source, fetchable_fields, relationship_data)
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
obj_hash['meta'] = source.meta_json if source.meta_json
else
- fetchable_fields = Set.new(source.fetchable_fields)
-
# TODO Should this maybe be using @id_formatter instead, for consistency?
id_format = source.class._attribute_options(:id)[:format]
# protect against ids that were declared as an attribute, but did not have a format set.
id_format = 'id' if id_format == :default
obj_hash['id'] = format_value(source.id, id_format)
@@ -168,35 +181,22 @@
obj_hash['links'] = links unless links.empty?
attributes = attributes_hash(source, fetchable_fields)
obj_hash['attributes'] = attributes unless attributes.empty?
- relationships = relationships_hash(source, fetchable_fields, include_directives)
- obj_hash['relationships'] = relationships unless relationships.blank?
+ relationships = relationships_hash(source, fetchable_fields, relationship_data)
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
meta = meta_hash(source)
obj_hash['meta'] = meta unless meta.empty?
end
obj_hash
end
private
- # Process the primary source object(s). This will then serialize associated object recursively based on the
- # requested includes. Fields are controlled fields option for each resource type, such
- # as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
- # The fields options controls both fields and included links references.
- def process_source_objects(source, include_directives)
- if source.respond_to?(:to_ary)
- source.each { |resource| process_source_objects(resource, include_directives) }
- else
- return {} if source.nil?
- add_resource(source, include_directives, true)
- end
- end
-
def supplying_attribute_fields(resource_klass)
@_supplying_attribute_fields.fetch resource_klass do
attrs = Set.new(resource_klass._attributes.keys.map(&:to_sym))
cur = resource_klass
while cur != JSONAPI::Resource
@@ -247,305 +247,137 @@
(meta.is_a?(Hash) && meta) || {}
end
def links_hash(source)
links = custom_links_hash(source)
- if !links.key?('self') && !source.class.exclude_link?(:self)
- links['self'] = link_builder.self_link(source)
- end
+ links['self'] = link_builder.self_link(source) unless links.key?('self')
links.compact
end
def custom_links_hash(source)
custom_links = source.custom_links(custom_generation_options)
(custom_links.is_a?(Hash) && custom_links) || {}
end
- def top_level_source_key(source)
- case source
- when CachedResourceFragment then "#{source.resource_klass}_#{source.id}"
- when Resource then "#{source.class}_#{@id_formatter.format(source.id)}"
- else raise "Unknown source type #{source.inspect}"
- end
- end
-
- def self_referential_and_already_in_source(resource)
- resource && @top_level_sources.include?(top_level_source_key(resource))
- end
-
- def relationships_hash(source, fetchable_fields, include_directives = {})
- if source.is_a?(CachedResourceFragment)
- return cached_relationships_hash(source, include_directives)
- end
-
- include_directives[:include_related] ||= {}
-
- relationships = source.class._relationships.select{|k,v| fetchable_fields.include?(k) }
+ def relationships_hash(source, fetchable_fields, relationship_data)
+ relationships = source.class._relationships.select{|k,_v| fetchable_fields.include?(k) }
field_set = supplying_relationship_fields(source.class) & relationships.keys
relationships.each_with_object({}) do |(name, relationship), hash|
- ia = include_directives[:include_related][name]
- include_linkage = ia && ia[:include]
- include_linked_children = ia && !ia[:include_related].empty?
-
+ include_data = false
if field_set.include?(name)
- ro = relationship_object(source, relationship, include_linkage)
- hash[format_key(name)] = ro unless ro.blank?
- end
-
- # If the object has been serialized once it will be in the related objects list,
- # but it's possible all children won't have been captured. So we must still go
- # through the relationships.
- if include_linkage || include_linked_children
- resources = if source.preloaded_fragments.has_key?(format_key(name))
- source.preloaded_fragments[format_key(name)].values
- else
- options = { filters: ia && ia[:include_filters] || {} }
- [source.public_send(name, options)].flatten(1).compact
- end
- resources.each do |resource|
- next if self_referential_and_already_in_source(resource)
- id = resource.id
- relationships_only = already_serialized?(relationship.type, id)
- if include_linkage && !relationships_only
- add_resource(resource, ia)
- elsif include_linked_children || relationships_only
- relationships_hash(resource, fetchable_fields, ia)
+ if relationship_data[name]
+ include_data = true
+ if relationship.is_a?(JSONAPI::Relationship::ToOne)
+ rids = relationship_data[name].first
+ else
+ rids = relationship_data[name]
end
end
+
+ hash[format_key(name)] = link_object(source, relationship, rids, include_data)
end
end
end
- def cached_relationships_hash(source, include_directives)
- h = source.relationships || {}
- return h unless include_directives.has_key?(:include_related)
+ def cached_relationships_hash(source, fetchable_fields, relationship_data)
+ relationships = {}
- relationships = source.resource_klass._relationships.select do |k,v|
- source.fetchable_fields.include?(k)
+ source.relationships.try(:each_pair) do |k,v|
+ if fetchable_fields.include?(unformat_key(k).to_sym)
+ relationships[k.to_sym] = v
+ end
end
- real_res = nil
- relationships.each do |rel_name, relationship|
- key = @key_formatter.format(rel_name)
- to_many = relationship.is_a? JSONAPI::Relationship::ToMany
+ field_set = supplying_relationship_fields(source.resource_klass).collect {|k| format_key(k).to_sym } & relationships.keys
- ia = include_directives[:include_related][rel_name]
- if ia
- if h.has_key?(key)
- h[key][:data] = to_many ? [] : nil
- end
+ relationships.each_with_object({}) do |(name, relationship), hash|
+ if field_set.include?(name)
- fragments = source.preloaded_fragments[key]
- if fragments.nil?
- # The resources we want were not preloaded, we'll have to bypass the cache.
- # This happens when including through belongs_to polymorphic relationships
- if real_res.nil?
- real_res = source.to_real_resource
- end
- relation_resources = [real_res.public_send(rel_name)].flatten(1).compact
- fragments = relation_resources.map{|r| [r.id, r]}.to_h
- end
- fragments.each do |id, f|
- add_resource(f, ia)
+ relationship_name = unformat_key(name).to_sym
+ relationship_klass = source.resource_klass._relationships[relationship_name]
- if h.has_key?(key)
- # The hash already has everything we need except the :data field
- data = {
- type: format_key(f.is_a?(Resource) ? f.class._type : f.type),
- id: @id_formatter.format(id)
- }
-
- if to_many
- h[key][:data] << data
- else
- h[key][:data] = data
- end
+ if relationship_klass.is_a?(JSONAPI::Relationship::ToOne)
+ # include_linkage = @always_include_to_one_linkage_data | relationship_klass.always_include_linkage_data
+ if relationship_data[relationship_name]
+ rids = relationship_data[relationship_name].first
+ relationship['data'] = to_one_linkage(rids)
end
+ else
+ # include_linkage = relationship_klass.always_include_linkage_data
+ if relationship_data[relationship_name]
+ rids = relationship_data[relationship_name]
+ relationship['data'] = to_many_linkage(rids)
+ end
end
+
+ hash[format_key(name)] = relationship
end
end
-
- return h
end
- def already_serialized?(type, id)
- type = format_key(type)
- id = @id_formatter.format(id)
- @included_objects.key?(type) && @included_objects[type].key?(id)
- end
-
def self_link(source, relationship)
link_builder.relationships_self_link(source, relationship)
end
def related_link(source, relationship)
link_builder.relationships_related_link(source, relationship)
end
- def default_relationship_links(source, relationship)
- links = {}
- links['self'] = self_link(source, relationship) unless relationship.exclude_link?(:self)
- links['related'] = related_link(source, relationship) unless relationship.exclude_link?(:related)
- links.compact
- end
-
- def to_one_linkage(source, relationship)
- linkage_id = foreign_key_value(source, relationship)
- linkage_type = format_key(relationship.type_for_source(source))
- return unless linkage_id.present? && linkage_type.present?
-
- {
- type: linkage_type,
- id: linkage_id,
- }
- end
-
- def to_many_linkage(source, relationship)
+ def to_many_linkage(rids)
linkage = []
- include_config = include_directives.include_config(relationship.name.to_sym) if include_directives
- include_filters = include_config[:include_filters] if include_config
- options = { filters: include_filters || {} }
- linkage_types_and_values = if source.preloaded_fragments.has_key?(format_key(relationship.name))
- source.preloaded_fragments[format_key(relationship.name)].map do |_, resource|
- [relationship.type, resource.id]
+ rids && rids.each do |details|
+ id = details.id
+ type = details.resource_klass.try(:_type)
+ if type && id
+ linkage.append({'type' => format_key(type), 'id' => @id_formatter.format(id)})
end
- elsif relationship.polymorphic?
- assoc = source.public_send("records_for_#{relationship.name}", options)
- # Avoid hitting the database again for values already pre-loaded
- if assoc.respond_to?(:loaded?) and assoc.loaded?
- assoc.map do |obj|
- [obj.type.underscore.pluralize, obj.id]
- end
- else
- assoc = assoc.unscope(:includes) if assoc.is_a?(ActiveRecord::Relation)
- assoc.pluck(:type, :id).map do |type, id|
- [type.underscore.pluralize, id]
- end
- end
- else
- source.public_send(relationship.name, options).map do |value|
- [relationship.type, value.id]
- end
end
- linkage_types_and_values.each do |type, value|
- if type && value
- linkage.append({type: format_key(type), id: @id_formatter.format(value)})
- end
- end
linkage
end
- def relationship_object_to_one(source, relationship, include_linkage)
- include_linkage = include_linkage | @always_include_to_one_linkage_data | relationship.always_include_linkage_data
- relationship_object_hash = {}
+ def to_one_linkage(rid)
+ return unless rid
- links = default_relationship_links(source, relationship)
+ {
+ 'type' => format_key(rid.resource_klass._type),
+ 'id' => @id_formatter.format(rid.id),
+ }
+ end
- relationship_object_hash['links'] = links unless links.blank?
- relationship_object_hash[:data] = to_one_linkage(source, relationship) if include_linkage
- relationship_object_hash
+ def link_object_to_one(source, relationship, rid, include_data)
+ link_object_hash = {}
+ link_object_hash['links'] = {}
+ link_object_hash['links']['self'] = self_link(source, relationship)
+ link_object_hash['links']['related'] = related_link(source, relationship)
+ link_object_hash['data'] = to_one_linkage(rid) if include_data
+ link_object_hash
end
- def relationship_object_to_many(source, relationship, include_linkage)
- include_linkage = include_linkage | relationship.always_include_linkage_data
- relationship_object_hash = {}
-
- links = default_relationship_links(source, relationship)
- relationship_object_hash['links'] = links unless links.blank?
- relationship_object_hash[:data] = to_many_linkage(source, relationship) if include_linkage
- relationship_object_hash
+ def link_object_to_many(source, relationship, rids, include_data)
+ link_object_hash = {}
+ link_object_hash['links'] = {}
+ link_object_hash['links']['self'] = self_link(source, relationship)
+ link_object_hash['links']['related'] = related_link(source, relationship)
+ link_object_hash['data'] = to_many_linkage(rids) if include_data
+ link_object_hash
end
- def relationship_object(source, relationship, include_linkage = false)
+ def link_object(source, relationship, rid, include_data)
if relationship.is_a?(JSONAPI::Relationship::ToOne)
- relationship_object_to_one(source, relationship, include_linkage)
+ link_object_to_one(source, relationship, rid, include_data)
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
- relationship_object_to_many(source, relationship, include_linkage)
+ link_object_to_many(source, relationship, rid, include_data)
end
end
- # Extracts the foreign key value for a to_one relationship.
- def foreign_key_value(source, relationship)
- # If you have changed the key_name, don't even try to look at `"#{relationship.name}_id"`
- # just load the association and call the custom key_name
- foreign_key_type_changed = relationship.options[:foreign_key_type_changed] || false
- related_resource_id =
- if source.preloaded_fragments.has_key?(format_key(relationship.name))
- source.preloaded_fragments[format_key(relationship.name)].values.first.try(:id)
- elsif !foreign_key_type_changed && source.respond_to?("#{relationship.name}_id")
- # If you have direct access to the underlying id, you don't have to load the relationship
- # which can save quite a lot of time when loading a lot of data.
- # This does not apply to e.g. has_one :through relationships.
- source.public_send("#{relationship.name}_id")
- else
- source.public_send(relationship.name).try(:id)
- end
- return nil unless related_resource_id
- @id_formatter.format(related_resource_id)
- end
-
- def foreign_key_types_and_values(source, relationship)
- if relationship.is_a?(JSONAPI::Relationship::ToMany)
- if relationship.polymorphic?
- assoc = source._model.public_send(relationship.name)
- # Avoid hitting the database again for values already pre-loaded
- if assoc.respond_to?(:loaded?) and assoc.loaded?
- assoc.map do |obj|
- [obj.type.underscore.pluralize, @id_formatter.format(obj.id)]
- end
- else
- assoc.pluck(:type, :id).map do |type, id|
- [type.underscore.pluralize, @id_formatter.format(id)]
- end
- end
- else
- source.public_send(relationship.name).map do |value|
- [relationship.type, @id_formatter.format(value.id)]
- end
- end
- end
- end
-
- # Sets that an object should be included in the primary document of the response.
- def set_primary(type, id)
- type = format_key(type)
- @included_objects[type][id][:primary] = true
- end
-
- def add_resource(source, include_directives, primary = false)
- type = source.is_a?(JSONAPI::CachedResourceFragment) ? source.type : source.class._type
- id = source.id
-
- @included_objects[type] ||= {}
- existing = @included_objects[type][id]
-
- if existing.nil?
- obj_hash = object_hash(source, include_directives)
- @included_objects[type][id] = {
- primary: primary,
- object_hash: obj_hash,
- includes: Set.new(include_directives[:include_related].keys)
- }
- else
- include_related = Set.new(include_directives[:include_related].keys)
- unless existing[:includes].superset?(include_related)
- obj_hash = object_hash(source, include_directives)
- @included_objects[type][id][:object_hash].deep_merge!(obj_hash)
- @included_objects[type][id][:includes].add(include_related)
- @included_objects[type][id][:primary] = existing[:primary] | primary
- end
- end
- end
-
def generate_link_builder(primary_resource_klass, options)
LinkBuilder.new(
base_url: options.fetch(:base_url, ''),
- primary_resource_klass: primary_resource_klass,
route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
- url_helpers: options.fetch(:url_helpers, options[:controller]),
+ primary_resource_klass: primary_resource_klass,
)
end
end
end