diff --git a/lib/jsonapi-resources.rb b/lib/jsonapi-resources.rb index 4ac0532bc..04fae654f 100644 --- a/lib/jsonapi-resources.rb +++ b/lib/jsonapi-resources.rb @@ -37,7 +37,7 @@ require 'jsonapi/active_relation/join_manager' require 'jsonapi/resource_identity' require 'jsonapi/resource_fragment' -require 'jsonapi/resource_id_tree' +require 'jsonapi/resource_tree' require 'jsonapi/resource_set' require 'jsonapi/path' require 'jsonapi/path_segment' diff --git a/lib/jsonapi/active_relation_resource.rb b/lib/jsonapi/active_relation_resource.rb index e2611613f..fa1c90469 100644 --- a/lib/jsonapi/active_relation_resource.rb +++ b/lib/jsonapi/active_relation_resource.rb @@ -2,6 +2,10 @@ module JSONAPI class ActiveRelationResource < BasicResource root_resource + def find_related_ids(relationship, options = {}) + self.class.find_related_fragments([self], relationship.name, options).keys.collect { |rid| rid.id } + end + class << self # Finds Resources using the `filters`. Pagination and sort options are used when provided # @@ -90,8 +94,11 @@ def find_to_populate_by_keys(keys, options = {}) # the ResourceInstances matching the filters, sorting, and pagination rules along with any request # additional_field values def find_fragments(filters, options = {}) - include_directives = options[:include_directives] ? options[:include_directives].include_directives : {} + include_directives = options.fetch(:include_directives, {}) resource_klass = self + + fragments = {} + linkage_relationships = to_one_relationships_for_linkage(include_directives[:include_related]) sort_criteria = options.fetch(:sort_criteria) { [] } @@ -129,18 +136,26 @@ def find_fragments(filters, options = {}) if linkage_relationship.polymorphic? && linkage_relationship.belongs_to? linkage_relationship.resource_types.each do |resource_type| klass = resource_klass_for(resource_type) - linkage_fields << {relationship_name: name, resource_klass: klass} - linkage_table_alias = join_manager.join_details_by_polymorphic_relationship(linkage_relationship, resource_type)[:alias] primary_key = klass._primary_key + + linkage_fields << {relationship_name: name, + resource_klass: klass, + field: "#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}", + alias: "#{linkage_table_alias}_#{primary_key}"} + pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") end else klass = linkage_relationship.resource_klass - linkage_fields << {relationship_name: name, resource_klass: klass} - linkage_table_alias = join_manager.join_details_by_relationship(linkage_relationship)[:alias] primary_key = klass._primary_key + + linkage_fields << {relationship_name: name, + resource_klass: klass, + field: "#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}", + alias: "#{linkage_table_alias}_#{primary_key}"} + pluck_fields << Arel.sql("#{concat_table_field(linkage_table_alias, primary_key)} AS #{linkage_table_alias}_#{primary_key}") end end @@ -158,7 +173,6 @@ def find_fragments(filters, options = {}) pluck_fields << Arel.sql(field) end - fragments = {} rows = records.pluck(*pluck_fields) rows.each do |row| rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0]) @@ -204,23 +218,23 @@ def find_fragments(filters, options = {}) # @return [Hash{ResourceIdentity => {identity: => ResourceIdentity, cache: cache_field, attributes: => {name => value}, related: {relationship_name: [] }}}] # the ResourceInstances matching the filters, sorting, and pagination rules along with any request # additional_field values - def find_related_fragments(source_rids, relationship_name, options = {}) + def find_related_fragments(source, relationship_name, options = {}) relationship = _relationship(relationship_name) if relationship.polymorphic? # && relationship.foreign_key_on == :self - find_related_polymorphic_fragments(source_rids, relationship, options, false) + find_related_polymorphic_fragments(source, relationship, options, false) else - find_related_monomorphic_fragments(source_rids, relationship, options, false) + find_related_monomorphic_fragments(source, relationship, options, false) end end - def find_included_fragments(source_rids, relationship_name, options) + def find_included_fragments(source, relationship_name, options) relationship = _relationship(relationship_name) if relationship.polymorphic? # && relationship.foreign_key_on == :self - find_related_polymorphic_fragments(source_rids, relationship, options, true) + find_related_polymorphic_fragments(source, relationship, options, true) else - find_related_monomorphic_fragments(source_rids, relationship, options, true) + find_related_monomorphic_fragments(source, relationship, options, true) end end @@ -231,7 +245,7 @@ def find_included_fragments(source_rids, relationship_name, options) # @option options [Hash] :context The context of the request, set in the controller # # @return [Integer] the count - def count_related(source_rid, relationship_name, options = {}) + def count_related(source_resource, relationship_name, options = {}) relationship = _relationship(relationship_name) related_klass = relationship.resource_klass @@ -244,7 +258,7 @@ def count_related(source_rid, relationship_name, options = {}) records = apply_request_settings_to_records(records: records(options), resource_klass: related_klass, - primary_keys: source_rid.id, + primary_keys: source_resource.id, join_manager: join_manager, filters: filters, options: options) @@ -375,11 +389,11 @@ def find_records_by_keys(keys, options = {}) apply_request_settings_to_records(records: records(options), primary_keys: keys, options: options) end - def find_related_monomorphic_fragments(source_rids, relationship, options, connect_source_identity) + def find_related_monomorphic_fragments(source_fragments, relationship, options, connect_source_identity) filters = options.fetch(:filters, {}) - source_ids = source_rids.collect {|rid| rid.id} + source_ids = source_fragments.collect {|item| item.identity.id} - include_directives = options[:include_directives] ? options[:include_directives].include_directives : {} + include_directives = options.fetch(:include_directives, {}) resource_klass = relationship.resource_klass linkage_relationships = resource_klass.to_one_relationships_for_linkage(include_directives[:include_related]) @@ -501,12 +515,12 @@ def find_related_monomorphic_fragments(source_rids, relationship, options, conne # Gets resource identities where the related resource is polymorphic and the resource type and id # are stored on the primary resources. Cache fields will always be on the related resources. - def find_related_polymorphic_fragments(source_rids, relationship, options, connect_source_identity) + def find_related_polymorphic_fragments(source_fragments, relationship, options, connect_source_identity) filters = options.fetch(:filters, {}) - source_ids = source_rids.collect {|rid| rid.id} + source_ids = source_fragments.collect {|item| item.identity.id} resource_klass = relationship.resource_klass - include_directives = options[:include_directives] ? options[:include_directives].include_directives : {} + include_directives = options.fetch(:include_directives, {}) linkage_relationships = [] diff --git a/lib/jsonapi/basic_resource.rb b/lib/jsonapi/basic_resource.rb index ea8b19ea7..73374f18a 100644 --- a/lib/jsonapi/basic_resource.rb +++ b/lib/jsonapi/basic_resource.rb @@ -44,8 +44,12 @@ def identity JSONAPI::ResourceIdentity.new(self.class, id) end + def cache_field_value + _model.public_send(self.class._cache_field) + end + def cache_id - [id, self.class.hash_cache_field(_model.public_send(self.class._cache_field))] + [id, self.class.hash_cache_field(cache_field_value)] end def is_new? @@ -285,9 +289,7 @@ def _replace_to_many_links(relationship_type, relationship_key_values, options) reflect = reflect_relationship?(relationship, options) if reflect - existing_rids = self.class.find_related_fragments([identity], relationship_type, options) - - existing = existing_rids.keys.collect { |rid| rid.id } + existing = find_related_ids(relationship, options) to_delete = existing - (relationship_key_values & existing) to_delete.each do |key| @@ -417,6 +419,10 @@ def _replace_fields(field_data) :completed end + def find_related_ids(relationship, options = {}) + send(relationship.foreign_key) + end + class << self def inherited(subclass) subclass.abstract(false) @@ -636,7 +642,7 @@ def model_name(model, options = {}) end def model_hint(model: _model_name, resource: _type) - resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s + resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::BasicResource)) ? resource._type : resource.to_s _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s end @@ -709,7 +715,7 @@ def resources_for(records, context) end def resource_for(model_record, context) - resource_klass = self.resource_klass_for_model(model_record) + resource_klass = resource_klass_for_model(model_record) resource_klass.new(model_record, context) end @@ -1083,7 +1089,7 @@ def _add_relationship(klass, *attrs) end end - # ResourceBuilder methods + # ResourceBuilder methods def define_relationship_methods(relationship_name, relationship_klass, options) relationship = register_relationship( relationship_name, diff --git a/lib/jsonapi/include_directives.rb b/lib/jsonapi/include_directives.rb index c1a1d7b3b..a75b5adcd 100644 --- a/lib/jsonapi/include_directives.rb +++ b/lib/jsonapi/include_directives.rb @@ -25,8 +25,8 @@ def initialize(resource_klass, includes_array) end end - def include_directives - @include_directives_hash + def [](name) + @include_directives_hash[name] end private diff --git a/lib/jsonapi/processor.rb b/lib/jsonapi/processor.rb index de3459c82..88c455590 100644 --- a/lib/jsonapi/processor.rb +++ b/lib/jsonapi/processor.rb @@ -49,7 +49,7 @@ def find verified_filters = resource_klass.verify_filters(filters, context) - find_options = { + options = { context: context, sort_criteria: sort_criteria, paginator: paginator, @@ -58,11 +58,9 @@ def find include_directives: include_directives } - resource_set = find_resource_set(resource_klass, - include_directives, - find_options) + resource_set = find_resource_set(include_directives, options) - resource_set.populate!(serializer, context, find_options) + resource_set.populate!(serializer, context, options) page_options = result_options if (JSONAPI.configuration.top_level_meta_include_record_count || (paginator && paginator.class.requires_record_count)) @@ -79,7 +77,7 @@ def find page_options[:pagination_params] = paginator.links_page_params(page_options.merge(fetched_resources: resource_set)) end - return JSONAPI::ResourcesSetOperationResult.new(:ok, resource_set, page_options) + JSONAPI::ResourcesSetOperationResult.new(:ok, resource_set, page_options) end def show @@ -90,21 +88,19 @@ def show key = resource_klass.verify_key(id, context) - find_options = { + options = { context: context, fields: fields, filters: { resource_klass._primary_key => key }, include_directives: include_directives } - resource_set = find_resource_set(resource_klass, - include_directives, - find_options) + resource_set = find_resource_set(include_directives, options) fail JSONAPI::Exceptions::RecordNotFound.new(id) if resource_set.resource_klasses.empty? - resource_set.populate!(serializer, context, find_options) + resource_set.populate!(serializer, context, options) - return JSONAPI::ResourceSetOperationResult.new(:ok, resource_set, result_options) + JSONAPI::ResourceSetOperationResult.new(:ok, resource_set, result_options) end def show_relationship @@ -117,25 +113,26 @@ def show_relationship parent_resource = resource_klass.find_by_key(parent_key, context: context) - find_options = { - context: context, - sort_criteria: sort_criteria, - paginator: paginator, - fields: fields, - include_directives: include_directives + options = { + context: context, + sort_criteria: sort_criteria, + paginator: paginator, + fields: fields, + include_directives: include_directives } - resource_id_tree = find_related_resource_id_tree(resource_klass, - JSONAPI::ResourceIdentity.new(resource_klass, parent_key), - relationship_type, - find_options, - nil) - - return JSONAPI::RelationshipOperationResult.new(:ok, - parent_resource, - resource_klass._relationship(relationship_type), - resource_id_tree.fragments.keys, - result_options) + resource_tree = find_related_resource_tree( + parent_resource, + relationship_type, + options, + nil + ) + + JSONAPI::RelationshipOperationResult.new(:ok, + parent_resource, + resource_klass._relationship(relationship_type), + resource_tree.fragments.keys, + result_options) end def show_related_resource @@ -146,11 +143,11 @@ def show_related_resource serializer = params[:serializer] fields = params[:fields] - find_options = { - context: context, - fields: fields, - filters: {}, - include_directives: include_directives + options = { + context: context, + fields: fields, + filters: {}, + include_directives: include_directives } source_resource = source_klass.find_by_key(source_id, context: context, fields: fields) @@ -158,11 +155,11 @@ def show_related_resource resource_set = find_related_resource_set(source_resource, relationship_type, include_directives, - find_options) + options) - resource_set.populate!(serializer, context, find_options) + resource_set.populate!(serializer, context, options) - return JSONAPI::ResourceSetOperationResult.new(:ok, resource_set, result_options) + JSONAPI::ResourceSetOperationResult.new(:ok, resource_set, result_options) end def show_related_resources @@ -178,8 +175,8 @@ def show_related_resources verified_filters = resource_klass.verify_filters(filters, context) - find_options = { - filters: verified_filters, + options = { + filters: verified_filters, sort_criteria: sort_criteria, paginator: paginator, fields: fields, @@ -192,19 +189,19 @@ def show_related_resources resource_set = find_related_resource_set(source_resource, relationship_type, include_directives, - find_options) + options) - resource_set.populate!(serializer, context, find_options) + resource_set.populate!(serializer, context, options) opts = result_options if ((JSONAPI.configuration.top_level_meta_include_record_count) || - (paginator && paginator.class.requires_record_count) || - (JSONAPI.configuration.top_level_meta_include_page_count)) + (paginator && paginator.class.requires_record_count) || + (JSONAPI.configuration.top_level_meta_include_page_count)) opts[:record_count] = source_resource.class.count_related( - source_resource.identity, - relationship_type, - find_options) + source_resource, + relationship_type, + options) end if (JSONAPI.configuration.top_level_meta_include_page_count && opts[:record_count]) @@ -219,11 +216,11 @@ def show_related_resources {} end - return JSONAPI::RelatedResourcesSetOperationResult.new(:ok, - source_resource, - relationship_type, - resource_set, - opts) + JSONAPI::RelatedResourcesSetOperationResult.new(:ok, + source_resource, + relationship_type, + resource_set, + opts) end def create_resource @@ -235,20 +232,18 @@ def create_resource resource = resource_klass.create(context) result = resource.replace_fields(data) - find_options = { - context: context, - fields: fields, - filters: { resource_klass._primary_key => resource.id }, - include_directives: include_directives + options = { + context: context, + fields: fields, + filters: { resource_klass._primary_key => resource.id }, + include_directives: include_directives } - resource_set = find_resource_set(resource_klass, - include_directives, - find_options) + resource_set = find_resource_set(include_directives, options) - resource_set.populate!(serializer, context, find_options) + resource_set.populate!(serializer, context, options) - return JSONAPI::ResourceSetOperationResult.new((result == :completed ? :created : :accepted), resource_set, result_options) + JSONAPI::ResourceSetOperationResult.new((result == :completed ? :created : :accepted), resource_set, result_options) end def remove_resource @@ -257,7 +252,7 @@ def remove_resource resource = resource_klass.find_by_key(resource_id, context: context) result = resource.remove - return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) end def replace_fields @@ -272,20 +267,18 @@ def replace_fields result = resource.replace_fields(data) - find_options = { - context: context, - fields: fields, - filters: { resource_klass._primary_key => resource.id }, - include_directives: include_directives + options = { + context: context, + fields: fields, + filters: { resource_klass._primary_key => resource.id }, + include_directives: include_directives } - resource_set = find_resource_set(resource_klass, - include_directives, - find_options) + resource_set = find_resource_set(include_directives, options) - resource_set.populate!(serializer, context, find_options) + resource_set.populate!(serializer, context, options) - return JSONAPI::ResourceSetOperationResult.new((result == :completed ? :ok : :accepted), resource_set, result_options) + JSONAPI::ResourceSetOperationResult.new((result == :completed ? :ok : :accepted), resource_set, result_options) end def replace_to_one_relationship @@ -296,7 +289,7 @@ def replace_to_one_relationship resource = resource_klass.find_by_key(resource_id, context: context) result = resource.replace_to_one_link(relationship_type, key_value) - return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) end def replace_polymorphic_to_one_relationship @@ -308,7 +301,7 @@ def replace_polymorphic_to_one_relationship resource = resource_klass.find_by_key(resource_id, context: context) result = resource.replace_polymorphic_to_one_link(relationship_type, key_value, key_type) - return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) end def create_to_many_relationships @@ -319,7 +312,7 @@ def create_to_many_relationships resource = resource_klass.find_by_key(resource_id, context: context) result = resource.create_to_many_links(relationship_type, data) - return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) end def replace_to_many_relationships @@ -330,7 +323,7 @@ def replace_to_many_relationships resource = resource_klass.find_by_key(resource_id, context: context) result = resource.replace_to_many_links(relationship_type, data) - return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) end def remove_to_many_relationships @@ -347,7 +340,7 @@ def remove_to_many_relationships complete = false end end - return JSONAPI::OperationResult.new(complete ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(complete ? :no_content : :accepted, result_options) end def remove_to_one_relationship @@ -357,7 +350,7 @@ def remove_to_one_relationship resource = resource_klass.find_by_key(resource_id, context: context) result = resource.remove_to_one_link(relationship_type) - return JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) + JSONAPI::OperationResult.new(result == :completed ? :no_content : :accepted, result_options) end def result_options @@ -366,91 +359,46 @@ def result_options options end - def find_resource_set(resource_klass, include_directives, options) - include_related = include_directives.include_directives[:include_related] if include_directives + def find_resource_set(include_directives, options) + include_related = include_directives[:include_related] if include_directives - resource_id_tree = find_resource_id_tree(resource_klass, options, include_related) + resource_tree = find_resource_tree(options, include_related) - JSONAPI::ResourceSet.new(resource_id_tree) + JSONAPI::ResourceSet.new(resource_tree) end def find_related_resource_set(resource, relationship_name, include_directives, options) - include_related = include_directives.include_directives[:include_related] if include_directives + include_related = include_directives[:include_related] if include_directives - resource_id_tree = find_resource_id_tree_from_resource_relationship(resource, relationship_name, options, include_related) + resource_tree = find_resource_tree_from_relationship(resource, relationship_name, options, include_related) - JSONAPI::ResourceSet.new(resource_id_tree) + JSONAPI::ResourceSet.new(resource_tree) end - private - def find_related_resource_id_tree(resource_klass, source_id, relationship_name, find_options, include_related) - options = find_options.except(:include_directives) + def find_resource_tree(options, include_related) options[:cache] = resource_klass.caching? - fragments = resource_klass.find_included_fragments([source_id], relationship_name, options) - - primary_resource_id_tree = PrimaryResourceIdTree.new - primary_resource_id_tree.add_resource_fragments(fragments, include_related) - - load_included(resource_klass, primary_resource_id_tree, include_related, options) - - primary_resource_id_tree + fragments = resource_klass.find_fragments(options[:filters], options) + PrimaryResourceTree.new(fragments: fragments, include_related: include_related, options: options) end - def find_resource_id_tree(resource_klass, find_options, include_related) - options = find_options + def find_related_resource_tree(parent_resource, relationship_name, options, include_related) + options = options.except(:include_directives) options[:cache] = resource_klass.caching? - fragments = resource_klass.find_fragments(find_options[:filters], options) - - primary_resource_id_tree = PrimaryResourceIdTree.new - primary_resource_id_tree.add_resource_fragments(fragments, include_related) - - load_included(resource_klass, primary_resource_id_tree, include_related, options) - - primary_resource_id_tree + fragments = resource_klass.find_included_fragments([parent_resource], relationship_name, options) + PrimaryResourceTree.new(fragments: fragments, include_related: include_related, options: options) end - def find_resource_id_tree_from_resource_relationship(resource, relationship_name, find_options, include_related) + def find_resource_tree_from_relationship(resource, relationship_name, options, include_related) relationship = resource.class._relationship(relationship_name) - options = find_options.except(:include_directives) + options = options.except(:include_directives) options[:cache] = relationship.resource_klass.caching? - fragments = resource.class.find_related_fragments([resource.identity], relationship_name, options) - - primary_resource_id_tree = PrimaryResourceIdTree.new - primary_resource_id_tree.add_resource_fragments(fragments, include_related) + fragments = resource.class.find_related_fragments([resource], relationship_name, options) - load_included(resource_klass, primary_resource_id_tree, include_related, options) - - primary_resource_id_tree - end - - def load_included(resource_klass, source_resource_id_tree, include_related, options) - source_rids = source_resource_id_tree.fragments.keys - - include_related.try(:each_key) do |key| - relationship = resource_klass._relationship(key) - relationship_name = relationship.name.to_sym - - find_related_resource_options = options.except(:filters, :sort_criteria, :paginator) - find_related_resource_options[:sort_criteria] = relationship.resource_klass.default_sort - find_related_resource_options[:cache] = resource_klass.caching? - - related_fragments = resource_klass.find_included_fragments( - source_rids, relationship_name, find_related_resource_options - ) - - related_resource_id_tree = source_resource_id_tree.fetch_related_resource_id_tree(relationship) - related_resource_id_tree.add_resource_fragments(related_fragments, include_related[key][include_related]) - - # Now recursively get the related resources for the currently found resources - load_included(relationship.resource_klass, - related_resource_id_tree, - include_related[relationship_name][:include_related], - options) - end + PrimaryResourceTree.new(fragments: fragments, include_related: include_related, options: options) end end end diff --git a/lib/jsonapi/relationship.rb b/lib/jsonapi/relationship.rb index 77e700b78..6ed3c54b8 100644 --- a/lib/jsonapi/relationship.rb +++ b/lib/jsonapi/relationship.rb @@ -24,7 +24,7 @@ def initialize(name, options = {}) end @always_include_optional_linkage_data = options.fetch(:always_include_optional_linkage_data, false) == true - @eager_load_on_include = options.fetch(:eager_load_on_include, false) == true + @eager_load_on_include = options.fetch(:eager_load_on_include, true) == true @allow_include = options[:allow_include] @class_name = nil @inverse_relationship = nil diff --git a/lib/jsonapi/resource_fragment.rb b/lib/jsonapi/resource_fragment.rb index 933ad6b8e..188e4caef 100644 --- a/lib/jsonapi/resource_fragment.rb +++ b/lib/jsonapi/resource_fragment.rb @@ -6,34 +6,41 @@ module JSONAPI # cache - the value of the cache field for the resource instance # related - a hash of arrays of related resource identities, grouped by relationship name # related_from - a set of related resource identities that loaded the fragment + # resource - a resource instance # # Todo: optionally use these for faster responses by bypassing model instantiation) # attributes - resource attributes class ResourceFragment - attr_reader :identity, :attributes, :related_from, :related + attr_reader :identity, :attributes, :related_from, :related, :resource attr_accessor :primary, :cache alias :cache_field :cache #ToDo: Rename one or the other - def initialize(identity) + def initialize(identity, resource: nil, cache: nil, primary: false) @identity = identity - @cache = nil + @cache = cache + @resource = resource + @primary = primary + @attributes = {} @related = {} - @primary = false @related_from = Set.new end def initialize_related(relationship_name) - @related ||= {} @related[relationship_name.to_sym] ||= Set.new end def add_related_identity(relationship_name, identity) initialize_related(relationship_name) - @related[relationship_name.to_sym] << identity + @related[relationship_name.to_sym] << identity if identity + end + + def merge_related_identities(relationship_name, identities) + initialize_related(relationship_name) + @related[relationship_name.to_sym].merge(identities) if identities end def add_related_from(identity) diff --git a/lib/jsonapi/resource_id_tree.rb b/lib/jsonapi/resource_id_tree.rb deleted file mode 100644 index 2bb2f456f..000000000 --- a/lib/jsonapi/resource_id_tree.rb +++ /dev/null @@ -1,112 +0,0 @@ -module JSONAPI - - # A tree structure representing the resource structure of the requested resource(s). This is an intermediate structure - # used to keep track of the resources, by identity, found at different included relationships. It will be flattened and - # the resource instances will be fetched from the cache or the record store. - class ResourceIdTree - - attr_reader :fragments, :related_resource_id_trees - - # Gets the related Resource Id Tree for a relationship, and creates it first if it does not exist - # - # @param relationship [JSONAPI::Relationship] - # - # @return [JSONAPI::RelatedResourceIdTree] the new or existing resource id tree for the requested relationship - def fetch_related_resource_id_tree(relationship) - relationship_name = relationship.name.to_sym - @related_resource_id_trees[relationship_name] ||= RelatedResourceIdTree.new(relationship, self) - end - - private - - def init_included_relationships(fragment, include_related) - include_related && include_related.each_key do |relationship_name| - fragment.initialize_related(relationship_name) - end - end - end - - class PrimaryResourceIdTree < ResourceIdTree - - # Creates a PrimaryResourceIdTree with no resources and no related ResourceIdTrees - def initialize - @fragments ||= {} - @related_resource_id_trees ||= {} - end - - # Adds each Resource Fragment to the Resources hash - # - # @param fragments [Hash] - # @param include_related [Hash] - # - # @return [null] - def add_resource_fragments(fragments, include_related) - fragments.each_value do |fragment| - add_resource_fragment(fragment, include_related) - end - end - - # Adds a Resource Fragment to the Resources hash - # - # @param fragment [JSONAPI::ResourceFragment] - # @param include_related [Hash] - # - # @return [null] - def add_resource_fragment(fragment, include_related) - fragment.primary = true - - init_included_relationships(fragment, include_related) - - @fragments[fragment.identity] = fragment - end - end - - class RelatedResourceIdTree < ResourceIdTree - - attr_reader :parent_relationship, :source_resource_id_tree - - # Creates a RelatedResourceIdTree with no resources and no related ResourceIdTrees. A connection to the parent - # ResourceIdTree is maintained. - # - # @param parent_relationship [JSONAPI::Relationship] - # @param source_resource_id_tree [JSONAPI::ResourceIdTree] - # - # @return [JSONAPI::RelatedResourceIdTree] the new or existing resource id tree for the requested relationship - def initialize(parent_relationship, source_resource_id_tree) - @fragments ||= {} - @related_resource_id_trees ||= {} - - @parent_relationship = parent_relationship - @parent_relationship_name = parent_relationship.name.to_sym - @source_resource_id_tree = source_resource_id_tree - end - - # Adds each Resource Fragment to the Resources hash - # - # @param fragments [Hash] - # @param include_related [Hash] - # - # @return [null] - def add_resource_fragments(fragments, include_related) - fragments.each_value do |fragment| - add_resource_fragment(fragment, include_related) - end - end - - # Adds a Resource Fragment to the fragments hash - # - # @param fragment [JSONAPI::ResourceFragment] - # @param include_related [Hash] - # - # @return [null] - def add_resource_fragment(fragment, include_related) - init_included_relationships(fragment, include_related) - - fragment.related_from.each do |rid| - @source_resource_id_tree.fragments[rid].add_related_identity(parent_relationship.name, fragment.identity) - end - - @fragments[fragment.identity] = fragment - end - end -end \ No newline at end of file diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index c7adb6c18..d3a03a631 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -3,7 +3,7 @@ class ResourceSerializer attr_reader :link_builder, :key_formatter, :serialization_options, :fields, :include_directives, :always_include_to_one_linkage_data, - :always_include_to_many_linkage_data + :always_include_to_many_linkage_data, :options # initialize # Options can include @@ -18,11 +18,12 @@ class ResourceSerializer # serialization_options: additional options that will be passed to resource meta and links lambdas def initialize(primary_resource_klass, options = {}) + @options = options @primary_resource_klass = primary_resource_klass @fields = options.fetch(:fields, {}) @include = options.fetch(:include, []) - @include_directives = options[:include_directives] - @include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include) + @include_directives = options.fetch(:include_directives, + JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)) @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) @@ -41,6 +42,19 @@ def initialize(primary_resource_klass, options = {}) @_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) + include_related = include_directives[:include_related] + resource_set = JSONAPI::ResourceSet.new(source, include_related, options) + resource_set.populate!(self, options[:context], options) + + if source.is_a?(Array) + serialize_resource_set_to_hash_plural(resource_set) + else + serialize_resource_set_to_hash_single(resource_set) + end + end + # Converts a resource_set to a hash, conforming to the JSONAPI structure def serialize_resource_set_to_hash_single(resource_set) diff --git a/lib/jsonapi/resource_set.rb b/lib/jsonapi/resource_set.rb index b1fb136bf..5cf6c6bbc 100644 --- a/lib/jsonapi/resource_set.rb +++ b/lib/jsonapi/resource_set.rb @@ -5,12 +5,24 @@ class ResourceSet attr_reader :resource_klasses, :populated - def initialize(resource_id_tree = nil) + def initialize(source, include_related = nil, options = nil) @populated = false - @resource_klasses = resource_id_tree.nil? ? {} : flatten_resource_id_tree(resource_id_tree) + tree = if source.is_a?(JSONAPI::ResourceTree) + source + elsif source.class < JSONAPI::BasicResource + JSONAPI::PrimaryResourceTree.new(resource: source, include_related: include_related, options: options) + elsif source.is_a?(Array) + JSONAPI::PrimaryResourceTree.new(resources: source, include_related: include_related, options: options) + end + + if tree + @resource_klasses = flatten_resource_tree(tree) + end end - def populate!(serializer, context, find_options) + def populate!(serializer, context, options) + return if @populated + # For each resource klass we want to generate the caching key # Hash for collecting types and ids @@ -21,9 +33,9 @@ def populate!(serializer, context, find_options) # @type [Lookup[]] lookups = [] - # Step One collect all of the lookups for the cache, or keys that don't require cache access @resource_klasses.each_key do |resource_klass| + missed_resource_ids[resource_klass] ||= [] serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_") context_json = resource_klass.attribute_caching_context(context).to_json @@ -47,7 +59,13 @@ def populate!(serializer, context, find_options) ) ) else - missed_resource_ids[resource_klass] = @resource_klasses[resource_klass].keys + @resource_klasses[resource_klass].keys.each do |k| + if @resource_klasses[resource_klass][k][:resource].nil? + missed_resource_ids[resource_klass] << k + else + register_resource(resource_klass, @resource_klasses[resource_klass][k][:resource]) + end + end end end @@ -60,7 +78,6 @@ def populate!(serializer, context, find_options) found_resources = {} end - # Step Three collect the results and collect hit/miss stats stats = {} found_resources.each do |resource_klass, resources| @@ -72,7 +89,6 @@ def populate!(serializer, context, find_options) stats[resource_klass][:misses] += 1 # Collect misses - missed_resource_ids[resource_klass] ||= [] missed_resource_ids[resource_klass].push(id) else stats[resource_klass][:hits] ||= 0 @@ -89,14 +105,15 @@ def populate!(serializer, context, find_options) # Step Four find any of the missing resources and join them into the result missed_resource_ids.each_pair do |resource_klass, ids| - find_opts = {context: context, fields: find_options[:fields]} + next if ids.empty? + + find_opts = {context: context, fields: options[:fields]} found_resources = resource_klass.find_to_populate_by_keys(ids, find_opts) found_resources.each do |resource| relationship_data = @resource_klasses[resource_klass][resource.id][:relationships] if resource_klass.caching? - serializer_config_key = serializer.config_key(resource_klass).gsub("/", "_") context_json = resource_klass.attribute_caching_context(context).to_json context_b64 = JSONAPI.configuration.resource_cache_digest_function.call(context_json) @@ -148,8 +165,8 @@ def report_stats(stats) end end - def flatten_resource_id_tree(resource_id_tree, flattened_tree = {}) - resource_id_tree.fragments.each_pair do |resource_rid, fragment| + def flatten_resource_tree(resource_tree, flattened_tree = {}) + resource_tree.fragments.each_pair do |resource_rid, fragment| resource_klass = resource_rid.resource_klass id = resource_rid.id @@ -157,7 +174,8 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {}) flattened_tree[resource_klass] ||= {} flattened_tree[resource_klass][id] ||= {primary: fragment.primary, relationships: {}} - flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache + flattened_tree[resource_klass][id][:cache_id] ||= fragment.cache if fragment.cache + flattened_tree[resource_klass][id][:resource] ||= fragment.resource if fragment.resource fragment.related.try(:each_pair) do |relationship_name, related_rids| flattened_tree[resource_klass][id][:relationships][relationship_name] ||= Set.new @@ -165,9 +183,9 @@ def flatten_resource_id_tree(resource_id_tree, flattened_tree = {}) end end - related_resource_id_trees = resource_id_tree.related_resource_id_trees - related_resource_id_trees.try(:each_value) do |related_resource_id_tree| - flatten_resource_id_tree(related_resource_id_tree, flattened_tree) + related_resource_trees = resource_tree.related_resource_trees + related_resource_trees.try(:each_value) do |related_resource_tree| + flatten_resource_tree(related_resource_tree, flattened_tree) end flattened_tree diff --git a/lib/jsonapi/resource_tree.rb b/lib/jsonapi/resource_tree.rb new file mode 100644 index 000000000..7f873c324 --- /dev/null +++ b/lib/jsonapi/resource_tree.rb @@ -0,0 +1,234 @@ +module JSONAPI + + # A tree structure representing the resource structure of the requested resource(s). This is an intermediate structure + # used to keep track of the resources, by identity, found at different included relationships. It will be flattened and + # the resource instances will be fetched from the cache or the record store. + class ResourceTree + + attr_reader :fragments, :related_resource_trees + + # Gets the related Resource Id Tree for a relationship, and creates it first if it does not exist + # + # @param relationship [JSONAPI::Relationship] + # + # @return [JSONAPI::RelatedResourceTree] the new or existing resource id tree for the requested relationship + def get_related_resource_tree(relationship) + relationship_name = relationship.name.to_sym + @related_resource_trees[relationship_name] ||= RelatedResourceTree.new(relationship, self) + end + + # Adds each Resource Fragment to the Resources hash + # + # @param fragments [Hash] + # @param include_related [Hash] + # + # @return [null] + def add_resource_fragments(fragments, include_related) + fragments.each_value do |fragment| + add_resource_fragment(fragment, include_related) + end + end + + # Adds a Resource Fragment to the fragments hash + # + # @param fragment [JSONAPI::ResourceFragment] + # @param include_related [Hash] + # + # @return [null] + def add_resource_fragment(fragment, include_related) + init_included_relationships(fragment, include_related) + + @fragments[fragment.identity] = fragment + end + + # Adds each Resource to the fragments hash + # + # @param resource [Hash] + # @param include_related [Hash] + # + # @return [null] + def add_resources(resources, include_related) + resources.each do |resource| + add_resource_fragment(JSONAPI::ResourceFragment.new(resource.identity, resource: resource), include_related) + end + end + + # Adds a Resource to the fragments hash + # + # @param fragment [JSONAPI::ResourceFragment] + # @param include_related [Hash] + # + # @return [null] + def add_resource(resource, include_related) + add_resource_fragment(JSONAPI::ResourceFragment.new(resource.identity, resource: resource), include_related) + end + + private + + def init_included_relationships(fragment, include_related) + include_related && include_related.each_key do |relationship_name| + fragment.initialize_related(relationship_name) + end + end + + def load_included(resource_klass, source_resource_tree, include_related, options) + include_related.try(:each_key) do |key| + relationship = resource_klass._relationship(key) + relationship_name = relationship.name.to_sym + + find_related_resource_options = options.except(:filters, :sort_criteria, :paginator) + find_related_resource_options[:sort_criteria] = relationship.resource_klass.default_sort + find_related_resource_options[:cache] = resource_klass.caching? + + related_fragments = resource_klass.find_included_fragments(source_resource_tree.fragments.values, + relationship_name, + find_related_resource_options) + + related_resource_tree = source_resource_tree.get_related_resource_tree(relationship) + related_resource_tree.add_resource_fragments(related_fragments, include_related[key][include_related]) + + # Now recursively get the related resources for the currently found resources + load_included(relationship.resource_klass, + related_resource_tree, + include_related[relationship_name][:include_related], + options) + end + end + + def add_resources_to_tree(resource_klass, + tree, + resources, + include_related, + source_rid: nil, + source_relationship_name: nil, + connect_source_identity: true) + fragments = {} + + resources.each do |resource| + next unless resource + + # fragments[resource.identity] ||= ResourceFragment.new(resource.identity, resource: resource) + # resource_fragment = fragments[resource.identity] + # ToDo: revert when not needed for testing + resource_fragment = if fragments[resource.identity] + fragments[resource.identity] + else + fragments[resource.identity] = ResourceFragment.new(resource.identity, resource: resource) + fragments[resource.identity] + end + + if resource.class.caching? + resource_fragment.cache = resource.cache_field_value + end + + linkage_relationships = resource_klass.to_one_relationships_for_linkage(resource.class, include_related) + linkage_relationships.each do |relationship_name| + related_resource = resource.send(relationship_name) + resource_fragment.add_related_identity(relationship_name, related_resource&.identity) + end + + if source_rid && connect_source_identity + resource_fragment.add_related_from(source_rid) + source_klass = source_rid.resource_klass + related_relationship_name = source_klass._relationships[source_relationship_name].inverse_relationship + if related_relationship_name + resource_fragment.add_related_identity(related_relationship_name, source_rid) + end + end + end + + tree.add_resource_fragments(fragments, include_related) + end + end + + class PrimaryResourceTree < ResourceTree + + # Creates a PrimaryResourceTree with no resources and no related ResourceTrees + def initialize(fragments: nil, resources: nil, resource: nil, include_related: nil, options: nil) + @fragments ||= {} + @related_resource_trees ||= {} + if fragments || resources || resource + if fragments + add_resource_fragments(fragments, include_related) + end + + if resources + add_resources(resources, include_related) + end + + if resource + add_resource(resource, include_related) + end + + complete_includes!(include_related, options) + end + end + + # Adds a Resource Fragment to the fragments hash + # + # @param fragment [JSONAPI::ResourceFragment] + # @param include_related [Hash] + # + # @return [null] + def add_resource_fragment(fragment, include_related) + fragment.primary = true + super(fragment, include_related) + end + + def complete_includes!(include_related, options) + # ToDo: can we skip if more than one resource_klass found? + resource_klasses = Set.new + @fragments.each_key { |identity| resource_klasses << identity.resource_klass } + + resource_klasses.each { |resource_klass| load_included(resource_klass, self, include_related, options)} + + self + end + end + + class RelatedResourceTree < ResourceTree + + attr_reader :parent_relationship, :source_resource_tree + + # Creates a RelatedResourceTree with no resources and no related ResourceTrees. A connection to the parent + # ResourceTree is maintained. + # + # @param parent_relationship [JSONAPI::Relationship] + # @param source_resource_tree [JSONAPI::ResourceTree] + # + # @return [JSONAPI::RelatedResourceTree] the new or existing resource id tree for the requested relationship + def initialize(parent_relationship, source_resource_tree) + @fragments ||= {} + @related_resource_trees ||= {} + + @parent_relationship = parent_relationship + @parent_relationship_name = parent_relationship.name.to_sym + @source_resource_tree = source_resource_tree + end + + # Adds a Resource Fragment to the fragments hash + # + # @param fragment [JSONAPI::ResourceFragment] + # @param include_related [Hash] + # + # @return [null] + def add_resource_fragment(fragment, include_related) + init_included_relationships(fragment, include_related) + + fragment.related_from.each do |rid| + @source_resource_tree.fragments[rid].add_related_identity(parent_relationship.name, fragment.identity) + end + + if @fragments[fragment.identity] + @fragments[fragment.identity].related_from.merge(fragment.related_from) + fragment.related.each_pair do |relationship_name, rids| + if rids + @fragments[fragment.identity].merge_related_identities(relationship_name, rids) + end + end + else + @fragments[fragment.identity] = fragment + end + end + end +end \ No newline at end of file diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index 3ecab8e83..af72da60e 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -24,6 +24,12 @@ def test_index assert json_response['data'].is_a?(Array) end + def test_index_includes + assert_cacheable_get :index, params: { include: 'author,comments' } + assert_response :success + assert json_response['data'].is_a?(Array) + end + def test_accept_header_missing @request.headers['Accept'] = nil diff --git a/test/fixtures/active_record.rb b/test/fixtures/active_record.rb index 227fff664..0489debe2 100644 --- a/test/fixtures/active_record.rb +++ b/test/fixtures/active_record.rb @@ -1597,11 +1597,16 @@ def find(filters, options = {}) end # Records - def find_fragments(filters, options = {}) + def find_fragments(filters, options) fragments = {} find_records(filters, options).each do |record| rid = JSONAPI::ResourceIdentity.new(resource_klass, record.id) - fragments[rid] = JSONAPI::ResourceFragment.new(rid) + # We can use either the id or the full resource. + # fragments[rid] = JSONAPI::ResourceFragment.new(rid) + # OR + # fragments[rid] = JSONAPI::ResourceFragment.new(rid, resource: resource_klass.new(record, options[:context])) + # In this case we will use the resource since we already looked up the model instance + fragments[rid] = JSONAPI::ResourceFragment.new(rid, resource: resource_klass.new(record, options[:context])) end fragments end diff --git a/test/helpers/configuration_helpers.rb b/test/helpers/configuration_helpers.rb index 5afe3296d..b3f14f443 100644 --- a/test/helpers/configuration_helpers.rb +++ b/test/helpers/configuration_helpers.rb @@ -29,7 +29,7 @@ def with_resource_caching(cache, classes = :all) with_jsonapi_config(new_config_options) do if classes == :all or (classes.is_a?(Hash) && classes.keys == [:except]) resource_classes = ObjectSpace.each_object(Class).select do |klass| - if klass < JSONAPI::Resource + if klass < JSONAPI::BasicResource # Not using Resource#_model_class to avoid tripping the warning early, which could # cause ResourceTest#test_nil_model_class to fail. model_class = klass._model_name.to_s.safe_constantize diff --git a/test/helpers/value_matchers.rb b/test/helpers/value_matchers.rb index c5bb2ab01..2908f8d4f 100644 --- a/test/helpers/value_matchers.rb +++ b/test/helpers/value_matchers.rb @@ -5,13 +5,21 @@ def matches_value?(v1, v2, options = {}) if v1 == :any # any value is acceptable elsif v1 == :not_nil - return false if v2 == nil + if v2 == nil + return false + end elsif v1.kind_of?(Hash) - return false unless matches_hash?(v1, v2, options) + unless matches_hash?(v1, v2, options) + return false + end elsif v1.kind_of?(Array) - return false unless matches_array?(v1, v2, options) + unless matches_array?(v1, v2, options) + return false + end else - return false unless v2 == v1 + unless v2 == v1 + return false + end end true end @@ -19,7 +27,9 @@ def matches_value?(v1, v2, options = {}) def matches_array?(array1, array2, options = {}) return false unless array1.kind_of?(Array) && array2.kind_of?(Array) if options[:exact] - return false unless array1.size == array2.size + unless array1.size == array2.size + return false + end end # order of items shouldn't matter: @@ -36,7 +46,9 @@ def matches_array?(array1, array2, options = {}) break end end - return false unless matched.has_key?(i.to_s) + unless matched.has_key?(i.to_s) + return false + end end true end @@ -45,14 +57,18 @@ def matches_array?(array1, array2, options = {}) def matches_hash?(hash1, hash2, options = {}) return false unless hash1.kind_of?(Hash) && hash2.kind_of?(Hash) if options[:exact] - return false unless hash1.size == hash2.size + unless hash1.size == hash2.size + return false + end end hash1 = hash1.deep_symbolize_keys hash2 = hash2.deep_symbolize_keys hash1.each do |k1, v1| - return false unless hash2.has_key?(k1) && matches_value?(v1, hash2[k1], options) + unless hash2.has_key?(k1) && matches_value?(v1, hash2[k1], options) + return false + end end true end diff --git a/test/unit/processor/default_processor_test.rb b/test/unit/processor/default_processor_test.rb index 1158f23d0..9ee55ba78 100644 --- a/test/unit/processor/default_processor_test.rb +++ b/test/unit/processor/default_processor_test.rb @@ -19,7 +19,7 @@ def setup # no includes filters = { id: [10, 12] } - find_options = { filters: filters } + options = { filters: filters } params = { filters: filters, include_directives: {}, @@ -29,12 +29,12 @@ def setup serializer: {} } p = JSONAPI::Processor.new(PostResource, :find, params) - $id_tree_no_includes = p.send(:find_resource_id_tree, PostResource, find_options, nil) + $id_tree_no_includes = p.send(:find_resource_tree, options, nil) $resource_set_no_includes = JSONAPI::ResourceSet.new($id_tree_no_includes) $populated_resource_set_no_includes = JSONAPI::ResourceSet.new($id_tree_no_includes).populate!($serializer, nil,{}) # has_one included - directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']).include_directives + directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']) params = { filters: filters, include_directives: directives, @@ -45,7 +45,7 @@ def setup } p = JSONAPI::Processor.new(PostResource, :find, params) - $id_tree_has_one_includes = p.send(:find_resource_id_tree, PostResource, find_options, directives[:include_related]) + $id_tree_has_one_includes = p.send(:find_resource_tree, options, directives[:include_related]) $resource_set_has_one_includes = JSONAPI::ResourceSet.new($id_tree_has_one_includes) $populated_resource_set_has_one_includes = JSONAPI::ResourceSet.new($id_tree_has_one_includes).populate!($serializer, nil,{}) end @@ -60,8 +60,8 @@ def after_teardown PersonResource.caching nil end - def test_id_tree_without_includes_should_be_a_resource_id_tree - assert $id_tree_no_includes.is_a?(JSONAPI::PrimaryResourceIdTree) + def test_id_tree_without_includes_should_be_a_resource_tree + assert $id_tree_no_includes.is_a?(JSONAPI::PrimaryResourceTree) end def test_id_tree_without_includes_should_have_resources @@ -69,7 +69,7 @@ def test_id_tree_without_includes_should_have_resources end def test_id_tree_without_includes_should_not_have_related_resources - assert_empty $id_tree_no_includes.related_resource_id_trees + assert_empty $id_tree_no_includes.related_resource_trees end def test_id_tree_without_includes_resource_relationships_should_be_empty @@ -77,14 +77,14 @@ def test_id_tree_without_includes_resource_relationships_should_be_empty assert_equal 0, $id_tree_no_includes.fragments[JSONAPI::ResourceIdentity.new(PostResource, 12)].related.length end - def test_id_tree_has_one_includes_should_be_a_resource_id_tree - assert $id_tree_has_one_includes.is_a?(JSONAPI::PrimaryResourceIdTree) + def test_id_tree_has_one_includes_should_be_a_resource_tree + assert $id_tree_has_one_includes.is_a?(JSONAPI::PrimaryResourceTree) end def test_id_tree_has_one_includes_should_have_included_resources - assert $id_tree_has_one_includes.related_resource_id_trees.is_a?(Hash) - assert $id_tree_has_one_includes.related_resource_id_trees[:author].is_a?(JSONAPI::RelatedResourceIdTree) - assert_equal 2, $id_tree_has_one_includes.related_resource_id_trees[:author].fragments.size + assert $id_tree_has_one_includes.related_resource_trees.is_a?(Hash) + assert $id_tree_has_one_includes.related_resource_trees[:author].is_a?(JSONAPI::RelatedResourceTree) + assert_equal 2, $id_tree_has_one_includes.related_resource_trees[:author].fragments.size end def test_id_tree_has_one_includes_should_have_resources @@ -110,5 +110,4 @@ def test_populated_resource_set_has_one_includes_relationships_are_resolved assert_equal 10, $populated_resource_set_has_one_includes.resource_klasses[PersonResource][1003][:relationships][:posts].first.id assert_equal 12, $populated_resource_set_has_one_includes.resource_klasses[PersonResource][1004][:relationships][:posts].first.id end - end \ No newline at end of file diff --git a/test/unit/resource/active_relation_resource_test.rb b/test/unit/resource/active_relation_resource_test.rb index 59f57fcda..858009c9b 100644 --- a/test/unit/resource/active_relation_resource_test.rb +++ b/test/unit/resource/active_relation_resource_test.rb @@ -1,6 +1,6 @@ require File.expand_path('../../../test_helper', __FILE__) -class ARPostResource < JSONAPI::Resource +class ArPostResource < JSONAPI::Resource model_name 'Post' attribute :headline, delegate: :title has_one :author @@ -13,22 +13,22 @@ def setup def test_find_fragments_no_attributes filters = {} - posts_identities = ARPostResource.find_fragments(filters) + posts_identities = ArPostResource.find_fragments(filters) assert_equal 20, posts_identities.length - assert_equal JSONAPI::ResourceIdentity.new(ARPostResource, 1), posts_identities.keys[0] - assert_equal JSONAPI::ResourceIdentity.new(ARPostResource, 1), posts_identities.values[0].identity + assert_equal JSONAPI::ResourceIdentity.new(ArPostResource, 1), posts_identities.keys[0] + assert_equal JSONAPI::ResourceIdentity.new(ArPostResource, 1), posts_identities.values[0].identity assert posts_identities.values[0].is_a?(JSONAPI::ResourceFragment) end def test_find_fragments_cache_field filters = {} options = { cache: true } - posts_identities = ARPostResource.find_fragments(filters, options) + posts_identities = ArPostResource.find_fragments(filters, options) assert_equal 20, posts_identities.length - assert_equal JSONAPI::ResourceIdentity.new(ARPostResource, 1), posts_identities.keys[0] - assert_equal JSONAPI::ResourceIdentity.new(ARPostResource, 1), posts_identities.values[0].identity + assert_equal JSONAPI::ResourceIdentity.new(ArPostResource, 1), posts_identities.keys[0] + assert_equal JSONAPI::ResourceIdentity.new(ArPostResource, 1), posts_identities.values[0].identity assert posts_identities.values[0].is_a?(JSONAPI::ResourceFragment) assert posts_identities.values[0].cache.is_a?(ActiveSupport::TimeWithZone) end @@ -36,11 +36,11 @@ def test_find_fragments_cache_field def test_find_fragments_cache_field_attributes filters = {} options = { attributes: [:headline, :author_id], cache: true } - posts_identities = ARPostResource.find_fragments(filters, options) + posts_identities = ArPostResource.find_fragments(filters, options) assert_equal 20, posts_identities.length - assert_equal JSONAPI::ResourceIdentity.new(ARPostResource, 1), posts_identities.keys[0] - assert_equal JSONAPI::ResourceIdentity.new(ARPostResource, 1), posts_identities.values[0].identity + assert_equal JSONAPI::ResourceIdentity.new(ArPostResource, 1), posts_identities.keys[0] + assert_equal JSONAPI::ResourceIdentity.new(ArPostResource, 1), posts_identities.values[0].identity assert posts_identities.values[0].is_a?(JSONAPI::ResourceFragment) assert_equal 2, posts_identities.values[0].attributes.length assert posts_identities.values[0].cache.is_a?(ActiveSupport::TimeWithZone) @@ -50,11 +50,12 @@ def test_find_fragments_cache_field_attributes def test_find_related_has_one_fragments_no_attributes options = {} - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 1), - JSONAPI::ResourceIdentity.new(ARPostResource, 2), - JSONAPI::ResourceIdentity.new(ARPostResource, 20)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 1), + JSONAPI::ResourceIdentity.new(ArPostResource, 2), + JSONAPI::ResourceIdentity.new(ArPostResource, 20)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = ARPostResource.find_included_fragments(source_rids, 'author', options) + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'author', options) assert_equal 2, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(AuthorResource, 1001), related_fragments.keys[0] @@ -65,11 +66,12 @@ def test_find_related_has_one_fragments_no_attributes def test_find_related_has_one_fragments_cache_field options = { cache: true } - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 1), - JSONAPI::ResourceIdentity.new(ARPostResource, 2), - JSONAPI::ResourceIdentity.new(ARPostResource, 20)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 1), + JSONAPI::ResourceIdentity.new(ArPostResource, 2), + JSONAPI::ResourceIdentity.new(ArPostResource, 20)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = ARPostResource.find_included_fragments(source_rids, 'author', options) + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'author', options) assert_equal 2, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(AuthorResource, 1001), related_fragments.keys[0] @@ -81,11 +83,12 @@ def test_find_related_has_one_fragments_cache_field def test_find_related_has_one_fragments_cache_field_attributes options = { cache: true, attributes: [:name] } - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 1), - JSONAPI::ResourceIdentity.new(ARPostResource, 2), - JSONAPI::ResourceIdentity.new(ARPostResource, 20)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 1), + JSONAPI::ResourceIdentity.new(ArPostResource, 2), + JSONAPI::ResourceIdentity.new(ArPostResource, 20)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = ARPostResource.find_included_fragments(source_rids, 'author', options) + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'author', options) assert_equal 2, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(AuthorResource, 1001), related_fragments.keys[0] @@ -99,12 +102,13 @@ def test_find_related_has_one_fragments_cache_field_attributes def test_find_related_has_many_fragments_no_attributes options = {} - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 1), - JSONAPI::ResourceIdentity.new(ARPostResource, 2), - JSONAPI::ResourceIdentity.new(ARPostResource, 12), - JSONAPI::ResourceIdentity.new(ARPostResource, 14)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 1), + JSONAPI::ResourceIdentity.new(ArPostResource, 2), + JSONAPI::ResourceIdentity.new(ArPostResource, 12), + JSONAPI::ResourceIdentity.new(ArPostResource, 14)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = ARPostResource.find_included_fragments(source_rids, 'tags', options) + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'tags', options) assert_equal 8, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(TagResource, 501), related_fragments.keys[0] @@ -117,9 +121,10 @@ def test_find_related_has_many_fragments_no_attributes def test_find_related_has_many_fragments_pagination params = ActionController::Parameters.new(number: 2, size: 4) options = { paginator: PagedPaginator.new(params) } - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 15)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 15)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = ARPostResource.find_included_fragments(source_rids, 'tags', options) + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'tags', options) assert_equal 1, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(TagResource, 516), related_fragments.keys[0] @@ -130,12 +135,13 @@ def test_find_related_has_many_fragments_pagination def test_find_related_has_many_fragments_cache_field options = { cache: true } - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 1), - JSONAPI::ResourceIdentity.new(ARPostResource, 2), - JSONAPI::ResourceIdentity.new(ARPostResource, 12), - JSONAPI::ResourceIdentity.new(ARPostResource, 14)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 1), + JSONAPI::ResourceIdentity.new(ArPostResource, 2), + JSONAPI::ResourceIdentity.new(ArPostResource, 12), + JSONAPI::ResourceIdentity.new(ArPostResource, 14)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = ARPostResource.find_included_fragments(source_rids, 'tags', options) + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'tags', options) assert_equal 8, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(TagResource, 501), related_fragments.keys[0] @@ -148,12 +154,13 @@ def test_find_related_has_many_fragments_cache_field def test_find_related_has_many_fragments_cache_field_attributes options = { cache: true, attributes: [:name] } - source_rids = [JSONAPI::ResourceIdentity.new(ARPostResource, 1), - JSONAPI::ResourceIdentity.new(ARPostResource, 2), - JSONAPI::ResourceIdentity.new(ARPostResource, 12), - JSONAPI::ResourceIdentity.new(ARPostResource, 14)] + source_rids = [JSONAPI::ResourceIdentity.new(ArPostResource, 1), + JSONAPI::ResourceIdentity.new(ArPostResource, 2), + JSONAPI::ResourceIdentity.new(ArPostResource, 12), + JSONAPI::ResourceIdentity.new(ArPostResource, 14)] - related_fragments = ARPostResource.find_included_fragments(source_rids, 'tags', options) + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } + related_fragments = ArPostResource.find_included_fragments(source_fragments, 'tags', options) assert_equal 8, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(TagResource, 501), related_fragments.keys[0] @@ -171,8 +178,9 @@ def test_find_related_polymorphic_fragments_no_attributes source_rids = [JSONAPI::ResourceIdentity.new(PictureResource, 1), JSONAPI::ResourceIdentity.new(PictureResource, 2), JSONAPI::ResourceIdentity.new(PictureResource, 3)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = PictureResource.find_included_fragments(source_rids, 'imageable', options) + related_fragments = PictureResource.find_included_fragments(source_fragments, 'imageable', options) assert_equal 2, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(ProductResource, 1), related_fragments.keys[0] @@ -189,8 +197,9 @@ def test_find_related_polymorphic_fragments_cache_field source_rids = [JSONAPI::ResourceIdentity.new(PictureResource, 1), JSONAPI::ResourceIdentity.new(PictureResource, 2), JSONAPI::ResourceIdentity.new(PictureResource, 3)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = PictureResource.find_included_fragments(source_rids, 'imageable', options) + related_fragments = PictureResource.find_included_fragments(source_fragments, 'imageable', options) assert_equal 2, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(ProductResource, 1), related_fragments.keys[0] @@ -208,8 +217,9 @@ def test_find_related_polymorphic_fragments_cache_field_attributes source_rids = [JSONAPI::ResourceIdentity.new(PictureResource, 1), JSONAPI::ResourceIdentity.new(PictureResource, 2), JSONAPI::ResourceIdentity.new(PictureResource, 3)] + source_fragments = source_rids.collect {|rid| JSONAPI::ResourceFragment.new(rid) } - related_fragments = PictureResource.find_included_fragments(source_rids, 'imageable', options) + related_fragments = PictureResource.find_included_fragments(source_fragments, 'imageable', options) assert_equal 2, related_fragments.length assert_equal JSONAPI::ResourceIdentity.new(ProductResource, 1), related_fragments.keys[0] diff --git a/test/unit/resource/resource_test.rb b/test/unit/resource/resource_test.rb index 0bb8d1aa7..7c6243a9e 100644 --- a/test/unit/resource/resource_test.rb +++ b/test/unit/resource/resource_test.rb @@ -268,16 +268,16 @@ def test_filter_on_has_one_relationship_id def test_to_many_relationship_filters post_resource = PostResource.new(Post.find(1), nil) - comments = PostResource.find_included_fragments([post_resource.identity], :comments, {}) + comments = PostResource.find_included_fragments([post_resource], :comments, {}) assert_equal(2, comments.size) - filtered_comments = PostResource.find_included_fragments([post_resource.identity], :comments, { filters: { body: 'i liked it' } }) + filtered_comments = PostResource.find_included_fragments([post_resource], :comments, { filters: { body: 'i liked it' } }) assert_equal(1, filtered_comments.size) end def test_to_many_relationship_sorts post_resource = PostResource.new(Post.find(1), nil) - comment_ids = post_resource.class.find_included_fragments([post_resource.identity], :comments, {}).keys.collect {|c| c.id } + comment_ids = post_resource.class.find_included_fragments([post_resource], :comments, {}).keys.collect {|c| c.id } assert_equal [1,2], comment_ids # define apply_filters method on post resource to sort descending @@ -291,7 +291,7 @@ def apply_sort(records, _order_options, options) end sorted_comment_ids = post_resource.class.find_included_fragments( - [post_resource.identity], + [post_resource], :comments, { sort_criteria: [{ field: 'id', direction: :desc }] }).keys.collect {|c| c.id} diff --git a/test/unit/serializer/include_directives_test.rb b/test/unit/serializer/include_directives_test.rb index ad6e6710d..552d13b1b 100644 --- a/test/unit/serializer/include_directives_test.rb +++ b/test/unit/serializer/include_directives_test.rb @@ -4,7 +4,7 @@ class IncludeDirectivesTest < ActiveSupport::TestCase def test_one_level_one_include - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts']).instance_variable_get(:@include_directives_hash) assert_hash_equals( { @@ -18,7 +18,7 @@ def test_one_level_one_include end def test_one_level_multiple_includes - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'comments', 'expense_entries']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'comments', 'expense_entries']).instance_variable_get(:@include_directives_hash) assert_hash_equals( { @@ -38,7 +38,7 @@ def test_one_level_multiple_includes end def test_multiple_level_multiple_includes - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'posts.comments', 'comments', 'expense_entries']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'posts.comments', 'comments', 'expense_entries']).instance_variable_get(:@include_directives_hash) assert_hash_equals( { @@ -63,7 +63,7 @@ def test_multiple_level_multiple_includes def test_two_levels_include_full_path - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments']).instance_variable_get(:@include_directives_hash) assert_hash_equals( { @@ -81,7 +81,7 @@ def test_two_levels_include_full_path end def test_two_levels_include_full_path_redundant - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'posts.comments']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts', 'posts.comments']).instance_variable_get(:@include_directives_hash) assert_hash_equals( { @@ -99,7 +99,7 @@ def test_two_levels_include_full_path_redundant end def test_three_levels_include_full - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments.tags']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['posts.comments.tags']).instance_variable_get(:@include_directives_hash) assert_hash_equals( { @@ -127,19 +127,19 @@ def test_three_levels_include_full # def test_invalid_includes_1 assert_raises JSONAPI::Exceptions::InvalidInclude do - JSONAPI::IncludeDirectives.new(PersonResource, ['../../../../']).include_directives + JSONAPI::IncludeDirectives.new(PersonResource, ['../../../../']).instance_variable_get(:@include_directives_hash) end end def test_invalid_includes_2 assert_raises JSONAPI::Exceptions::InvalidInclude do - JSONAPI::IncludeDirectives.new(PersonResource, ['posts./sdaa./........']).include_directives + JSONAPI::IncludeDirectives.new(PersonResource, ['posts./sdaa./........']).instance_variable_get(:@include_directives_hash) end end def test_invalid_includes_3 assert_raises JSONAPI::Exceptions::InvalidInclude do - JSONAPI::IncludeDirectives.new(PersonResource, ['invalid../../../../']).include_directives + JSONAPI::IncludeDirectives.new(PersonResource, ['invalid../../../../']).instance_variable_get(:@include_directives_hash) end end end diff --git a/test/unit/serializer/serializer_test.rb b/test/unit/serializer/serializer_test.rb index b3cda28e8..33455b0f1 100644 --- a/test/unit/serializer/serializer_test.rb +++ b/test/unit/serializer/serializer_test.rb @@ -21,9 +21,9 @@ def after_teardown def test_serializer post_1_identity = JSONAPI::ResourceIdentity.new(PostResource, 1) - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['']) id_tree.add_resource_fragment(JSONAPI::ResourceFragment.new(post_1_identity), directives[:include_related]) resource_set = JSONAPI::ResourceSet.new(id_tree) @@ -81,8 +81,64 @@ def test_serializer ) end + def test_serialize_source_to_hash + post = posts(:post_1) + post_resource = PostResource.new(post, {}) + + serializer = JSONAPI::ResourceSerializer.new( + PostResource, + base_url: 'http://example.com', + url_helpers: TestApp.routes.url_helpers) + + serialized = serializer.serialize_to_hash(post_resource) + + assert_hash_equals( + { + data: { + type: 'posts', + id: '1', + links: { + self: 'http://example.com/posts/1', + }, + attributes: { + title: 'New post', + body: 'A body!!!', + subject: 'New post' + }, + relationships: { + section: { + links: { + self: 'http://example.com/posts/1/relationships/section', + related: 'http://example.com/posts/1/section' + } + }, + author: { + links: { + self: 'http://example.com/posts/1/relationships/author', + related: 'http://example.com/posts/1/author' + } + }, + tags: { + links: { + self: 'http://example.com/posts/1/relationships/tags', + related: 'http://example.com/posts/1/tags' + } + }, + comments: { + links: { + self: 'http://example.com/posts/1/relationships/comments', + related: 'http://example.com/posts/1/comments' + } + } + } + } + }, + serialized + ) + end + def test_serializer_nil_handling - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new resource_set = JSONAPI::ResourceSet.new(id_tree) @@ -104,9 +160,9 @@ def test_serializer_nil_handling def test_serializer_namespaced_resource_with_custom_resource_links post_1_identity = JSONAPI::ResourceIdentity.new(Api::V1::PostResource, 1) - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new - directives = JSONAPI::IncludeDirectives.new(PersonResource, ['']).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, ['']) id_tree.add_resource_fragment(JSONAPI::ResourceFragment.new(post_1_identity), directives[:include_related]) resource_set = JSONAPI::ResourceSet.new(id_tree) @@ -161,9 +217,9 @@ def test_serializer_namespaced_resource_with_custom_resource_links def test_serializer_limited_fieldset post_1_identity = JSONAPI::ResourceIdentity.new(PostResource, 1) - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new - directives = JSONAPI::IncludeDirectives.new(PersonResource, []).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, []) id_tree.add_resource_fragment(JSONAPI::ResourceFragment.new(post_1_identity), directives[:include_related]) resource_set = JSONAPI::ResourceSet.new(id_tree) @@ -204,18 +260,12 @@ def test_serializer_limited_fieldset def test_serializer_include post_1_identity = JSONAPI::ResourceIdentity.new(PostResource, 1) person_1001_identity = JSONAPI::ResourceIdentity.new(PersonResource, 1001) - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new - directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']).include_directives + directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']) id_tree.add_resource_fragment(JSONAPI::ResourceFragment.new(post_1_identity), directives[:include_related]) - - rel_id_tree = id_tree.fetch_related_resource_id_tree(PostResource._relationships[:author]) - - author_fragment = JSONAPI::ResourceFragment.new(person_1001_identity) - author_fragment.add_related_from(post_1_identity) - author_fragment.add_related_identity(:posts, post_1_identity) - rel_id_tree.add_resource_fragment(author_fragment, directives[:include_related][:author][:include_related]) + id_tree.complete_includes!(directives[:include_related], {}) resource_set = JSONAPI::ResourceSet.new(id_tree) @@ -332,21 +382,297 @@ def test_serializer_include ) end + def test_serializer_source_to_hash_include + post = posts(:post_1) + post_resource = PostResource.new(post, {}) + + serializer = JSONAPI::ResourceSerializer.new( + PostResource, + url_helpers: TestApp.routes.url_helpers, + include_directives: JSONAPI::IncludeDirectives.new(PostResource, ['author'])) + + serialized = serializer.serialize_to_hash(post_resource) + + assert_hash_equals( + { + data: { + type: 'posts', + id: '1', + links: { + self: '/posts/1' + }, + attributes: { + title: 'New post', + body: 'A body!!!', + subject: 'New post' + }, + relationships: { + section: { + links: { + self: '/posts/1/relationships/section', + related: '/posts/1/section' + } + }, + author: { + links: { + self: '/posts/1/relationships/author', + related: '/posts/1/author' + }, + data: { + type: 'people', + id: '1001' + } + }, + tags: { + links: { + self: '/posts/1/relationships/tags', + related: '/posts/1/tags' + } + }, + comments: { + links: { + self: '/posts/1/relationships/comments', + related: '/posts/1/comments' + } + } + } + }, + included: [ + { + type: 'people', + id: '1001', + attributes: { + name: 'Joe Author', + email: 'joe@xyz.fake', + dateJoined: '2013-08-07 16:25:00 -0400' + }, + links: { + self: '/people/1001' + }, + relationships: { + comments: { + links: { + self: '/people/1001/relationships/comments', + related: '/people/1001/comments' + } + }, + posts: { + links: { + self: '/people/1001/relationships/posts', + related: '/people/1001/posts' + }, + data: [ + { + type: 'posts', + id: '1' + } + ] + }, + preferences: { + links: { + self: '/people/1001/relationships/preferences', + related: '/people/1001/preferences' + } + }, + hairCut: { + links: { + self: '/people/1001/relationships/hairCut', + related: '/people/1001/hairCut' + } + }, + vehicles: { + links: { + self: '/people/1001/relationships/vehicles', + related: '/people/1001/vehicles' + } + }, + expenseEntries: { + links: { + self: '/people/1001/relationships/expenseEntries', + related: '/people/1001/expenseEntries' + } + } + } + } + ] + }, + serialized + ) + end + + def test_serializer_source_array_to_hash_include + post_resources = [PostResource.new(posts(:post_1), {}), PostResource.new(posts(:post_2), {})] + + serializer = JSONAPI::ResourceSerializer.new( + PostResource, + url_helpers: TestApp.routes.url_helpers, + include_directives: JSONAPI::IncludeDirectives.new(PostResource, ['author'])) + + serialized = serializer.serialize_to_hash(post_resources) + + assert_hash_equals( + { + data: [ + { + type: 'posts', + id: '1', + links: { + self: '/posts/1' + }, + attributes: { + title: 'New post', + body: 'A body!!!', + subject: 'New post' + }, + relationships: { + section: { + links: { + self: '/posts/1/relationships/section', + related: '/posts/1/section' + } + }, + author: { + links: { + self: '/posts/1/relationships/author', + related: '/posts/1/author' + }, + data: { + type: 'people', + id: '1001' + } + }, + tags: { + links: { + self: '/posts/1/relationships/tags', + related: '/posts/1/tags' + } + }, + comments: { + links: { + self: '/posts/1/relationships/comments', + related: '/posts/1/comments' + } + } + } + }, + { + type: 'posts', + id: '2', + links: { + self: '/posts/2' + }, + attributes: { + title: 'JR Solves your serialization woes!', + body: 'Use JR', + subject: 'JR Solves your serialization woes!' + }, + relationships: { + section: { + links: { + self: '/posts/2/relationships/section', + related: '/posts/2/section' + } + }, + author: { + links: { + self: '/posts/2/relationships/author', + related: '/posts/2/author' + }, + data: { + type: 'people', + id: '1001' + } + }, + tags: { + links: { + self: '/posts/2/relationships/tags', + related: '/posts/2/tags' + } + }, + comments: { + links: { + self: '/posts/2/relationships/comments', + related: '/posts/2/comments' + } + } + } + } + ], + included: [ + { + type: 'people', + id: '1001', + attributes: { + name: 'Joe Author', + email: 'joe@xyz.fake', + dateJoined: '2013-08-07 16:25:00 -0400' + }, + links: { + self: '/people/1001' + }, + relationships: { + comments: { + links: { + self: '/people/1001/relationships/comments', + related: '/people/1001/comments' + } + }, + posts: { + links: { + self: '/people/1001/relationships/posts', + related: '/people/1001/posts' + }, + data: [ + { + type: 'posts', + id: '1' + }, + { + type: 'posts', + id: '2' + } + ] + }, + preferences: { + links: { + self: '/people/1001/relationships/preferences', + related: '/people/1001/preferences' + } + }, + hairCut: { + links: { + self: '/people/1001/relationships/hairCut', + related: '/people/1001/hairCut' + } + }, + vehicles: { + links: { + self: '/people/1001/relationships/vehicles', + related: '/people/1001/vehicles' + } + }, + expenseEntries: { + links: { + self: '/people/1001/relationships/expenseEntries', + related: '/people/1001/expenseEntries' + } + } + } + } + ] + }, + serialized + ) + end + def test_serializer_key_format post_1_identity = JSONAPI::ResourceIdentity.new(PostResource, 1) - person_1001_identity = JSONAPI::ResourceIdentity.new(PersonResource, 1001) - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new - directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']).include_directives + directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']) id_tree.add_resource_fragment(JSONAPI::ResourceFragment.new(post_1_identity), directives[:include_related]) - - rel_id_tree = id_tree.fetch_related_resource_id_tree(PostResource._relationships[:author]) - - author_fragment = JSONAPI::ResourceFragment.new(person_1001_identity) - author_fragment.add_related_from(post_1_identity) - author_fragment.add_related_identity(:posts, post_1_identity) - rel_id_tree.add_resource_fragment(author_fragment, directives[:include_related][:author][:include_related]) + id_tree.complete_includes!(directives[:include_related], {}) resource_set = JSONAPI::ResourceSet.new(id_tree) @@ -469,9 +795,9 @@ def test_serializers_linkage_even_without_included_resource post_1_identity = JSONAPI::ResourceIdentity.new(PostResource, 1) person_1001_identity = JSONAPI::ResourceIdentity.new(PersonResource, 1001) - id_tree = JSONAPI::PrimaryResourceIdTree.new + id_tree = JSONAPI::PrimaryResourceTree.new - directives = JSONAPI::IncludeDirectives.new(PersonResource, []).include_directives + directives = JSONAPI::IncludeDirectives.new(PersonResource, []) fragment = JSONAPI::ResourceFragment.new(post_1_identity) @@ -539,4 +865,122 @@ def test_serializers_linkage_even_without_included_resource serialized ) end + + def test_serializer_include_from_resource + serializer = JSONAPI::ResourceSerializer.new(PostResource, url_helpers: TestApp.routes.url_helpers) + + directives = JSONAPI::IncludeDirectives.new(PostResource, ['author']) + + resource_set = JSONAPI::ResourceSet.new(PostResource.find_by_key(1), directives[:include_related], {}) + resource_set.populate!(serializer, {}, {}) + + serialized = serializer.serialize_resource_set_to_hash_single(resource_set) + + assert_hash_equals( + { + data: { + type: 'posts', + id: '1', + links: { + self: '/posts/1' + }, + attributes: { + title: 'New post', + body: 'A body!!!', + subject: 'New post' + }, + relationships: { + section: { + links: { + self: '/posts/1/relationships/section', + related: '/posts/1/section' + } + }, + author: { + links: { + self: '/posts/1/relationships/author', + related: '/posts/1/author' + }, + data: { + type: 'people', + id: '1001' + } + }, + tags: { + links: { + self: '/posts/1/relationships/tags', + related: '/posts/1/tags' + } + }, + comments: { + links: { + self: '/posts/1/relationships/comments', + related: '/posts/1/comments' + } + } + } + }, + included: [ + { + type: 'people', + id: '1001', + attributes: { + name: 'Joe Author', + email: 'joe@xyz.fake', + dateJoined: '2013-08-07 16:25:00 -0400' + }, + links: { + self: '/people/1001' + }, + relationships: { + comments: { + links: { + self: '/people/1001/relationships/comments', + related: '/people/1001/comments' + } + }, + posts: { + links: { + self: '/people/1001/relationships/posts', + related: '/people/1001/posts' + }, + data: [ + { + type: 'posts', + id: '1' + } + ] + }, + preferences: { + links: { + self: '/people/1001/relationships/preferences', + related: '/people/1001/preferences' + } + }, + hairCut: { + links: { + self: '/people/1001/relationships/hairCut', + related: '/people/1001/hairCut' + } + }, + vehicles: { + links: { + self: '/people/1001/relationships/vehicles', + related: '/people/1001/vehicles' + } + }, + expenseEntries: { + links: { + self: '/people/1001/relationships/expenseEntries', + related: '/people/1001/expenseEntries' + } + } + } + } + ] + }, + serialized + ) + end + end