lib/jsonapi/resource_serializer.rb in jsonapi-resources-0.4.4 vs lib/jsonapi/resource_serializer.rb in jsonapi-resources-0.5.0

- old
+ new

@@ -5,25 +5,28 @@ # include: # Purpose: determines which objects will be side loaded with the source objects in a linked section # Example: ['comments','author','comments.tags','author.posts'] # fields: # Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and - # association ids in the links section for a resource. Fields are global for a resource type. + # 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 # base_url: a string to prepend to generated resource links - def initialize(primary_resource_klass, options = {}) - @primary_resource_klass = primary_resource_klass - @primary_class_name = @primary_resource_klass._type + attr_reader :url_generator - @fields = options.fetch(:fields, {}) - @include = options.fetch(:include, []) + 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) - @route_formatter = options.fetch(:route_formatter, JSONAPI.configuration.route_formatter) - @base_url = options.fetch(:base_url, '') + @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter) + @url_generator = 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) end # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure def serialize_to_hash(source) is_resource_collection = source.respond_to?(:to_ary) @@ -49,28 +52,28 @@ primary_hash[:included] = included_objects if included_objects.size > 0 primary_hash end - def serialize_to_links_hash(source, requested_association) - if requested_association.is_a?(JSONAPI::Association::HasOne) - data = has_one_linkage(source, requested_association) + def serialize_to_links_hash(source, requested_relationship) + if requested_relationship.is_a?(JSONAPI::Relationship::ToOne) + data = to_one_linkage(source, requested_relationship) else - data = has_many_linkage(source, requested_association) + data = to_many_linkage(source, requested_relationship) end { links: { - self: self_link(source, requested_association), - related: related_link(source, requested_association) + self: self_link(source, requested_relationship), + related: related_link(source, requested_relationship) }, data: data } end def find_link(query_params) - "#{@base_url}/#{formatted_module_path_from_klass(@primary_resource_klass)}#{@route_formatter.format(@primary_resource_klass._type.to_s)}?#{query_params.to_query}" + url_generator.query_link(query_params) end private # Process the primary source object(s). This will then serialize associated object recursively based on the @@ -79,22 +82,22 @@ # The fields options controls both fields and included links references. def process_primary(source, include_directives) if source.respond_to?(:to_ary) source.each do |resource| id = resource.id - if already_serialized?(@primary_class_name, id) + if already_serialized?(resource.class._type, id) set_primary(@primary_class_name, id) end - add_included_object(@primary_class_name, id, object_hash(resource, include_directives), true) + add_included_object(id, object_hash(resource, include_directives), true) end else return {} if source.nil? resource = source id = resource.id - add_included_object(@primary_class_name, id, object_hash(source, include_directives), true) + add_included_object(id, object_hash(source, include_directives), true) end end # Returns a serialized hash for the source model def object_hash(source, include_directives) @@ -129,64 +132,64 @@ fields = requested & fields unless requested.nil? fields.each_with_object({}) do |name, hash| format = source.class._attribute_options(name)[:format] unless name == :id - hash[format_key(name)] = format_value(source.send(name), format) + hash[format_key(name)] = format_value(source.public_send(name), format) end end end def relationship_data(source, include_directives) - associations = source.class._associations + relationships = source.class._relationships requested = requested_fields(source.class._type) - fields = associations.keys + fields = relationships.keys fields = requested & fields unless requested.nil? field_set = Set.new(fields) - included_associations = source.fetchable_fields & associations.keys + included_relationships = source.fetchable_fields & relationships.keys data = {} - associations.each_with_object(data) do |(name, association), hash| - if included_associations.include? name + relationships.each_with_object(data) do |(name, relationship), hash| + if included_relationships.include? name ia = include_directives[:include_related][name] include_linkage = ia && ia[:include] include_linked_children = ia && !ia[:include_related].empty? if field_set.include?(name) - hash[format_key(name)] = link_object(source, association, include_linkage) + hash[format_key(name)] = link_object(source, relationship, include_linkage) end - type = association.type + 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 associations. + # through the relationships. if include_linkage || include_linked_children - if association.is_a?(JSONAPI::Association::HasOne) - resource = source.send(name) + if relationship.is_a?(JSONAPI::Relationship::ToOne) + resource = source.public_send(name) if resource id = resource.id - type = association.type_for_source(source) - associations_only = already_serialized?(type, id) - if include_linkage && !associations_only - add_included_object(type, id, object_hash(resource, ia)) - elsif include_linked_children || associations_only + 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 end - elsif association.is_a?(JSONAPI::Association::HasMany) - resources = source.send(name) + elsif relationship.is_a?(JSONAPI::Relationship::ToMany) + resources = source.public_send(name) resources.each do |resource| id = resource.id - associations_only = already_serialized?(type, id) - if include_linkage && !associations_only - add_included_object(type, id, object_hash(resource, ia)) - elsif include_linked_children || associations_only + 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 @@ -194,109 +197,94 @@ end end def relationship_links(source) links = {} - links[:self] = self_href(source) + links[:self] = url_generator.self_link(source) links end - def formatted_module_path(source) - formatted_module_path_from_klass(source.class) - end - - def formatted_module_path_from_klass(klass) - klass.name =~ /::[^:]+\Z/ ? (@route_formatter.format($`).freeze.gsub('::', '/') + '/').downcase : '' - end - - def self_href(source) - "#{@base_url}/#{formatted_module_path(source)}#{@route_formatter.format(source.class._type.to_s)}/#{source.id}" - end - def already_serialized?(type, id) type = format_key(type) @included_objects.key?(type) && @included_objects[type].key?(id) end - def format_route(route) - @route_formatter.format(route.to_s) + def self_link(source, relationship) + url_generator.relationships_self_link(source, relationship) end - def self_link(source, association) - "#{self_href(source)}/relationships/#{format_route(association.name)}" + def related_link(source, relationship) + url_generator.relationships_related_link(source, relationship) end - def related_link(source, association) - "#{self_href(source)}/#{format_route(association.name)}" - end - - def has_one_linkage(source, association) + def to_one_linkage(source, relationship) linkage = {} - linkage_id = foreign_key_value(source, association) + linkage_id = foreign_key_value(source, relationship) if linkage_id - linkage[:type] = format_key(association.type_for_source(source)) + linkage[:type] = format_key(relationship.type_for_source(source)) linkage[:id] = linkage_id else linkage = nil end linkage end - def has_many_linkage(source, association) + def to_many_linkage(source, relationship) linkage = [] - linkage_types_and_values = foreign_key_types_and_values(source, association) + linkage_types_and_values = foreign_key_types_and_values(source, relationship) linkage_types_and_values.each do |type, value| linkage.append({type: format_key(type), id: value}) end linkage end - def link_object_has_one(source, association) + def link_object_to_one(source, relationship, include_linkage) + include_linkage = include_linkage | @always_include_to_one_linkage_data | relationship.always_include_linkage_data link_object_hash = {} link_object_hash[:links] = {} - link_object_hash[:links][:self] = self_link(source, association) - link_object_hash[:links][:related] = related_link(source, association) - link_object_hash[:data] = has_one_linkage(source, association) + 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(source, relationship) if include_linkage link_object_hash end - def link_object_has_many(source, association, include_linkage) + def link_object_to_many(source, relationship, include_linkage) link_object_hash = {} link_object_hash[:links] = {} - link_object_hash[:links][:self] = self_link(source, association) - link_object_hash[:links][:related] = related_link(source, association) - link_object_hash[:data] = has_many_linkage(source, association) if include_linkage + 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(source, relationship) if include_linkage link_object_hash end - def link_object(source, association, include_linkage = false) - if association.is_a?(JSONAPI::Association::HasOne) - link_object_has_one(source, association) - elsif association.is_a?(JSONAPI::Association::HasMany) - link_object_has_many(source, association, include_linkage) + def link_object(source, relationship, include_linkage = false) + if relationship.is_a?(JSONAPI::Relationship::ToOne) + link_object_to_one(source, relationship, include_linkage) + elsif relationship.is_a?(JSONAPI::Relationship::ToMany) + link_object_to_many(source, relationship, include_linkage) end end - # Extracts the foreign key value for a has_one association. - def foreign_key_value(source, association) - foreign_key = association.foreign_key - value = source.send(foreign_key) + # 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) end - def foreign_key_types_and_values(source, association) - if association.is_a?(JSONAPI::Association::HasMany) - if association.polymorphic? - source.model.send(association.name).pluck(:type, :id).map do |type, id| + 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)] end else - source.send(association.foreign_key).map do |value| - [association.type, IdValueFormatter.format(value)] + source.public_send(relationship.foreign_key).map do |value| + [relationship.type, IdValueFormatter.format(value)] end end end end @@ -305,16 +293,17 @@ type = format_key(type) @included_objects[type][id][:primary] = true end # Collects the hashes for all objects processed by the serializer - def add_included_object(type, id, object_hash, primary = false) - type = format_key(type) + def add_included_object(id, object_hash, primary = false) + type = object_hash['type'] @included_objects[type] = {} unless @included_objects.key?(type) if already_serialized?(type, id) + @included_objects[type][id][:object_hash].merge!(object_hash) set_primary(type, id) if primary else @included_objects[type].store(id, primary: primary, object_hash: object_hash) end end @@ -324,8 +313,16 @@ end def format_value(value, format) value_formatter = JSONAPI::ValueFormatter.value_formatter_for(format) value_formatter.format(value) + end + + def generate_link_builder(primary_resource_klass, options) + LinkBuilder.new( + base_url: options.fetch(:base_url, ''), + route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter), + primary_resource_klass: primary_resource_klass, + ) end end end