mirror of
https://github.com/ditkrg/jsonapi-deserializable.git
synced 2026-01-22 13:56:50 +00:00
Implements deserialization with included and allows specifying custom deserializers for relationships
This commit is contained in:
parent
56a1d14225
commit
887c506fd2
@ -8,6 +8,7 @@ module JSONAPI
|
|||||||
class << self
|
class << self
|
||||||
attr_accessor :type_block, :id_block, :attr_blocks,
|
attr_accessor :type_block, :id_block, :attr_blocks,
|
||||||
:has_one_rel_blocks, :has_many_rel_blocks,
|
:has_one_rel_blocks, :has_many_rel_blocks,
|
||||||
|
:has_one_rel_options, :has_many_rel_options,
|
||||||
:default_attr_block, :default_has_one_rel_block,
|
:default_attr_block, :default_has_one_rel_block,
|
||||||
:default_has_many_rel_block,
|
:default_has_many_rel_block,
|
||||||
:key_formatter
|
:key_formatter
|
||||||
@ -16,6 +17,10 @@ module JSONAPI
|
|||||||
self.attr_blocks = {}
|
self.attr_blocks = {}
|
||||||
self.has_one_rel_blocks = {}
|
self.has_one_rel_blocks = {}
|
||||||
self.has_many_rel_blocks = {}
|
self.has_many_rel_blocks = {}
|
||||||
|
|
||||||
|
self.has_one_rel_options = {}
|
||||||
|
self.has_many_rel_options = {}
|
||||||
|
|
||||||
self.key_formatter = proc { |k| k }
|
self.key_formatter = proc { |k| k }
|
||||||
|
|
||||||
def self.inherited(klass)
|
def self.inherited(klass)
|
||||||
@ -25,6 +30,10 @@ module JSONAPI
|
|||||||
klass.attr_blocks = attr_blocks.dup
|
klass.attr_blocks = attr_blocks.dup
|
||||||
klass.has_one_rel_blocks = has_one_rel_blocks.dup
|
klass.has_one_rel_blocks = has_one_rel_blocks.dup
|
||||||
klass.has_many_rel_blocks = has_many_rel_blocks.dup
|
klass.has_many_rel_blocks = has_many_rel_blocks.dup
|
||||||
|
|
||||||
|
klass.has_one_rel_options = has_one_rel_options.dup
|
||||||
|
klass.has_many_rel_options = has_many_rel_options.dup
|
||||||
|
|
||||||
klass.default_attr_block = default_attr_block
|
klass.default_attr_block = default_attr_block
|
||||||
klass.default_has_one_rel_block = default_has_one_rel_block
|
klass.default_has_one_rel_block = default_has_one_rel_block
|
||||||
klass.default_has_many_rel_block = default_has_many_rel_block
|
klass.default_has_many_rel_block = default_has_many_rel_block
|
||||||
@ -36,12 +45,17 @@ module JSONAPI
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize(payload, root: '/data')
|
def initialize(payload, root: '/data')
|
||||||
@data = payload || {}
|
@data = ((payload || {}).key?('data') ? payload['data'] : payload) || {}
|
||||||
|
|
||||||
@root = root
|
@root = root
|
||||||
@type = @data['type']
|
@type = @data['type']
|
||||||
@id = @data['id']
|
@id = @data['id']
|
||||||
@attributes = @data['attributes'] || {}
|
@attributes = @data['attributes'] || {}
|
||||||
@relationships = @data['relationships'] || {}
|
@relationships = @data['relationships'] || {}
|
||||||
|
|
||||||
|
# Objectifies each included object
|
||||||
|
@included = initialize_included(payload.key?('included') ? payload['included'] : [])
|
||||||
|
|
||||||
deserialize!
|
deserialize!
|
||||||
|
|
||||||
freeze
|
freeze
|
||||||
@ -52,16 +66,60 @@ module JSONAPI
|
|||||||
end
|
end
|
||||||
alias to_h to_hash
|
alias to_h to_hash
|
||||||
|
|
||||||
attr_reader :reverse_mapping
|
attr_reader :reverse_mapping, :key_to_type_mapping
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def initialize_included(included)
|
||||||
|
return nil unless included.present?
|
||||||
|
|
||||||
|
# For each included, create an object of the correct type
|
||||||
|
included.map do |data|
|
||||||
|
|
||||||
|
# Find the key of type
|
||||||
|
key = key_to_type_mapping_inverted[data['type']&.to_s&.to_sym]
|
||||||
|
|
||||||
|
# Finds the deserializer
|
||||||
|
deserializer = merged_rel_options&.[](key)&.[](:deserializer)
|
||||||
|
|
||||||
|
# If the deserializer is not available, uses the current class to create the object
|
||||||
|
if deserializer.blank?
|
||||||
|
# Important to wrap this around this hash. This will be crucial for use in method `find_in_included/2` defined in the same class.
|
||||||
|
# If the deserializer is created using the current class, we will need to pluck all its attributes
|
||||||
|
{ has_deserializer: false, object: self.class.new({ 'data' => data }) }
|
||||||
|
else
|
||||||
|
|
||||||
|
# If the deserializer is created using a given class, we will need to call .to_h on it instead of plucking all its attributes
|
||||||
|
{ has_deserializer: true, object: deserializer.new({ 'data' => data }) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def included_types
|
||||||
|
return [] unless @included.present?
|
||||||
|
@included.map { |doc| doc.instance_variable_get(:@type) }.uniq
|
||||||
|
end
|
||||||
|
|
||||||
def register_mappings(keys, path)
|
def register_mappings(keys, path)
|
||||||
keys.each do |k|
|
keys.each do |k|
|
||||||
@reverse_mapping[k] = @root + path
|
@reverse_mapping[k] = @root + path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def key_to_type_mapping_inverted
|
||||||
|
# Goes through the options of has_many / has_one and creates a hash of type => key
|
||||||
|
# Example: { books: 'books', people: 'author' }
|
||||||
|
# In the example above, people is the type of the objects in "included", but the name of the key is 'author'
|
||||||
|
# It creates this mapping so that to find the right derserializer for the given key (if any)
|
||||||
|
self.class.has_one_rel_options.map { |h, k| { h => k[:type]} }.reduce({}, :merge).invert.merge(
|
||||||
|
self.class.has_many_rel_options.map { |h, k| { h => k[:type]} }.reduce({}, :merge)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merged_rel_options
|
||||||
|
self.class.has_one_rel_options.merge(self.class.has_many_rel_options)
|
||||||
|
end
|
||||||
|
|
||||||
def deserialize!
|
def deserialize!
|
||||||
@reverse_mapping = {}
|
@reverse_mapping = {}
|
||||||
hashes = [deserialize_type, deserialize_id,
|
hashes = [deserialize_type, deserialize_id,
|
||||||
@ -103,7 +161,7 @@ module JSONAPI
|
|||||||
end
|
end
|
||||||
|
|
||||||
def deserialize_rels
|
def deserialize_rels
|
||||||
@relationships
|
@relationships
|
||||||
.map { |key, val| deserialize_rel(key, val) }
|
.map { |key, val| deserialize_rel(key, val) }
|
||||||
.reduce({}, :merge)
|
.reduce({}, :merge)
|
||||||
end
|
end
|
||||||
@ -120,12 +178,21 @@ module JSONAPI
|
|||||||
def deserialize_has_one_rel(key, val)
|
def deserialize_has_one_rel(key, val)
|
||||||
block = self.class.has_one_rel_blocks[key] ||
|
block = self.class.has_one_rel_blocks[key] ||
|
||||||
self.class.default_has_one_rel_block
|
self.class.default_has_one_rel_block
|
||||||
|
|
||||||
|
options = self.class.has_one_rel_options[key] || {}
|
||||||
|
|
||||||
return {} unless block
|
return {} unless block
|
||||||
|
|
||||||
id = val['data'] && val['data']['id']
|
id = val['data'] && val['data']['id']
|
||||||
type = val['data'] && val['data']['type']
|
type = val['data'] && val['data']['type']
|
||||||
hash = block.call(val, id, type, self.class.key_formatter.call(key))
|
hash = block.call(val, id, type, self.class.key_formatter.call(key))
|
||||||
|
|
||||||
register_mappings(hash.keys, "/relationships/#{key}")
|
register_mappings(hash.keys, "/relationships/#{key}")
|
||||||
|
|
||||||
|
if options.[](:with_included)
|
||||||
|
return {**hash, key.to_sym => find_in_included(id:, type:)}
|
||||||
|
end
|
||||||
|
|
||||||
hash
|
hash
|
||||||
end
|
end
|
||||||
# rubocop: enable Metrics/AbcSize
|
# rubocop: enable Metrics/AbcSize
|
||||||
@ -134,14 +201,32 @@ module JSONAPI
|
|||||||
def deserialize_has_many_rel(key, val)
|
def deserialize_has_many_rel(key, val)
|
||||||
block = self.class.has_many_rel_blocks[key] ||
|
block = self.class.has_many_rel_blocks[key] ||
|
||||||
self.class.default_has_many_rel_block
|
self.class.default_has_many_rel_block
|
||||||
|
|
||||||
|
|
||||||
|
options = self.class.has_many_rel_options[key] || {}
|
||||||
|
|
||||||
return {} unless block && val['data'].is_a?(Array)
|
return {} unless block && val['data'].is_a?(Array)
|
||||||
|
|
||||||
ids = val['data'].map { |ri| ri['id'] }
|
ids = val['data'].map { |ri| ri['id'] }
|
||||||
types = val['data'].map { |ri| ri['type'] }
|
types = val['data'].map { |ri| ri['type'] }
|
||||||
hash = block.call(val, ids, types, self.class.key_formatter.call(key))
|
hash = block.call(val, ids, types, self.class.key_formatter.call(key))
|
||||||
|
|
||||||
register_mappings(hash.keys, "/relationships/#{key}")
|
register_mappings(hash.keys, "/relationships/#{key}")
|
||||||
|
|
||||||
|
if options.[](:with_included)
|
||||||
|
return {**hash, key.to_sym => ids.map { |id| find_in_included(id: id, type: types[ids.index(id)]) }}
|
||||||
|
end
|
||||||
|
|
||||||
hash
|
hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_in_included(id:, type:)
|
||||||
|
# Cross referencing the relationship id and type with the included objects
|
||||||
|
cross_reference = @included.select { |doc| doc[:object]&.instance_variable_get(:@id) == id && doc[:object].instance_variable_get(:@type) == type }&.first
|
||||||
|
|
||||||
|
# If the deserializer is created using a given class, we will need to call .to_h on it instead of plucking all its attributes
|
||||||
|
cross_reference[:has_deserializer] ? cross_reference[:object].to_h : cross_reference[:object].instance_variable_get(:@attributes).to_h
|
||||||
|
end
|
||||||
# rubocop: enable Metrics/AbcSize
|
# rubocop: enable Metrics/AbcSize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -32,17 +32,19 @@ module JSONAPI
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_one(key = nil, &block)
|
def has_one(key = nil, with_included: false, deserializer: nil, type: key, &block)
|
||||||
if key
|
if key
|
||||||
has_one_rel_blocks[key.to_s] = block || DEFAULT_HAS_ONE_BLOCK
|
has_one_rel_blocks[key.to_s] = block || DEFAULT_HAS_ONE_BLOCK
|
||||||
|
has_one_rel_options[key.to_s] = { with_included:, deserializer:, type: type&.to_s&.to_sym }
|
||||||
else
|
else
|
||||||
self.default_has_one_rel_block = block || DEFAULT_HAS_ONE_BLOCK
|
self.default_has_one_rel_block = block || DEFAULT_HAS_ONE_BLOCK
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_many(key = nil, &block)
|
def has_many(key = nil, with_included: false, deserializer: nil, type: key, &block)
|
||||||
if key
|
if key
|
||||||
has_many_rel_blocks[key.to_s] = block || DEFAULT_HAS_MANY_BLOCK
|
has_many_rel_blocks[key.to_s] = block || DEFAULT_HAS_MANY_BLOCK
|
||||||
|
has_many_rel_options[key.to_s] = { with_included:, deserializer:, type: type&.to_s&.to_sym }
|
||||||
else
|
else
|
||||||
self.default_has_many_rel_block = block || DEFAULT_HAS_MANY_BLOCK
|
self.default_has_many_rel_block = block || DEFAULT_HAS_MANY_BLOCK
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user