Skip to content

Commit

Permalink
feat(serializer): serialize hash instead of an active record object (#22
Browse files Browse the repository at this point in the history
)
  • Loading branch information
nicolasalexandre9 authored Jan 19, 2024
1 parent c2b3120 commit 70dea37
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def handle_request(args = {})
name: args[:params]['collection_name'],
content: JSONAPI::Serializer.serialize(
records,
class_name: @collection.name,
is_collection: true,
serializer: Serializer::ForestSerializer,
include: projection.relations.keys,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def handle_request(args = {})
content: JSONAPI::Serializer.serialize(
records,
is_collection: true,
class_name: @child_collection.name,
serializer: Serializer::ForestSerializer,
include: projection.relations.keys
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def handle_request(args = {})
name: args[:params]['collection_name'],
content: JSONAPI::Serializer.serialize(
records[0],
class_name: @collection.name,
is_collection: false,
serializer: Serializer::ForestSerializer,
include: projection.relations.keys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def handle_request(args = {})
content: JSONAPI::Serializer.serialize(
record,
is_collection: false,
class_name: @collection.name,
serializer: Serializer::ForestSerializer
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def handle_request(args = {})
content: JSONAPI::Serializer.serialize(
records[0],
is_collection: false,
class_name: @collection.name,
serializer: Serializer::ForestSerializer
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@ def base_url
end

def type
class_name = object.class.name
@@class_names[class_name] ||= class_name.demodulize.underscore.freeze
class_name = @options[:class_name]
@@class_names[class_name] ||= class_name
end

def id
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(@options[:class_name])
primary_keys = ForestAdminDatasourceToolkit::Utils::Schema.primary_keys(forest_collection)
id = []
primary_keys.each { |key| id << @object[key] }

id.join('|')
end

def format_name(attribute_name)
Expand All @@ -41,7 +50,7 @@ def format_field(name, options)
end

def attributes
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(object.class.name.demodulize.underscore)
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(@options[:class_name])
fields = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'Column' }
fields.each { |field_name, _field| add_attribute(field_name) }
return {} if attributes_map.nil?
Expand All @@ -61,11 +70,7 @@ def evaluate_attr_or_block(attribute_name, attr_or_block)
instance_eval(&attr_or_block)
else
# Default behavior, call a method by the name of the attribute.
begin
object.try(attr_or_block)
rescue
nil
end
object[attr_or_block]
end
end

Expand Down Expand Up @@ -104,7 +109,8 @@ def add_to_many_association(name, options = {}, &block)
end

def relationships
forest_collection = ForestAdminAgent::Facades::Container.datasource.get_collection(object.class.name.demodulize.underscore)
datasource = ForestAdminAgent::Facades::Container.datasource
forest_collection = datasource.get_collection(@options[:class_name])
relations_to_many = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'OneToMany' || field.type == 'ManyToMany' }
relations_to_one = forest_collection.schema[:fields].select { |_field_name, field| field.type == 'OneToOne' || field.type == 'ManyToOne' }

Expand All @@ -124,10 +130,13 @@ def relationships
end

object = has_one_relationship(attribute_name, attr_data)
if object.nil?
if object.nil? || object.empty?
data[formatted_attribute_name]['data'] = nil
else
related_object_serializer = ForestSerializer.new(object, @options)
relation = datasource.get_collection(@options[:class_name]).schema[:fields][attribute_name.to_s]
options = @options.clone
options[:class_name] = datasource.get_collection(relation.foreign_collection).name
related_object_serializer = ForestSerializer.new(object, options)
data[formatted_attribute_name]['data'] = {
'type' => related_object_serializer.type.to_s,
'id' => related_object_serializer.id.to_s,
Expand All @@ -152,6 +161,9 @@ def relationships
if @_include_linkages.include?(formatted_attribute_name) || attr_data[:options][:include_data]
data[formatted_attribute_name]['data'] = []
objects = has_many_relationship(attribute_name, attr_data) || []
relation = datasource.get_collection(@options[:class_name]).schema[:fields][attribute_name.to_s]
options = @options.clone
options[:class_name] = datasource.get_collection(relation.foreign_collection).name
objects.each do |obj|
related_object_serializer = JSONAPI::Serializer.find_serializer(obj, @options)
data[formatted_attribute_name]['data'] << {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def self.find_recursive_relationships(root_object, root_inclusion_tree, results,
end

# We're finding relationships for compound documents, so skip anything that doesn't exist.
next if object.nil?
next if object.nil? || object.empty?

# Full linkage: a request for comments.author MUST automatically include comments
# in the response.
Expand All @@ -58,6 +58,8 @@ def self.find_recursive_relationships(root_object, root_inclusion_tree, results,
# If it is not set, that indicates that this is an inner path and not a leaf and will
# be followed by the recursion below.
objects.each do |obj|
relation = ForestAdminAgent::Facades::Container.datasource.get_collection(options[:class_name]).schema[:fields][attribute_name]
relation_class_name = ForestAdminAgent::Facades::Container.datasource.get_collection(relation.foreign_collection).name
obj_serializer = JSONAPI::Serializer.find_serializer(obj, options)
# Use keys of ['posts', '1'] for the results to enforce uniqueness.
# Spec: A compound document MUST NOT include more than one resource object for each
Expand All @@ -82,7 +84,8 @@ def self.find_recursive_relationships(root_object, root_inclusion_tree, results,
# so merge the include_linkages each time we see it to load all the relevant linkages.
current_child_includes += (results[key] && results[key][:include_linkages]) || []
current_child_includes.uniq!
results[key] = { object: obj, include_linkages: current_child_includes }

results[key] = { object: obj, include_linkages: current_child_includes, class_name: relation_class_name }
end
end

Expand All @@ -96,6 +99,123 @@ def self.find_recursive_relationships(root_object, root_inclusion_tree, results,
end
nil
end

def self.serialize(objects, options = {})
# Normalize option strings to symbols.
options[:is_collection] = options.delete('is_collection') || options[:is_collection] || false
options[:include] = options.delete('include') || options[:include]
options[:serializer] = options.delete('serializer') || options[:serializer]
options[:namespace] = options.delete('namespace') || options[:namespace]
options[:context] = options.delete('context') || options[:context] || {}
options[:skip_collection_check] = options.delete('skip_collection_check') || options[:skip_collection_check] || false
options[:base_url] = options.delete('base_url') || options[:base_url]
options[:jsonapi] = options.delete('jsonapi') || options[:jsonapi]
options[:meta] = options.delete('meta') || options[:meta]
options[:links] = options.delete('links') || options[:links]
options[:fields] = options.delete('fields') || options[:fields] || {}

# Deprecated: use serialize_errors method instead
options[:errors] = options.delete('errors') || options[:errors]

# Normalize includes.
includes = options[:include]
includes = (includes.is_a?(String) ? includes.split(',') : includes).uniq if includes

# Transforms input so that the comma-separated fields are separate symbols in array
# and keys are stringified
# Example:
# {posts: 'title,author,long_comments'} => {'posts' => [:title, :author, :long_comments]}
# {posts: ['title', 'author', 'long_comments'} => {'posts' => [:title, :author, :long_comments]}
#
fields = {}
# Normalize fields to accept a comma-separated string or an array of strings.
options[:fields].map do |type, whitelisted_fields|
whitelisted_fields = [whitelisted_fields] if whitelisted_fields.is_a?(Symbol)
whitelisted_fields = whitelisted_fields.split(',') if whitelisted_fields.is_a?(String)
fields[type.to_s] = whitelisted_fields.map(&:to_sym)
end

# An internal-only structure that is passed through serializers as they are created.
passthrough_options = {
context: options[:context],
serializer: options[:serializer],
namespace: options[:namespace],
include: includes,
fields: fields,
base_url: options[:base_url],
class_name: options[:class_name]
}

if !options[:skip_collection_check] && options[:is_collection] && !objects.respond_to?(:each)
raise JSONAPI::Serializer::AmbiguousCollectionError.new(
'Attempted to serialize a single object as a collection.')
end

# Automatically include linkage data for any relation that is also included.
if includes
include_linkages = includes.map { |key| key.to_s.split('.').first }
passthrough_options[:include_linkages] = include_linkages
end

# Spec: Primary data MUST be either:
# - a single resource object or null, for requests that target single resources.
# - an array of resource objects or an empty array ([]), for resource collections.
# http://jsonapi.org/format/#document-structure-top-level
if options[:is_collection] && !objects.any?
primary_data = []
elsif !options[:is_collection] && objects.nil?
primary_data = nil
elsif options[:is_collection]
# Have object collection.
primary_data = serialize_primary_multi(objects, passthrough_options)
else
# Duck-typing check for a collection being passed without is_collection true.
# We always must be told if serializing a collection because the JSON:API spec distinguishes
# how to serialize null single resources vs. empty collections.
if !options[:skip_collection_check] && objects.is_a?(Array)
raise JSONAPI::Serializer::AmbiguousCollectionError.new(
'Must provide `is_collection: true` to `serialize` when serializing collections.')
end
# Have single object.
primary_data = serialize_primary(objects, passthrough_options)
end
result = {
'data' => primary_data,
}
result['jsonapi'] = options[:jsonapi] if options[:jsonapi]
result['meta'] = options[:meta] if options[:meta]
result['links'] = options[:links] if options[:links]
result['errors'] = options[:errors] if options[:errors]

# If 'include' relationships are given, recursively find and include each object.
if includes
relationship_data = {}
inclusion_tree = parse_relationship_paths(includes)

# Given all the primary objects (either the single root object or collection of objects),
# recursively search and find related associations that were specified as includes.
objects = options[:is_collection] ? objects.to_a : [objects]
objects.compact.each do |obj|
# Use the mutability of relationship_data as the return datastructure to take advantage
# of the internal special merging logic.
find_recursive_relationships(obj, inclusion_tree, relationship_data, passthrough_options)
end

result['included'] = relationship_data.map do |_, data|
included_passthrough_options = {}
included_passthrough_options[:base_url] = passthrough_options[:base_url]
included_passthrough_options[:context] = passthrough_options[:context]
included_passthrough_options[:fields] = passthrough_options[:fields]
included_passthrough_options[:serializer] = find_serializer_class(data[:object], options)
included_passthrough_options[:namespace] = passthrough_options[:namespace]
included_passthrough_options[:include_linkages] = data[:include_linkages]
included_passthrough_options[:class_name] = data[:class_name]

serialize_primary(data[:object], included_passthrough_options)
end
end
result
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,6 @@ def respond_to?(arg)

describe 'with relation' do
before do
person_class = Struct.new(:id, :name) do
def respond_to?(arg)
return false if arg == :each

super(arg)
end
end

passport_class = Struct.new(:id, :person_id) do
def respond_to?(arg)
return false if arg == :each

super(arg)
end
end
stub_const('Person', person_class)
stub_const('Passport', passport_class)

@datasource = Datasource.new
collection_person = instance_double(
Collection,
Expand Down Expand Up @@ -177,8 +159,12 @@ def respond_to?(arg)
type: 'persons'
}
args[:params]['collection_name'] = 'person'
allow(@datasource.get_collection('person')).to receive(:create).and_return(Person.new(1, 'john'))
allow(@datasource.get_collection('passport')).to receive(:update).and_return(Passport.new(1, 1))
allow(@datasource.get_collection('person')).to receive(:create).and_return(
{ 'id' => 1, 'name' => 'john' }
)
allow(@datasource.get_collection('passport')).to receive(:update).and_return(
{ 'id' => 1, 'person_id' => 1 }
)

result = store.handle_request(args)
expect(@datasource.get_collection('person')).to have_received(:create) do |caller, data|
Expand Down Expand Up @@ -217,7 +203,9 @@ def respond_to?(arg)
type: 'persons'
}
args[:params]['collection_name'] = 'passport'
allow(@datasource.get_collection('passport')).to receive(:create).and_return(Passport.new(1, 1))
allow(@datasource.get_collection('passport')).to receive(:create).and_return(
{ 'id' => 1, 'person_id' => 1 }
)

result = store.handle_request(args)
expect(@datasource.get_collection('passport')).to have_received(:create) do |caller, data|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ def initialize(datasource, model)
def list(_caller, filter, projection)
query = Utils::Query.new(self, projection, filter).build

query.all
query.all.map { |record| Utils::ActiveRecordSerializer.new(record).to_hash }
end

def aggregate(_caller, filter, aggregation, limit = nil)
Utils::QueryAggregate.new(self, aggregation, filter, limit).get
end

def create(_caller, data)
@model.create(data)
Utils::ActiveRecordSerializer.new(@model.create(data)).to_hash
end

def update(_caller, filter, data)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module ForestAdminDatasourceActiveRecord
module Utils
ActiveRecordSerializer = Struct.new(:object) do
def to_hash
hash_object(object)
end

def hash_object(object, with_associations: true)
hash = {}

return {} if object.nil?

hash.merge! object.attributes

if with_associations
each_association_collection(object) do |association_name, item|
hash[association_name] = hash_object(item, with_associations: false)
end
end

hash
end

def each_association_collection(object)
one_associations = %i[has_one belongs_to]
object.class.reflect_on_all_associations.filter { |a| one_associations.include?(a.macro) }
.each { |association| yield(association.name.to_s, object.send(association.name.to_s)) }
end
end
end
end

0 comments on commit 70dea37

Please sign in to comment.