Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fd37936812 |
45
.rubocop.yml
45
.rubocop.yml
@ -2,6 +2,9 @@ require:
|
|||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
|
|
||||||
Style/FrozenStringLiteralComment:
|
Style/FrozenStringLiteralComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@ -38,41 +41,25 @@ Performance/TimesMap:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/**/**.rb'
|
- 'spec/**/**.rb'
|
||||||
|
|
||||||
# TODO: Fix these...
|
Metrics/ModuleLength:
|
||||||
Style/Documentation:
|
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/GuardClause:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
|
||||||
Style/ConditionalAssignment:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
|
||||||
Style/IfUnlessModifier:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
|
||||||
Lint/AssignmentInCondition:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
|
||||||
Metrics:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/**/**.rb'
|
||||||
|
|
||||||
|
Metrics/MethodLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Layout/LineLength:
|
Metrics/CyclomaticComplexity:
|
||||||
Exclude:
|
Max: 12
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
|
||||||
Naming/PredicateName:
|
Metrics/PerceivedComplexity:
|
||||||
Exclude:
|
Max: 12
|
||||||
- 'lib/**/**.rb'
|
|
||||||
|
Metrics/AbcSize:
|
||||||
|
Max: 36
|
||||||
|
|
||||||
Naming/AccessorMethodName:
|
Naming/AccessorMethodName:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/**/**.rb'
|
- '**/**/dsl.rb'
|
||||||
|
@ -19,9 +19,7 @@ Gem::Specification.new do |gem|
|
|||||||
gem.require_paths = ['lib']
|
gem.require_paths = ['lib']
|
||||||
gem.extra_rdoc_files = ['LICENSE.txt', 'README.md']
|
gem.extra_rdoc_files = ['LICENSE.txt', 'README.md']
|
||||||
|
|
||||||
gem.add_runtime_dependency('activesupport', '>= 4.2')
|
gem.add_development_dependency('dry-inflector')
|
||||||
|
|
||||||
gem.add_development_dependency('activerecord')
|
|
||||||
gem.add_development_dependency('bundler')
|
gem.add_development_dependency('bundler')
|
||||||
gem.add_development_dependency('byebug')
|
gem.add_development_dependency('byebug')
|
||||||
gem.add_development_dependency('ffaker')
|
gem.add_development_dependency('ffaker')
|
||||||
@ -33,4 +31,5 @@ Gem::Specification.new do |gem|
|
|||||||
gem.add_development_dependency('rubocop-rspec')
|
gem.add_development_dependency('rubocop-rspec')
|
||||||
gem.add_development_dependency('simplecov')
|
gem.add_development_dependency('simplecov')
|
||||||
gem.add_development_dependency('sqlite3')
|
gem.add_development_dependency('sqlite3')
|
||||||
|
gem.add_development_dependency('activesupport')
|
||||||
end
|
end
|
||||||
|
@ -1,12 +1,67 @@
|
|||||||
require 'fast_jsonapi'
|
require 'jsonapi/serializer/base'
|
||||||
|
require 'jsonapi/serializer/core'
|
||||||
|
require 'jsonapi/serializer/dsl'
|
||||||
|
require 'jsonapi/serializer/errors'
|
||||||
|
require 'jsonapi/serializer/trackable'
|
||||||
|
|
||||||
|
# Provides JSONAPI related functionality
|
||||||
module JSONAPI
|
module JSONAPI
|
||||||
|
# Provides JSONAPI serialization functionality
|
||||||
module Serializer
|
module Serializer
|
||||||
# TODO: Move and cleanup the old implementation...
|
extend Trackable
|
||||||
|
|
||||||
|
# Self registers any inherited/extended class to keep track of it
|
||||||
|
#
|
||||||
|
# @return nothing
|
||||||
def self.included(base)
|
def self.included(base)
|
||||||
|
super
|
||||||
|
|
||||||
|
register_serializer(base)
|
||||||
|
|
||||||
base.class_eval do
|
base.class_eval do
|
||||||
include FastJsonapi::ObjectSerializer
|
include ::JSONAPI::Serializer::Base
|
||||||
|
extend ::JSONAPI::Serializer::DSL
|
||||||
|
extend ::JSONAPI::Serializer::Core
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Serializes an object or a collection
|
||||||
|
#
|
||||||
|
# The `options` are passed to the serializer instance and follow the
|
||||||
|
# same purpose.
|
||||||
|
#
|
||||||
|
# The only extra option is the `options[:serializers]`
|
||||||
|
# one can use to pass a mapping of object to serializer classes to
|
||||||
|
# be strict about how each object is serialized.
|
||||||
|
#
|
||||||
|
# @param object_or_collection [Object] to be serialized
|
||||||
|
# @param options [Hash] serialization parameters
|
||||||
|
# @return [Hash] of serialized JSONAPI data
|
||||||
|
def self.serialize(object_or_collection, options: nil)
|
||||||
|
options = options.dup || {}
|
||||||
|
serializers = options.delete(:serializers)
|
||||||
|
is_col = options[:is_collection] = collection?(
|
||||||
|
object_or_collection, force: options[:is_collection]
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = for_object(object_or_collection.first, serializers) if is_col
|
||||||
|
serializer ||= for_object(object_or_collection, serializers)
|
||||||
|
|
||||||
|
serializer.new(object_or_collection, options).serializable_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detects a collection/enumerable
|
||||||
|
#
|
||||||
|
# @param resource [Object] to detect
|
||||||
|
# @return [TrueClass] on a successful detection
|
||||||
|
def self.collection?(resource, force: nil)
|
||||||
|
return force unless force.nil?
|
||||||
|
|
||||||
|
# Rails 4 does not use [Enumerable]...
|
||||||
|
active_record = defined?(ActiveRecord::Relation)
|
||||||
|
return true if active_record && resource.is_a?(ActiveRecord::Relation)
|
||||||
|
|
||||||
|
resource.is_a?(Enumerable) && !resource.respond_to?(:each_pair)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
53
lib/jsonapi/serializer/base.rb
Normal file
53
lib/jsonapi/serializer/base.rb
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module JSONAPI
|
||||||
|
module Serializer
|
||||||
|
# Our serializer PORO functionality
|
||||||
|
module Base
|
||||||
|
def initialize(resource, opts = {})
|
||||||
|
@resource = resource
|
||||||
|
@options = opts.dup
|
||||||
|
@params = @options.delete(:params) || {}
|
||||||
|
@fieldsets = @options.delete(:fields) || {}
|
||||||
|
@includes = @options.delete(:include) || []
|
||||||
|
@include = @includes.map(&:to_s).map(&:strip).reject(&:empty?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serializable_hash
|
||||||
|
is_collection = ::JSONAPI::Serializer.collection?(
|
||||||
|
@resource, force: @options[:is_collection]
|
||||||
|
)
|
||||||
|
|
||||||
|
jsonapi = { data: nil }
|
||||||
|
jsonapi[:data] = [] if is_collection
|
||||||
|
jsonapi[:meta] = @options[:meta] if @options[:meta].is_a?(Hash)
|
||||||
|
jsonapi[:links] = @options[:links] if @options[:links].is_a?(Hash)
|
||||||
|
|
||||||
|
return jsonapi if @resource.nil? || (is_collection && @resource.empty?)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
included = []
|
||||||
|
|
||||||
|
# Used to avoid including duplicate records...
|
||||||
|
included_oids = Set.new
|
||||||
|
|
||||||
|
Array(@resource).each do |record|
|
||||||
|
serializer_class = self.class unless is_collection
|
||||||
|
serializer_class ||= JSONAPI::Serializer.for_object(record)
|
||||||
|
|
||||||
|
fieldset = @fieldsets[serializer_class.record_type]
|
||||||
|
data << serializer_class.record_hash(record, fieldset, @params)
|
||||||
|
|
||||||
|
included += serializer_class.record_includes(
|
||||||
|
record, @includes, included_oids, @fieldsets, @params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
jsonapi[:data] = data
|
||||||
|
jsonapi[:data] = data.first unless is_collection
|
||||||
|
jsonapi[:included] = included unless @includes.empty?
|
||||||
|
jsonapi
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
397
lib/jsonapi/serializer/core.rb
Normal file
397
lib/jsonapi/serializer/core.rb
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'digest/sha1'
|
||||||
|
|
||||||
|
module JSONAPI
|
||||||
|
module Serializer
|
||||||
|
# Our JSONAPI implementation
|
||||||
|
module Core
|
||||||
|
DEFAULT_CACHE_NAMESPACE = 'jsonapi-serializer'
|
||||||
|
|
||||||
|
# Generates the JSONAPI [Hash] for a record
|
||||||
|
#
|
||||||
|
# @param record [Object] the record to process
|
||||||
|
# @param fieldset [Array<String>] of attributes to serialize
|
||||||
|
# @param params [Hash] the record processing parameters
|
||||||
|
# @return [Hash]
|
||||||
|
def record_hash(record, fieldset, params)
|
||||||
|
if @cache_store_instance
|
||||||
|
cache_opts = record_cache_options(
|
||||||
|
@cache_store_options, fieldset, @options
|
||||||
|
)
|
||||||
|
|
||||||
|
rhash = @cache_store_instance.fetch(record, **cache_opts) do
|
||||||
|
rels = cachable_relationships_to_serialize
|
||||||
|
record_hash_data(record, fieldset, params, rels)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless uncachable_relationships_to_serialize.nil?
|
||||||
|
rels = uncachable_relationships_to_serialize
|
||||||
|
rhash[:relationships] = (rhash[:relationships] || {}).merge(
|
||||||
|
relationships_hash(record, rels, fieldset, params)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
rels = @relationships_to_serialize
|
||||||
|
rhash = record_hash_data(record, fieldset, params, rels)
|
||||||
|
end
|
||||||
|
|
||||||
|
rhash[:meta] = meta_hash(@meta_to_serialize, record, params)
|
||||||
|
rhash.delete(:meta) if rhash[:meta].nil?
|
||||||
|
rhash.delete(:links) if rhash[:links].nil?
|
||||||
|
|
||||||
|
rhash
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generates the JSONAPI [Array] for (includes) related records of a record
|
||||||
|
#
|
||||||
|
# @param record [Object] the record to process
|
||||||
|
# @param items [Array<String>] items to include
|
||||||
|
# @param known [Set] all the item identifiers already included
|
||||||
|
# @param fieldsets [Array<String>] of attributes to serialize
|
||||||
|
# @param params [Hash] the record processing parameters
|
||||||
|
# @return [Array] of data
|
||||||
|
# rubocop:disable Metrics/BlockLength
|
||||||
|
def record_includes(record, items, known, fieldsets, params)
|
||||||
|
return [] if items.nil? || @relationships_to_serialize.nil?
|
||||||
|
return [] if items.empty? || @relationships_to_serialize.empty?
|
||||||
|
|
||||||
|
items = parse_includes_list(items)
|
||||||
|
|
||||||
|
items.each_with_object([]) do |(item, item_includes), included|
|
||||||
|
to_include = record_include_item(item, record, params)
|
||||||
|
next if to_include.nil?
|
||||||
|
|
||||||
|
rel_objects, rel_options = to_include
|
||||||
|
|
||||||
|
Array(rel_objects).each do |rel_obj|
|
||||||
|
serializer = rel_options[:serializer]
|
||||||
|
|
||||||
|
if serializer.is_a?(Proc)
|
||||||
|
serializer = call_proc(
|
||||||
|
serializer, rel_obj, params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
serializer ||= ::JSONAPI::Serializer.for_object(
|
||||||
|
rel_obj, rel_options[:serializers]
|
||||||
|
)
|
||||||
|
|
||||||
|
if item_includes.any?
|
||||||
|
included.concat(
|
||||||
|
serializer.record_includes(
|
||||||
|
rel_obj, item_includes, known, fieldsets, params
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
rel_obj_id = serializer.record_type.to_s.dup.concat(
|
||||||
|
serializer.id_from_record(rel_obj, params).to_s
|
||||||
|
)
|
||||||
|
|
||||||
|
next if known.include?(rel_obj_id)
|
||||||
|
|
||||||
|
known << rel_obj_id
|
||||||
|
|
||||||
|
included << serializer.record_hash(
|
||||||
|
rel_obj, fieldsets[serializer.record_type], params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/BlockLength
|
||||||
|
|
||||||
|
# Returns the record identifier value
|
||||||
|
#
|
||||||
|
# @param record [Object] the record to process
|
||||||
|
# @param params [Hash] the record processing parameters
|
||||||
|
# @return [String] identifier of the record
|
||||||
|
def id_from_record(record, params)
|
||||||
|
raise ::JSONAPI::Serializer::IdError if record_id.nil?
|
||||||
|
|
||||||
|
call_proc_or_method(record_id, record, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the record identifier data
|
||||||
|
#
|
||||||
|
# @param record [Object] the record to process
|
||||||
|
# @param params [Hash] the record processing parameters
|
||||||
|
# @return [Hash] with the type and identifier of the record
|
||||||
|
def id_hash(id, use_default: false)
|
||||||
|
if id.present?
|
||||||
|
{ id: id.to_s, type: record_type }
|
||||||
|
elsif use_default
|
||||||
|
{ id: nil, type: record_type }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def record_include_item(item, record, params)
|
||||||
|
relationship = @relationships_to_serialize[item]
|
||||||
|
|
||||||
|
raise IncludeError.new(item, self) if relationship.nil?
|
||||||
|
|
||||||
|
rel_options = relationship[:options]
|
||||||
|
|
||||||
|
return unless condition_passes?(rel_options[:if], record, params)
|
||||||
|
|
||||||
|
objects = call_proc_or_method(
|
||||||
|
relationship[:object_block] || relationship[:name], record, params
|
||||||
|
)
|
||||||
|
|
||||||
|
return if objects.nil?
|
||||||
|
return if objects.respond_to?(:empty?) && objects.empty?
|
||||||
|
|
||||||
|
[objects, rel_options]
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_hash_data(record, fieldset, params, relationships)
|
||||||
|
temp_hash = id_hash(id_from_record(record, params), use_default: true)
|
||||||
|
|
||||||
|
if @attributes_to_serialize
|
||||||
|
temp_hash[:attributes] = attributes_hash(
|
||||||
|
record, fieldset, params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless relationships.nil?
|
||||||
|
temp_hash[:relationships] = relationships_hash(
|
||||||
|
record,
|
||||||
|
relationships,
|
||||||
|
fieldset,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
temp_hash[:links] = links_hash(@data_links, record, params)
|
||||||
|
temp_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def relationships_hash(record, relationships, fieldset, params)
|
||||||
|
relationships = relationships.slice(*fieldset) unless fieldset.nil?
|
||||||
|
|
||||||
|
relationships.each_with_object({}) do |(key, rel), rhash|
|
||||||
|
rel_opts = rel[:options]
|
||||||
|
|
||||||
|
next unless condition_passes?(rel_opts[:if], record, params)
|
||||||
|
|
||||||
|
key = run_key_transform(key)
|
||||||
|
|
||||||
|
rhash[key] = {
|
||||||
|
data: relationship_ids(rel, record, params),
|
||||||
|
meta: meta_hash(rel_opts[:meta], record, params),
|
||||||
|
links: links_hash(rel_opts[:links], record, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
rhash[key].delete(:meta) if rhash[key][:meta].nil?
|
||||||
|
rhash[key].delete(:links) if rhash[key][:links].nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the relationship linkage data
|
||||||
|
#
|
||||||
|
# @param relationship [Hash] representing the relationship definition
|
||||||
|
# @param record [Object] the record to process
|
||||||
|
# @param params [Hash] the object processing parameters
|
||||||
|
# @return [Array] of hashes
|
||||||
|
# rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
|
||||||
|
def relationship_ids(relationship, record, params)
|
||||||
|
rel_opts = relationship[:options]
|
||||||
|
has_many = (relationship[:relationship_type] == :has_many)
|
||||||
|
serializer = rel_opts[:serializer]
|
||||||
|
|
||||||
|
ids_rails_postfix = '_id'
|
||||||
|
ids_rails_postfix = '_ids' if has_many
|
||||||
|
|
||||||
|
if serializer.is_a?(Class)
|
||||||
|
ids_meth = rel_opts[:ids_method_name]
|
||||||
|
ids_meth ||= relationship[:name].to_s + ids_rails_postfix
|
||||||
|
|
||||||
|
ids = record.public_send(ids_meth) if record.respond_to?(ids_meth)
|
||||||
|
|
||||||
|
return serializer.id_hash(ids) unless ids.nil? && has_many
|
||||||
|
|
||||||
|
return ids.map! { |oid| serializer.id_hash(oid) } unless ids.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
obj_method_name = relationship[:object_block] || relationship[:name]
|
||||||
|
rel_objects = call_proc_or_method(obj_method_name, record, params)
|
||||||
|
rel_objects = Array(rel_objects)
|
||||||
|
ids = rel_objects.map do |robj|
|
||||||
|
robj_ser = serializer
|
||||||
|
|
||||||
|
robj_ser = call_proc(robj_ser, robj, params) if robj_ser.is_a?(Proc)
|
||||||
|
|
||||||
|
robj_ser ||= ::JSONAPI::Serializer.for_object(robj)
|
||||||
|
robj_ser.id_hash(robj_ser.id_from_record(robj, params))
|
||||||
|
end
|
||||||
|
|
||||||
|
return ids if has_many
|
||||||
|
|
||||||
|
ids.first
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
|
||||||
|
|
||||||
|
def cachable_relationships_to_serialize
|
||||||
|
return nil if @relationships_to_serialize.nil?
|
||||||
|
|
||||||
|
@cachable_relationships_to_serialize ||= (
|
||||||
|
@relationships_to_serialize.to_a -
|
||||||
|
uncachable_relationships_to_serialize.to_a
|
||||||
|
).to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
def uncachable_relationships_to_serialize
|
||||||
|
return nil if @relationships_to_serialize.nil?
|
||||||
|
|
||||||
|
@uncachable_relationships_to_serialize ||= \
|
||||||
|
@relationships_to_serialize.select do |_, rel|
|
||||||
|
rel[:options][:serializer]&.cache_store_instance.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Processes the meta
|
||||||
|
#
|
||||||
|
# @param maybe_meta [Object] either a [Hash], [String] or [Proc] to call
|
||||||
|
# @param record [Object] the meta object to process
|
||||||
|
# @param params [Hash] the object processing parameters
|
||||||
|
# @return [Hash] or nothing
|
||||||
|
def meta_hash(maybe_meta, record, params)
|
||||||
|
return maybe_meta if maybe_meta.is_a?(Hash)
|
||||||
|
|
||||||
|
return if maybe_meta.nil?
|
||||||
|
|
||||||
|
call_proc_or_method(maybe_meta, record, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Processes the links
|
||||||
|
#
|
||||||
|
# @param maybe_links [Object] either a [Hash], [String] or [Proc] to call
|
||||||
|
# @param record [Object] the meta object to process
|
||||||
|
# @param params [Hash] the object processing parameters
|
||||||
|
# @return [Hash] or nothing
|
||||||
|
def links_hash(maybe_links, record, params)
|
||||||
|
return if maybe_links.nil?
|
||||||
|
|
||||||
|
is_callable = !maybe_links.is_a?(Hash)
|
||||||
|
return call_proc_or_method(maybe_links, record, params) if is_callable
|
||||||
|
|
||||||
|
maybe_links.each_with_object({}) do |(key, link), lhash|
|
||||||
|
options = {}
|
||||||
|
method = link
|
||||||
|
|
||||||
|
if link.is_a?(Hash)
|
||||||
|
options = link[:options]
|
||||||
|
method = link[:method]
|
||||||
|
end
|
||||||
|
|
||||||
|
next unless condition_passes?(options[:if], record, params)
|
||||||
|
|
||||||
|
key = run_key_transform(key)
|
||||||
|
|
||||||
|
lhash[key] = call_proc_or_method(method, record, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cache options helper. Use it to adapt cache keys/rules.
|
||||||
|
#
|
||||||
|
# If a fieldset is specified, it modifies the namespace to include the
|
||||||
|
# fields from the fieldset.
|
||||||
|
#
|
||||||
|
# @param options [Hash] default cache options
|
||||||
|
# @param fieldset [Array, nil] passed fieldset values
|
||||||
|
# @param params [Hash] the serializer params
|
||||||
|
#
|
||||||
|
# @return [Hash] processed options hash
|
||||||
|
# rubocop:disable Lint/UnusedMethodArgument
|
||||||
|
def record_cache_options(options, fieldset, params)
|
||||||
|
return options unless fieldset
|
||||||
|
|
||||||
|
options = options ? options.dup : {}
|
||||||
|
options[:namespace] ||= const_get('DEFAULT_CACHE_NAMESPACE')
|
||||||
|
|
||||||
|
fskey = fieldset.join('_')
|
||||||
|
|
||||||
|
# Use a fixed-length fieldset key if the current length is more than
|
||||||
|
# the length of a SHA1 digest
|
||||||
|
fskey = Digest::SHA1.hexdigest(fskey) if fskey.length > 40
|
||||||
|
|
||||||
|
options[:namespace] = "#{options[:namespace]}-fieldset:#{fskey}"
|
||||||
|
options
|
||||||
|
end
|
||||||
|
# rubocop:enable Lint/UnusedMethodArgument
|
||||||
|
|
||||||
|
def attributes_hash(record, fieldset, params)
|
||||||
|
attributes = @attributes_to_serialize
|
||||||
|
attributes = attributes.slice(*fieldset) unless fieldset.nil?
|
||||||
|
|
||||||
|
attributes.each_with_object({}) do |(key, attribute), ahash|
|
||||||
|
options = attribute[:options] || {}
|
||||||
|
method = attribute[:method]
|
||||||
|
|
||||||
|
next unless condition_passes?(options[:if], record, params)
|
||||||
|
|
||||||
|
key = run_key_transform(key)
|
||||||
|
|
||||||
|
ahash[key] = call_proc_or_method(method, record, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def condition_passes?(maybe_proc, record, params)
|
||||||
|
return true unless maybe_proc.is_a?(Proc)
|
||||||
|
|
||||||
|
call_proc(maybe_proc, record, params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call_proc_or_method(maybe_proc, record, params)
|
||||||
|
return call_proc(maybe_proc, record, params) if maybe_proc.is_a?(Proc)
|
||||||
|
|
||||||
|
record.public_send(maybe_proc)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Calls [Proc] with respect to the number of parameters it takes
|
||||||
|
#
|
||||||
|
# @param proc [Proc] to call
|
||||||
|
# @param params [Array] of parameters to be passed to the Proc
|
||||||
|
# @return [Object] the result of the Proc call with the supplied parameters
|
||||||
|
def call_proc(proc, *params)
|
||||||
|
proc.call(*params.take(proc.parameters.length))
|
||||||
|
end
|
||||||
|
|
||||||
|
# It chops out the root association (first part) from each include.
|
||||||
|
#
|
||||||
|
# It keeps an unique list and collects all of the rest of the include
|
||||||
|
# value to hand it off to the next related to include serializer.
|
||||||
|
#
|
||||||
|
# This method will turn that include array into a Hash that looks like:
|
||||||
|
#
|
||||||
|
# {
|
||||||
|
# authors: Set.new([
|
||||||
|
# 'books',
|
||||||
|
# 'books.genre',
|
||||||
|
# 'books.genre.books',
|
||||||
|
# 'books.genre.books.authors',
|
||||||
|
# 'books.genre.books.genre'
|
||||||
|
# ]),
|
||||||
|
# genre: Set.new(['books'])
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Because the serializer only cares about the root associations
|
||||||
|
# included, it only needs the first segment of each include
|
||||||
|
# (for books, it's the "authors" and "genre") and it doesn't need to
|
||||||
|
# waste cycles parsing the rest of the include value. That will be done
|
||||||
|
# by the next serializer in line.
|
||||||
|
#
|
||||||
|
# @param includes_list [List] to be parsed
|
||||||
|
# @return [Hash]
|
||||||
|
def parse_includes_list(includes_list)
|
||||||
|
includes_list.each_with_object({}) do |include_item, include_sets|
|
||||||
|
root, tail = include_item.to_s.split('.', 2)
|
||||||
|
include_sets[root.to_sym] ||= Set.new
|
||||||
|
include_sets[root.to_sym] << tail.to_sym if tail
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
192
lib/jsonapi/serializer/dsl.rb
Normal file
192
lib/jsonapi/serializer/dsl.rb
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# rubocop:disable Lint/DuplicateRescueException
|
||||||
|
begin
|
||||||
|
require 'active_support/inflector'
|
||||||
|
rescue LoadError
|
||||||
|
require 'dry/inflector'
|
||||||
|
rescue LoadError
|
||||||
|
warn(
|
||||||
|
'No inflector found. Install `dry-inflector` or `active_support/inflector`'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# rubocop:enable Lint/DuplicateRescueException
|
||||||
|
|
||||||
|
module JSONAPI
|
||||||
|
module Serializer
|
||||||
|
# Our serializer DSL
|
||||||
|
module DSL
|
||||||
|
attr_writer :record_type, :record_id
|
||||||
|
attr_accessor(
|
||||||
|
:transform_method,
|
||||||
|
:attributes_to_serialize,
|
||||||
|
:relationships_to_serialize,
|
||||||
|
:data_links,
|
||||||
|
:meta_to_serialize,
|
||||||
|
:cache_store_instance,
|
||||||
|
:cache_store_options
|
||||||
|
)
|
||||||
|
|
||||||
|
def inherited(subclass)
|
||||||
|
super
|
||||||
|
|
||||||
|
subclass.attributes_to_serialize = attributes_to_serialize.dup
|
||||||
|
subclass.relationships_to_serialize = relationships_to_serialize.dup
|
||||||
|
subclass.transform_method = transform_method
|
||||||
|
subclass.data_links = data_links.dup
|
||||||
|
subclass.cache_store_instance = cache_store_instance
|
||||||
|
subclass.cache_store_options = cache_store_options
|
||||||
|
subclass.meta_to_serialize = meta_to_serialize
|
||||||
|
subclass.record_id = record_id
|
||||||
|
|
||||||
|
::JSONAPI::Serializer.register_serializer(subclass)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_key_transform(transform_name)
|
||||||
|
@transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_type(type_name)
|
||||||
|
@record_type = type_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_type
|
||||||
|
@record_type ||= run_key_transform(
|
||||||
|
name.chomp('Serializer'),
|
||||||
|
[:demodulize, :underscore]
|
||||||
|
)
|
||||||
|
|
||||||
|
run_key_transform(@record_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_id(id_name = nil, &block)
|
||||||
|
@record_id = block || id_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def record_id
|
||||||
|
@record_id || :id
|
||||||
|
end
|
||||||
|
|
||||||
|
def attributes(*attributes_list, **options, &block)
|
||||||
|
@attributes_to_serialize ||= {}
|
||||||
|
|
||||||
|
attributes_list.each do |attr_name|
|
||||||
|
@attributes_to_serialize[attr_name] = {
|
||||||
|
key: attr_name,
|
||||||
|
method: block || attr_name,
|
||||||
|
options: options.freeze
|
||||||
|
}.freeze
|
||||||
|
end
|
||||||
|
end
|
||||||
|
alias attribute attributes
|
||||||
|
|
||||||
|
def belongs_to(relationship_name, options = {}, &block)
|
||||||
|
@relationships_to_serialize ||= {}
|
||||||
|
|
||||||
|
@relationships_to_serialize[relationship_name] = {
|
||||||
|
name: relationship_name,
|
||||||
|
relationship_type: __callee__,
|
||||||
|
options: options,
|
||||||
|
object_block: block
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the resolver...
|
||||||
|
::JSONAPI::Serializer
|
||||||
|
.serializers.map(&:resolve_relationship_serializers!)
|
||||||
|
end
|
||||||
|
alias has_one belongs_to
|
||||||
|
alias has_many belongs_to
|
||||||
|
|
||||||
|
def meta(meta_name = nil, &block)
|
||||||
|
@meta_to_serialize = meta_name || block
|
||||||
|
end
|
||||||
|
|
||||||
|
def link(link_name, link_method_name = nil, **options, &block)
|
||||||
|
@data_links ||= {}
|
||||||
|
|
||||||
|
@data_links[link_name] = {
|
||||||
|
key: link_name,
|
||||||
|
method: link_method_name || block,
|
||||||
|
options: options.freeze
|
||||||
|
}.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_options(cache_options)
|
||||||
|
@cache_store_instance = cache_options[:store]
|
||||||
|
@cache_store_options = cache_options.except(:store).freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_relationship_serializers!
|
||||||
|
return if @relationships_to_serialize.nil?
|
||||||
|
|
||||||
|
@relationships_to_serialize.each do |_rel_name, relationship|
|
||||||
|
next if relationship.frozen?
|
||||||
|
|
||||||
|
resolve_relationship_serializer(relationship)
|
||||||
|
end
|
||||||
|
rescue NotFoundError
|
||||||
|
# Ignore it, probably not all serializers are defined...
|
||||||
|
end
|
||||||
|
|
||||||
|
def run_key_transform(key, transform_methods = nil)
|
||||||
|
return key.to_sym if inflector.nil?
|
||||||
|
|
||||||
|
tmethods = @transform_method || transform_methods
|
||||||
|
|
||||||
|
return key.to_sym if tmethods.nil?
|
||||||
|
|
||||||
|
Array(tmethods).each do |tmethod|
|
||||||
|
key = inflector.send(tmethod, key.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
key.to_sym
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Maps tranformations to inflector methods
|
||||||
|
TRANSFORMS_MAPPING = {
|
||||||
|
camel: :camelize,
|
||||||
|
camel_lower: [:camelize, :lower],
|
||||||
|
dash: :dasherize,
|
||||||
|
underscore: :underscore
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# Resolves serializer options of relationship into proper classe(s)
|
||||||
|
#
|
||||||
|
# We skip classes and procs since those suggest per record serializer
|
||||||
|
# classes. On no options available, the name of the relationship is used
|
||||||
|
# to reflect on the serializer class.
|
||||||
|
#
|
||||||
|
# @params relationship [Hash] the relationship data
|
||||||
|
# @return nil
|
||||||
|
def resolve_relationship_serializer(relationship)
|
||||||
|
opts = relationship[:options]
|
||||||
|
|
||||||
|
# The two are exclusive...
|
||||||
|
return opts.delete(:serializer) unless opts[:serializers].nil?
|
||||||
|
|
||||||
|
sname = opts[:serializer] || relationship[:name].to_s
|
||||||
|
|
||||||
|
return if sname.is_a?(Class) || sname.is_a?(Proc)
|
||||||
|
|
||||||
|
singularize = relationship[:relationship_type] == :has_many
|
||||||
|
|
||||||
|
sname = inflector.singularize(sname) if inflector && singularize
|
||||||
|
|
||||||
|
opts[:serializer] = ::JSONAPI::Serializer.for_type(sname)
|
||||||
|
|
||||||
|
opts.freeze && relationship.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper method to pick an available inflector implementation
|
||||||
|
#
|
||||||
|
# @return [Object]
|
||||||
|
def inflector
|
||||||
|
ActiveSupport::Inflector
|
||||||
|
rescue NameError
|
||||||
|
Dry::Inflector.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -2,17 +2,44 @@
|
|||||||
|
|
||||||
module JSONAPI
|
module JSONAPI
|
||||||
module Serializer
|
module Serializer
|
||||||
|
# Generic error class
|
||||||
class Error < StandardError; end
|
class Error < StandardError; end
|
||||||
class UnsupportedIncludeError < Error
|
|
||||||
|
# Used to indicate that a resource `id` is missing
|
||||||
|
class IdError < StandardError
|
||||||
|
def message
|
||||||
|
'Resource ID is missing, see: '\
|
||||||
|
'https://jsonapi.org/format/#document-resource-object-identification'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Used to indicate that there's no serializer for an object class or type
|
||||||
|
class NotFoundError < Error
|
||||||
|
attr_reader :object, :classes
|
||||||
|
|
||||||
|
def initialize(object, classes)
|
||||||
|
super()
|
||||||
|
@object = object
|
||||||
|
@classes = classes
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
"No serializer found for #{object.inspect} in #{classes.join(',')}."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Used to indicate when there's a problem with an item from includes
|
||||||
|
class IncludeError < Error
|
||||||
attr_reader :include_item, :klass
|
attr_reader :include_item, :klass
|
||||||
|
|
||||||
def initialize(include_item, klass)
|
def initialize(include_item, klass)
|
||||||
|
super()
|
||||||
@include_item = include_item
|
@include_item = include_item
|
||||||
@klass = klass
|
@klass = klass
|
||||||
end
|
end
|
||||||
|
|
||||||
def message
|
def message
|
||||||
"#{include_item} is not specified as a relationship on #{klass}"
|
"#{include_item} is not available on #{klass}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
71
lib/jsonapi/serializer/trackable.rb
Normal file
71
lib/jsonapi/serializer/trackable.rb
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module JSONAPI
|
||||||
|
module Serializer
|
||||||
|
# Allows tracking and registering the descendant serializer classes
|
||||||
|
module Trackable
|
||||||
|
# Adds a [Class] to the list of known serializers
|
||||||
|
#
|
||||||
|
# Mostly used internally to resolve easily record serializer classes
|
||||||
|
#
|
||||||
|
# @param klass [Class] to add to the list of known serializers
|
||||||
|
# @return [Class] or nothing if the class is already cached
|
||||||
|
def register_serializer(klass)
|
||||||
|
@serializers ||= []
|
||||||
|
@serializers << klass unless @serializers.include?(klass)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a list of available serializers
|
||||||
|
#
|
||||||
|
# @return [Array] of classes
|
||||||
|
def serializers
|
||||||
|
@serializers
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the serializer class for a record/object
|
||||||
|
#
|
||||||
|
# It follows the basic convention that the object class name and its
|
||||||
|
# serializer class name are in the same namespace and the later ends
|
||||||
|
# with the name `Serializer`.
|
||||||
|
#
|
||||||
|
# Ex.: [MyApp::UserModel] has the serializer [MyApp::UserModelSerializer]
|
||||||
|
#
|
||||||
|
# @param object [Object] to find the serialization class for
|
||||||
|
# @param mapping [Hash] custom map of model to serializer classes
|
||||||
|
# @return [Class] of the serialization class
|
||||||
|
def for_object(object, mapping = nil)
|
||||||
|
if mapping.is_a?(Hash)
|
||||||
|
return (
|
||||||
|
mapping[object.class] || NotFoundError.new(object, mapping.values)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
serializers.each do |klass|
|
||||||
|
return klass if klass.name == "#{object.class.name}Serializer"
|
||||||
|
end
|
||||||
|
|
||||||
|
raise NotFoundError.new(object, serializers)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the serializer class for a type
|
||||||
|
#
|
||||||
|
# It looks for existing serializers types that match
|
||||||
|
#
|
||||||
|
# Ex.: [MyApp::UserModelSerializer.record_type] would be `user`.
|
||||||
|
#
|
||||||
|
# @param record_type [String] to find the serialization class for
|
||||||
|
# @param mapping [Hash] custom map of model to serializer classes
|
||||||
|
# @return [Class] of the serialization class
|
||||||
|
def for_type(record_type, mapping = nil)
|
||||||
|
classes = mapping.values if mapping.is_a?(Hash)
|
||||||
|
classes ||= serializers
|
||||||
|
|
||||||
|
classes.each do |klass|
|
||||||
|
return klass if klass.record_type.to_s == record_type.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
raise NotFoundError.new(record_type, classes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,5 +1,5 @@
|
|||||||
module JSONAPI
|
module JSONAPI
|
||||||
module Serializer
|
module Serializer
|
||||||
VERSION = '2.1.0'.freeze
|
VERSION = '3.0.0'.freeze
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
21
spec/fixtures/actor.rb
vendored
21
spec/fixtures/actor.rb
vendored
@ -46,22 +46,29 @@ class CamelCaseActorSerializer
|
|||||||
obj.movie_urls.values[0]
|
obj.movie_urls.values[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many(
|
has_many(:played_movies, serializer: :movie) do |object|
|
||||||
:played_movies,
|
|
||||||
serializer: :movie
|
|
||||||
) do |object|
|
|
||||||
object.movies
|
object.movies
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class DasherizedActorSerializer < CamelCaseActorSerializer
|
||||||
|
set_type :user_actor
|
||||||
|
set_key_transform :dash
|
||||||
|
end
|
||||||
|
|
||||||
class BadMovieSerializerActorSerializer < ActorSerializer
|
class BadMovieSerializerActorSerializer < ActorSerializer
|
||||||
has_many :played_movies, serializer: :bad, object_method_name: :movies
|
has_many :played_movies do
|
||||||
|
self
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module Cached
|
module Cached
|
||||||
|
class Actor < ::Actor; end
|
||||||
|
|
||||||
class ActorSerializer < ::ActorSerializer
|
class ActorSerializer < ::ActorSerializer
|
||||||
# TODO: Fix this, the serializer gets cached on inherited classes...
|
set_type :cached_actor
|
||||||
has_many :played_movies, serializer: :movie do |object|
|
|
||||||
|
has_many(:played_movies, serializer: :cached_movie) do |object|
|
||||||
object.movies
|
object.movies
|
||||||
end
|
end
|
||||||
|
|
||||||
|
54
spec/fixtures/movie.rb
vendored
54
spec/fixtures/movie.rb
vendored
@ -26,7 +26,7 @@ class Movie
|
|||||||
@url ||= FFaker::Internet.http_url
|
@url ||= FFaker::Internet.http_url
|
||||||
return @url if obj.nil?
|
return @url if obj.nil?
|
||||||
|
|
||||||
@url + '?' + obj.hash.to_s
|
"#{@url}?#{obj.hash}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def owner=(ownr)
|
def owner=(ownr)
|
||||||
@ -53,16 +53,17 @@ class MovieSerializer
|
|||||||
object.year
|
object.year
|
||||||
end
|
end
|
||||||
|
|
||||||
link :self, :url
|
link :self, :url, if: ->(object, _params) { object.is_a?(Movie) }
|
||||||
|
|
||||||
belongs_to :owner, serializer: UserSerializer
|
belongs_to :owner, serializer: UserSerializer
|
||||||
|
|
||||||
belongs_to :actor_or_user,
|
belongs_to(
|
||||||
id_method_name: :uid,
|
:actor_or_user,
|
||||||
polymorphic: {
|
serializers: {
|
||||||
Actor => :actor,
|
Actor => ActorSerializer,
|
||||||
User => :user
|
User => UserSerializer
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
has_many(
|
has_many(
|
||||||
:actors,
|
:actors,
|
||||||
@ -74,52 +75,41 @@ class MovieSerializer
|
|||||||
)
|
)
|
||||||
has_one(
|
has_one(
|
||||||
:creator,
|
:creator,
|
||||||
object_method_name: :owner,
|
|
||||||
id_method_name: :uid,
|
|
||||||
serializer: ->(object, _params) { UserSerializer if object.is_a?(User) }
|
serializer: ->(object, _params) { UserSerializer if object.is_a?(User) }
|
||||||
)
|
) do |object|
|
||||||
|
object.owner
|
||||||
|
end
|
||||||
has_many(
|
has_many(
|
||||||
:actors_and_users,
|
:actors_and_users,
|
||||||
id_method_name: :uid,
|
serializers: {
|
||||||
polymorphic: {
|
Actor => ActorSerializer,
|
||||||
Actor => :actor,
|
User => UserSerializer
|
||||||
User => :user
|
|
||||||
}
|
}
|
||||||
) do |obj|
|
) do |obj|
|
||||||
obj.polymorphics
|
obj.polymorphics
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many(
|
has_many(:dynamic_actors_and_users) do |obj|
|
||||||
:dynamic_actors_and_users,
|
|
||||||
id_method_name: :uid,
|
|
||||||
polymorphic: true
|
|
||||||
) do |obj|
|
|
||||||
obj.polymorphics
|
obj.polymorphics
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many(
|
has_many(:auto_detected_actors_and_users) do |obj|
|
||||||
:auto_detected_actors_and_users,
|
|
||||||
id_method_name: :uid
|
|
||||||
) do |obj|
|
|
||||||
obj.polymorphics
|
obj.polymorphics
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module Cached
|
module Cached
|
||||||
|
class Movie < ::Movie; end
|
||||||
|
|
||||||
class MovieSerializer < ::MovieSerializer
|
class MovieSerializer < ::MovieSerializer
|
||||||
|
set_type :cached_movie
|
||||||
|
|
||||||
cache_options(
|
cache_options(
|
||||||
store: ActorSerializer.cache_store_instance,
|
store: ActorSerializer.cache_store_instance,
|
||||||
namespace: 'test'
|
namespace: 'test'
|
||||||
)
|
)
|
||||||
|
|
||||||
has_one(
|
has_one(:creator, serializer: :cached_actor) do |obj|
|
||||||
:creator,
|
|
||||||
id_method_name: :uid,
|
|
||||||
serializer: :actor,
|
|
||||||
# TODO: Remove this undocumented option.
|
|
||||||
# Delegate the caching to the serializer exclusively.
|
|
||||||
cached: false
|
|
||||||
) do |obj|
|
|
||||||
obj.owner
|
obj.owner
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,8 +2,8 @@ require 'spec_helper'
|
|||||||
|
|
||||||
RSpec.describe JSONAPI::Serializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:actor) do
|
let(:actor) do
|
||||||
faked = Actor.fake
|
faked = Cached::Actor.fake
|
||||||
movie = Movie.fake
|
movie = Cached::Movie.fake
|
||||||
movie.owner = User.fake
|
movie.owner = User.fake
|
||||||
movie.actors = [faked]
|
movie.actors = [faked]
|
||||||
faked.movies = [movie]
|
faked.movies = [movie]
|
||||||
@ -16,11 +16,14 @@ RSpec.describe JSONAPI::Serializer do
|
|||||||
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
|
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
|
||||||
|
|
||||||
Cached::ActorSerializer.new(
|
Cached::ActorSerializer.new(
|
||||||
[actor, actor], include: ['played_movies', 'played_movies.owner']
|
[actor, actor],
|
||||||
|
include: ['played_movies', 'played_movies.owner']
|
||||||
).serializable_hash
|
).serializable_hash
|
||||||
|
|
||||||
expect(cache_store.delete(actor, namespace: 'test')).to be(true)
|
expect(cache_store.delete(actor, namespace: 'test')).to be(true)
|
||||||
expect(cache_store.delete(actor.movies[0], namespace: 'test')).to be(true)
|
expect(
|
||||||
|
cache_store.delete(actor.movies[0], namespace: 'test')
|
||||||
|
).to be(true)
|
||||||
expect(
|
expect(
|
||||||
cache_store.delete(actor.movies[0].owner, namespace: 'test')
|
cache_store.delete(actor.movies[0].owner, namespace: 'test')
|
||||||
).to be(false)
|
).to be(false)
|
||||||
@ -29,7 +32,9 @@ RSpec.describe JSONAPI::Serializer do
|
|||||||
context 'without relationships' do
|
context 'without relationships' do
|
||||||
let(:user) { User.fake }
|
let(:user) { User.fake }
|
||||||
|
|
||||||
let(:serialized) { Cached::UserSerializer.new(user).serializable_hash.as_json }
|
let(:serialized) do
|
||||||
|
Cached::UserSerializer.new(user).serializable_hash.as_json
|
||||||
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect(serialized['data']).not_to have_key('relationships')
|
expect(serialized['data']).not_to have_key('relationships')
|
||||||
@ -43,37 +48,46 @@ RSpec.describe JSONAPI::Serializer do
|
|||||||
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
|
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
|
||||||
|
|
||||||
Cached::ActorSerializer.new(
|
Cached::ActorSerializer.new(
|
||||||
[actor], fields: { actor: %i[first_name] }
|
[actor], fields: { cached_actor: %i[first_name] }
|
||||||
).serializable_hash
|
).serializable_hash
|
||||||
|
|
||||||
# Expect cached keys to match the passed fieldset
|
# Expect cached keys to match the passed fieldset
|
||||||
expect(cache_store.read(actor, namespace: 'test-fieldset:first_name')[:attributes].keys).to eq(%i[first_name])
|
cached_actor = cache_store.read(
|
||||||
|
actor, namespace: 'test-fieldset:first_name'
|
||||||
|
)
|
||||||
|
|
||||||
Cached::ActorSerializer.new(
|
expect(cached_actor[:attributes].keys).to eq(%i[first_name])
|
||||||
[actor]
|
|
||||||
).serializable_hash
|
Cached::ActorSerializer.new([actor]).serializable_hash
|
||||||
|
|
||||||
# Expect cached keys to match all valid actor fields (no fieldset)
|
# Expect cached keys to match all valid actor fields (no fieldset)
|
||||||
expect(cache_store.read(actor, namespace: 'test')[:attributes].keys).to eq(%i[first_name last_name email])
|
cached_actor = cache_store.read(actor, namespace: 'test')
|
||||||
expect(cache_store.delete(actor, namespace: 'test')).to be(true)
|
|
||||||
expect(cache_store.delete(actor, namespace: 'test-fieldset:first_name')).to be(true)
|
expect(cached_actor[:attributes].keys)
|
||||||
|
.to eq(%i[first_name last_name email])
|
||||||
|
|
||||||
|
expect(
|
||||||
|
cache_store.delete(actor, namespace: 'test-fieldset:first_name')
|
||||||
|
).to be(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when long fieldset is provided' do
|
context 'when long fieldset is provided' do
|
||||||
let(:actor_keys) { %i[first_name last_name more_fields yet_more_fields so_very_many_fields] }
|
let(:actor_keys) do
|
||||||
|
%i[first_name last_name more_fields yet_more_fields so_very_many_fields]
|
||||||
|
end
|
||||||
let(:digest_key) { Digest::SHA1.hexdigest(actor_keys.join('_')) }
|
let(:digest_key) { Digest::SHA1.hexdigest(actor_keys.join('_')) }
|
||||||
|
|
||||||
it 'includes the hashed fieldset in the namespace' do
|
it 'includes the hashed fieldset in the namespace' do
|
||||||
Cached::ActorSerializer.new(
|
Cached::ActorSerializer.new(
|
||||||
[actor], fields: { actor: actor_keys }
|
[actor], fields: { cached_actor: actor_keys }
|
||||||
).serializable_hash
|
).serializable_hash
|
||||||
|
|
||||||
expect(cache_store.read(actor, namespace: "test-fieldset:#{digest_key}")[:attributes].keys).to eq(
|
cached_actor = cache_store.read(
|
||||||
%i[first_name last_name]
|
actor, namespace: "test-fieldset:#{digest_key}"
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(cache_store.delete(actor, namespace: "test-fieldset:#{digest_key}")).to be(true)
|
expect(cached_actor[:attributes].keys).to eq(%i[first_name last_name])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,19 +7,14 @@ RSpec.describe JSONAPI::Serializer do
|
|||||||
describe 'with errors' do
|
describe 'with errors' do
|
||||||
it do
|
it do
|
||||||
expect do
|
expect do
|
||||||
BadMovieSerializerActorSerializer.new(
|
BadMovieSerializerActorSerializer.new(actor).serializable_hash
|
||||||
actor, include: ['played_movies']
|
end.to raise_error(JSONAPI::Serializer::NotFoundError)
|
||||||
)
|
|
||||||
end.to raise_error(
|
|
||||||
NameError, /cannot resolve a serializer class for 'bad'/
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect { ActorSerializer.new(actor, include: ['bad_include']) }
|
expect do
|
||||||
.to raise_error(
|
ActorSerializer.new(actor, include: ['bad_include']).serializable_hash
|
||||||
JSONAPI::Serializer::UnsupportedIncludeError, /bad_include is not specified as a relationship/
|
end.to raise_error(JSONAPI::Serializer::IncludeError)
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,9 +10,9 @@ RSpec.describe JSONAPI::Serializer do
|
|||||||
|
|
||||||
it do
|
it do
|
||||||
payload = event_name = nil
|
payload = event_name = nil
|
||||||
notification_name = (
|
notification_name = ''.concat(
|
||||||
::JSONAPI::Serializer::Instrumentation::NOTIFICATION_NAMESPACE +
|
::JSONAPI::Serializer::Instrumentation::NOTIFICATION_NAMESPACE,
|
||||||
'serializable_hash'
|
'serializable_hash'
|
||||||
)
|
)
|
||||||
|
|
||||||
ActiveSupport::Notifications.subscribe(
|
ActiveSupport::Notifications.subscribe(
|
||||||
|
@ -2,12 +2,12 @@ require 'spec_helper'
|
|||||||
|
|
||||||
RSpec.describe JSONAPI::Serializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:actor) { Actor.fake }
|
let(:actor) { Actor.fake }
|
||||||
let(:params) { {} }
|
|
||||||
let(:serialized) do
|
|
||||||
CamelCaseActorSerializer.new(actor, params).serializable_hash.as_json
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'camel case key tranformation' do
|
describe 'camel case key tranformation' do
|
||||||
|
let(:serialized) do
|
||||||
|
CamelCaseActorSerializer.new(actor).serializable_hash.as_json
|
||||||
|
end
|
||||||
|
|
||||||
it do
|
it do
|
||||||
expect(serialized['data']).to have_id(actor.uid)
|
expect(serialized['data']).to have_id(actor.uid)
|
||||||
expect(serialized['data']).to have_type('UserActor')
|
expect(serialized['data']).to have_type('UserActor')
|
||||||
@ -16,4 +16,18 @@ RSpec.describe JSONAPI::Serializer do
|
|||||||
expect(serialized['data']).to have_link('MovieUrl').with_value(nil)
|
expect(serialized['data']).to have_link('MovieUrl').with_value(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'inherited class dasherized case key tranformation' do
|
||||||
|
let(:serialized) do
|
||||||
|
DasherizedActorSerializer.new(actor).serializable_hash.as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
it do
|
||||||
|
expect(serialized['data']).to have_id(actor.uid)
|
||||||
|
expect(serialized['data']).to have_type('user-actor')
|
||||||
|
expect(serialized['data']).to have_attribute('first-name')
|
||||||
|
expect(serialized['data']).to have_relationship('played-movies')
|
||||||
|
expect(serialized['data']).to have_link('movie-url').with_value(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
22
spec/integration/mixed_collection_spec.rb
Normal file
22
spec/integration/mixed_collection_spec.rb
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe JSONAPI::Serializer do
|
||||||
|
let(:user) { User.fake }
|
||||||
|
let(:actor) { Actor.fake }
|
||||||
|
let(:movie) { Movie.fake }
|
||||||
|
let(:serialized) do
|
||||||
|
JSONAPI::Serializer.serialize(
|
||||||
|
[user, actor, movie]
|
||||||
|
).as_json
|
||||||
|
end
|
||||||
|
|
||||||
|
it do
|
||||||
|
expect(serialized['data']).to include(
|
||||||
|
have_type(:user).and(have_id(user.uid))
|
||||||
|
).and(
|
||||||
|
include(have_type('actor').and(have_id(actor.uid)))
|
||||||
|
).and(
|
||||||
|
include(have_type('movie').and(have_id(movie.id)))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user