lib/jsonapi/resource_serializer.rb in jsonapi-resources-0.7.1.beta1 vs lib/jsonapi/resource_serializer.rb in jsonapi-resources-0.7.1.beta2
- old
+ new
@@ -10,29 +10,36 @@
# Example: ['comments','author','comments.tags','author.posts']
# fields:
# Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and
# relationship ids in the links section for a resource. Fields are global for a resource type.
# Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
- # key_formatter: KeyFormatter class to override the default configuration
+ # key_formatter: KeyFormatter instance to override the default configuration
# serializer_options: additional options that will be passed to resource meta and links lambdas
def initialize(primary_resource_klass, options = {})
@primary_class_name = primary_resource_klass._type
@fields = options.fetch(:fields, {})
@include = options.fetch(:include, [])
@include_directives = options[:include_directives]
@key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
+ @id_formatter = ValueFormatter.value_formatter_for(:id)
@link_builder = generate_link_builder(primary_resource_klass, options)
@always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
JSONAPI.configuration.always_include_to_one_linkage_data)
@always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
JSONAPI.configuration.always_include_to_many_linkage_data)
@serialization_options = options.fetch(:serialization_options, {})
+
+ # Warning: This makes ResourceSerializer non-thread-safe. That's not a problem with the
+ # request-specific way it's currently used, though.
+ @value_formatter_type_cache = NaiveCache.new{|arg| ValueFormatter.value_formatter_for(arg) }
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.compact.map {|s| top_level_source_key(s) })
+
is_resource_collection = source.respond_to?(:to_ary)
@included_objects = {}
@include_directives ||= JSONAPI::IncludeDirectives.new(@include)
@@ -79,12 +86,11 @@
def format_key(key)
@key_formatter.format(key)
end
def format_value(value, format)
- value_formatter = JSONAPI::ValueFormatter.value_formatter_for(format)
- value_formatter.format(value)
+ @value_formatter_type_cache.get(format).format(value)
end
private
# Process the primary source object(s). This will then serialize associated object recursively based on the
@@ -112,23 +118,22 @@
id_format = 'id' if id_format == :default
obj_hash['id'] = format_value(source.id, id_format)
obj_hash['type'] = format_key(source.class._type.to_s)
- links = relationship_links(source)
+ links = links_hash(source)
obj_hash['links'] = links unless links.empty?
- attributes = attribute_hash(source)
+ attributes = attributes_hash(source)
obj_hash['attributes'] = attributes unless attributes.empty?
- relationships = relationship_data(source, include_directives)
+ relationships = relationships_hash(source, include_directives)
obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
- meta = source.meta(custom_generation_options)
- if meta.is_a?(Hash) && !meta.empty?
- obj_hash['meta'] = meta
- end
+ meta = meta_hash(source)
+ obj_hash['meta'] = meta unless meta.empty?
+
obj_hash
end
def requested_fields(klass)
return if @fields.nil? || @fields.empty?
@@ -137,11 +142,11 @@
elsif klass.superclass != JSONAPI::Resource
requested_fields(klass.superclass)
end
end
- def attribute_hash(source)
+ def attributes_hash(source)
requested = requested_fields(source.class)
fields = source.fetchable_fields & source.class._attributes.keys.to_a
fields = requested & fields unless requested.nil?
fields.each_with_object({}) do |name, hash|
@@ -157,11 +162,35 @@
serializer: self,
serialization_options: @serialization_options
}
end
- def relationship_data(source, include_directives)
+ def meta_hash(source)
+ meta = source.meta(custom_generation_options)
+ (meta.is_a?(Hash) && meta) || {}
+ end
+
+ def links_hash(source)
+ {
+ self: link_builder.self_link(source)
+ }.merge(custom_links_hash(source)).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)
+ "#{source.class}_#{source.id}"
+ end
+
+ def self_referential_and_already_in_source(resource)
+ resource && @top_level_sources.include?(top_level_source_key(resource))
+ end
+
+ def relationships_hash(source, include_directives)
relationships = source.class._relationships
requested = requested_fields(source.class)
fields = relationships.keys
fields = requested & fields unless requested.nil?
@@ -175,57 +204,35 @@
if included_relationships.include? name
ia = include_directives[:include_related][name]
include_linkage = ia && ia[:include]
include_linked_children = ia && !ia[:include_related].empty?
+ resources = (include_linkage || include_linked_children) && [source.public_send(name)].flatten.compact
if field_set.include?(name)
hash[format_key(name)] = link_object(source, relationship, include_linkage)
end
- type = relationship.type
-
# 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
- if relationship.is_a?(JSONAPI::Relationship::ToOne)
- resource = source.public_send(name)
- if resource
- id = resource.id
- type = relationship.type_for_source(source)
- relationships_only = already_serialized?(type, id)
- if include_linkage && !relationships_only
- add_included_object(id, object_hash(resource, ia))
- elsif include_linked_children || relationships_only
- relationship_data(resource, ia)
- 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_included_object(id, object_hash(resource, ia))
+ elsif include_linked_children || relationships_only
+ relationships_hash(resource, ia)
end
- elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
- resources = source.public_send(name)
- resources.each do |resource|
- id = resource.id
- relationships_only = already_serialized?(type, id)
- if include_linkage && !relationships_only
- add_included_object(id, object_hash(resource, ia))
- elsif include_linked_children || relationships_only
- relationship_data(resource, ia)
- end
- end
end
end
end
end
end
- def relationship_links(source)
- links = {}
- links[:self] = link_builder.self_link(source)
-
- links
- end
-
def already_serialized?(type, id)
type = format_key(type)
@included_objects.key?(type) && @included_objects[type].key?(id)
end
@@ -290,21 +297,29 @@
# Extracts the foreign key value for a to_one relationship.
def foreign_key_value(source, relationship)
foreign_key = relationship.foreign_key
value = source.public_send(foreign_key)
- IdValueFormatter.format(value)
+ @id_formatter.format(value)
end
def foreign_key_types_and_values(source, relationship)
if relationship.is_a?(JSONAPI::Relationship::ToMany)
if relationship.polymorphic?
- source._model.public_send(relationship.name).pluck(:type, :id).map do |type, id|
- [type.pluralize, IdValueFormatter.format(id)]
+ 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.foreign_key).map do |value|
- [relationship.type, IdValueFormatter.format(value)]
+ [relationship.type, @id_formatter.format(value)]
end
end
end
end