lib/jsonapi/resource_tree.rb
# frozen_string_literal: true
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