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