commit
49193ab8f3
69
README.md
69
README.md
@ -30,6 +30,8 @@ Fast JSON API serialized 250 records in 3.01 ms
|
|||||||
* [Collection Serialization](#collection-serialization)
|
* [Collection Serialization](#collection-serialization)
|
||||||
* [Caching](#caching)
|
* [Caching](#caching)
|
||||||
* [Params](#params)
|
* [Params](#params)
|
||||||
|
* [Conditional Attributes](#conditional-attributes)
|
||||||
|
* [Conditional Relationships](#conditional-relationships)
|
||||||
* [Contributing](#contributing)
|
* [Contributing](#contributing)
|
||||||
|
|
||||||
|
|
||||||
@ -259,6 +261,26 @@ hash = MovieSerializer.new([movie, movie], options).serializable_hash
|
|||||||
json_string = MovieSerializer.new([movie, movie], options).serialized_json
|
json_string = MovieSerializer.new([movie, movie], options).serialized_json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Control Over Collection Serialization
|
||||||
|
|
||||||
|
You can use `is_collection` option to have better control over collection serialization.
|
||||||
|
|
||||||
|
If this option is not provided or `nil` autedetect logic is used to try understand
|
||||||
|
if provided resource is a single object or collection.
|
||||||
|
|
||||||
|
Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but
|
||||||
|
**cannot** guarantee that single vs collection will be always detected properly.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
options[:is_collection]
|
||||||
|
```
|
||||||
|
|
||||||
|
was introduced to be able to have precise control this behavior
|
||||||
|
|
||||||
|
- `nil` or not provided: will try to autodetect single vs collection (please, see notes above)
|
||||||
|
- `true` will always treat input resource as *collection*
|
||||||
|
- `false` will always treat input resource as *single object*
|
||||||
|
|
||||||
### Caching
|
### Caching
|
||||||
Requires a `cache_key` method be defined on model:
|
Requires a `cache_key` method be defined on model:
|
||||||
|
|
||||||
@ -307,6 +329,53 @@ serializer.serializable_hash
|
|||||||
Custom attributes and relationships that only receive the resource are still possible by defining
|
Custom attributes and relationships that only receive the resource are still possible by defining
|
||||||
the block to only receive one argument.
|
the block to only receive one argument.
|
||||||
|
|
||||||
|
### Conditional Attributes
|
||||||
|
|
||||||
|
Conditional attributes can be defined by passing a Proc to the `if` key on the `attribute` method. Return `true` if the attribute should be serialized, and `false` if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class MovieSerializer
|
||||||
|
include FastJsonapi::ObjectSerializer
|
||||||
|
|
||||||
|
attributes :name, :year
|
||||||
|
attribute :release_year, if: Proc.new do |record|
|
||||||
|
# Release year will only be serialized if it's greater than 1990
|
||||||
|
record.release_year > 1990
|
||||||
|
end
|
||||||
|
|
||||||
|
attribute :director, if: Proc.new do |record, params|
|
||||||
|
# The director will be serialized only if the :admin key of params is true
|
||||||
|
params && params[:admin] == true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ...
|
||||||
|
current_user = User.find(cookies[:current_user_id])
|
||||||
|
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
|
||||||
|
serializer.serializable_hash
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Relationships
|
||||||
|
|
||||||
|
Conditional relationships can be defined by passing a Proc to the `if` key. Return `true` if the relationship should be serialized, and `false` if not. The record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class MovieSerializer
|
||||||
|
include FastJsonapi::ObjectSerializer
|
||||||
|
|
||||||
|
# Actors will only be serialized if the record has any associated actors
|
||||||
|
has_many :actors, if: Proc.new { |record| record.actors.any? }
|
||||||
|
|
||||||
|
# Owner will only be serialized if the :admin key of params is true
|
||||||
|
belongs_to :owner, if: Proc.new { |record, params| params && params[:admin] == true }
|
||||||
|
end
|
||||||
|
|
||||||
|
# ...
|
||||||
|
current_user = User.find(cookies[:current_user_id])
|
||||||
|
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
|
||||||
|
serializer.serializable_hash
|
||||||
|
```
|
||||||
|
|
||||||
### Customizable Options
|
### Customizable Options
|
||||||
|
|
||||||
Option | Purpose | Example
|
Option | Purpose | Example
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
if defined?(::ActiveRecord)
|
|
||||||
::ActiveRecord::Associations::Builder::HasOne.class_eval do
|
::ActiveRecord::Associations::Builder::HasOne.class_eval do
|
||||||
# Based on
|
# Based on
|
||||||
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
|
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
|
||||||
@ -17,4 +16,3 @@ if defined?(::ActiveRecord)
|
|||||||
CODE
|
CODE
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
@ -2,5 +2,9 @@
|
|||||||
|
|
||||||
module FastJsonapi
|
module FastJsonapi
|
||||||
require 'fast_jsonapi/object_serializer'
|
require 'fast_jsonapi/object_serializer'
|
||||||
|
if defined?(::Rails)
|
||||||
|
require 'fast_jsonapi/railtie'
|
||||||
|
elsif defined?(::ActiveRecord)
|
||||||
require 'extensions/has_one'
|
require 'extensions/has_one'
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
29
lib/fast_jsonapi/attribute.rb
Normal file
29
lib/fast_jsonapi/attribute.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
module FastJsonapi
|
||||||
|
class Attribute
|
||||||
|
attr_reader :key, :method, :conditional_proc
|
||||||
|
|
||||||
|
def initialize(key:, method:, options: {})
|
||||||
|
@key = key
|
||||||
|
@method = method
|
||||||
|
@conditional_proc = options[:if]
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(record, serialization_params, output_hash)
|
||||||
|
if include_attribute?(record, serialization_params)
|
||||||
|
output_hash[key] = if method.is_a?(Proc)
|
||||||
|
method.arity == 1 ? method.call(record) : method.call(record, serialization_params)
|
||||||
|
else
|
||||||
|
record.public_send(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_attribute?(record, serialization_params)
|
||||||
|
if conditional_proc.present?
|
||||||
|
conditional_proc.call(record, serialization_params)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
lib/fast_jsonapi/link.rb
Normal file
18
lib/fast_jsonapi/link.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module FastJsonapi
|
||||||
|
class Link
|
||||||
|
attr_reader :key, :method
|
||||||
|
|
||||||
|
def initialize(key:, method:)
|
||||||
|
@key = key
|
||||||
|
@method = method
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(record, serialization_params, output_hash)
|
||||||
|
output_hash[key] = if method.is_a?(Proc)
|
||||||
|
method.arity == 1 ? method.call(record) : method.call(record, serialization_params)
|
||||||
|
else
|
||||||
|
record.public_send(method)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -3,6 +3,9 @@
|
|||||||
require 'active_support/core_ext/object'
|
require 'active_support/core_ext/object'
|
||||||
require 'active_support/concern'
|
require 'active_support/concern'
|
||||||
require 'active_support/inflector'
|
require 'active_support/inflector'
|
||||||
|
require 'fast_jsonapi/attribute'
|
||||||
|
require 'fast_jsonapi/relationship'
|
||||||
|
require 'fast_jsonapi/link'
|
||||||
require 'fast_jsonapi/serialization_core'
|
require 'fast_jsonapi/serialization_core'
|
||||||
|
|
||||||
module FastJsonapi
|
module FastJsonapi
|
||||||
@ -25,7 +28,7 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def serializable_hash
|
def serializable_hash
|
||||||
return hash_for_collection if is_collection?(@resource)
|
return hash_for_collection if is_collection?(@resource, @is_collection)
|
||||||
|
|
||||||
hash_for_one_record
|
hash_for_one_record
|
||||||
end
|
end
|
||||||
@ -72,6 +75,7 @@ module FastJsonapi
|
|||||||
@known_included_objects = {}
|
@known_included_objects = {}
|
||||||
@meta = options[:meta]
|
@meta = options[:meta]
|
||||||
@links = options[:links]
|
@links = options[:links]
|
||||||
|
@is_collection = options[:is_collection]
|
||||||
@params = options[:params] || {}
|
@params = options[:params] || {}
|
||||||
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
|
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
|
||||||
|
|
||||||
@ -81,8 +85,10 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_collection?(resource)
|
def is_collection?(resource, force_is_collection = nil)
|
||||||
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
|
return force_is_collection unless force_is_collection.nil?
|
||||||
|
|
||||||
|
resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
|
||||||
end
|
end
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
@ -118,6 +124,9 @@ module FastJsonapi
|
|||||||
underscore: :underscore
|
underscore: :underscore
|
||||||
}
|
}
|
||||||
self.transform_method = mapping[transform_name.to_sym]
|
self.transform_method = mapping[transform_name.to_sym]
|
||||||
|
|
||||||
|
# ensure that the record type is correctly transformed
|
||||||
|
set_type(reflected_record_type) if reflected_record_type
|
||||||
end
|
end
|
||||||
|
|
||||||
def run_key_transform(input)
|
def run_key_transform(input)
|
||||||
@ -149,48 +158,51 @@ module FastJsonapi
|
|||||||
|
|
||||||
def attributes(*attributes_list, &block)
|
def attributes(*attributes_list, &block)
|
||||||
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
|
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
|
||||||
|
options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
|
||||||
self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
|
self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
|
||||||
|
|
||||||
attributes_list.each do |attr_name|
|
attributes_list.each do |attr_name|
|
||||||
method_name = attr_name
|
method_name = attr_name
|
||||||
key = run_key_transform(method_name)
|
key = run_key_transform(method_name)
|
||||||
attributes_to_serialize[key] = block || method_name
|
attributes_to_serialize[key] = Attribute.new(
|
||||||
|
key: key,
|
||||||
|
method: block || method_name,
|
||||||
|
options: options
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :attribute, :attributes
|
alias_method :attribute, :attributes
|
||||||
|
|
||||||
def add_relationship(name, relationship)
|
def add_relationship(relationship)
|
||||||
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
|
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
|
||||||
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
|
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
|
||||||
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
|
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
|
||||||
|
|
||||||
if !relationship[:cached]
|
if !relationship.cached
|
||||||
self.uncachable_relationships_to_serialize[name] = relationship
|
self.uncachable_relationships_to_serialize[relationship.name] = relationship
|
||||||
else
|
else
|
||||||
self.cachable_relationships_to_serialize[name] = relationship
|
self.cachable_relationships_to_serialize[relationship.name] = relationship
|
||||||
end
|
end
|
||||||
self.relationships_to_serialize[name] = relationship
|
self.relationships_to_serialize[relationship.name] = relationship
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_many(relationship_name, options = {}, &block)
|
def has_many(relationship_name, options = {}, &block)
|
||||||
name = relationship_name.to_sym
|
relationship = create_relationship(relationship_name, :has_many, options, block)
|
||||||
hash = create_relationship_hash(relationship_name, :has_many, options, block)
|
add_relationship(relationship)
|
||||||
add_relationship(name, hash)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_one(relationship_name, options = {}, &block)
|
def has_one(relationship_name, options = {}, &block)
|
||||||
name = relationship_name.to_sym
|
relationship = create_relationship(relationship_name, :has_one, options, block)
|
||||||
hash = create_relationship_hash(relationship_name, :has_one, options, block)
|
add_relationship(relationship)
|
||||||
add_relationship(name, hash)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def belongs_to(relationship_name, options = {}, &block)
|
def belongs_to(relationship_name, options = {}, &block)
|
||||||
name = relationship_name.to_sym
|
relationship = create_relationship(relationship_name, :belongs_to, options, block)
|
||||||
hash = create_relationship_hash(relationship_name, :belongs_to, options, block)
|
add_relationship(relationship)
|
||||||
add_relationship(name, hash)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_relationship_hash(base_key, relationship_type, options, block)
|
def create_relationship(base_key, relationship_type, options, block)
|
||||||
name = base_key.to_sym
|
name = base_key.to_sym
|
||||||
if relationship_type == :has_many
|
if relationship_type == :has_many
|
||||||
base_serialization_key = base_key.to_s.singularize
|
base_serialization_key = base_key.to_s.singularize
|
||||||
@ -201,7 +213,7 @@ module FastJsonapi
|
|||||||
base_key_sym = name
|
base_key_sym = name
|
||||||
id_postfix = '_id'
|
id_postfix = '_id'
|
||||||
end
|
end
|
||||||
{
|
Relationship.new(
|
||||||
key: options[:key] || run_key_transform(base_key),
|
key: options[:key] || run_key_transform(base_key),
|
||||||
name: name,
|
name: name,
|
||||||
id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
|
id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
|
||||||
@ -210,9 +222,10 @@ module FastJsonapi
|
|||||||
object_block: block,
|
object_block: block,
|
||||||
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
|
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
|
||||||
relationship_type: relationship_type,
|
relationship_type: relationship_type,
|
||||||
cached: options[:cached] || false,
|
cached: options[:cached],
|
||||||
polymorphic: fetch_polymorphic_option(options)
|
polymorphic: fetch_polymorphic_option(options),
|
||||||
}
|
conditional_proc: options[:if]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def compute_serializer_name(serializer_key)
|
def compute_serializer_name(serializer_key)
|
||||||
@ -233,7 +246,11 @@ module FastJsonapi
|
|||||||
self.data_links = {} if self.data_links.nil?
|
self.data_links = {} if self.data_links.nil?
|
||||||
link_method_name = link_name if link_method_name.nil?
|
link_method_name = link_name if link_method_name.nil?
|
||||||
key = run_key_transform(link_name)
|
key = run_key_transform(link_name)
|
||||||
self.data_links[key] = block || link_method_name
|
|
||||||
|
self.data_links[key] = Link.new(
|
||||||
|
key: key,
|
||||||
|
method: block || link_method_name
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_includes!(includes)
|
def validate_includes!(includes)
|
||||||
@ -244,8 +261,8 @@ module FastJsonapi
|
|||||||
parse_include_item(include_item).each do |parsed_include|
|
parse_include_item(include_item).each do |parsed_include|
|
||||||
relationship_to_include = klass.relationships_to_serialize[parsed_include]
|
relationship_to_include = klass.relationships_to_serialize[parsed_include]
|
||||||
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
|
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
|
||||||
raise NotImplementedError if relationship_to_include[:polymorphic].is_a?(Hash)
|
raise NotImplementedError if relationship_to_include.polymorphic.is_a?(Hash)
|
||||||
klass = relationship_to_include[:serializer].to_s.constantize
|
klass = relationship_to_include.serializer.to_s.constantize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
11
lib/fast_jsonapi/railtie.rb
Normal file
11
lib/fast_jsonapi/railtie.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails/railtie'
|
||||||
|
|
||||||
|
class Railtie < Rails::Railtie
|
||||||
|
initializer 'fast_jsonapi.active_record' do
|
||||||
|
ActiveSupport.on_load :active_record do
|
||||||
|
require 'extensions/has_one'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
99
lib/fast_jsonapi/relationship.rb
Normal file
99
lib/fast_jsonapi/relationship.rb
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
module FastJsonapi
|
||||||
|
class Relationship
|
||||||
|
attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc
|
||||||
|
|
||||||
|
def initialize(
|
||||||
|
key:,
|
||||||
|
name:,
|
||||||
|
id_method_name:,
|
||||||
|
record_type:,
|
||||||
|
object_method_name:,
|
||||||
|
object_block:,
|
||||||
|
serializer:,
|
||||||
|
relationship_type:,
|
||||||
|
cached: false,
|
||||||
|
polymorphic:,
|
||||||
|
conditional_proc:
|
||||||
|
)
|
||||||
|
@key = key
|
||||||
|
@name = name
|
||||||
|
@id_method_name = id_method_name
|
||||||
|
@record_type = record_type
|
||||||
|
@object_method_name = object_method_name
|
||||||
|
@object_block = object_block
|
||||||
|
@serializer = serializer
|
||||||
|
@relationship_type = relationship_type
|
||||||
|
@cached = cached
|
||||||
|
@polymorphic = polymorphic
|
||||||
|
@conditional_proc = conditional_proc
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialize(record, serialization_params, output_hash)
|
||||||
|
if include_relationship?(record, serialization_params)
|
||||||
|
empty_case = relationship_type == :has_many ? [] : nil
|
||||||
|
output_hash[key] = {
|
||||||
|
data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_associated_object(record, params)
|
||||||
|
return object_block.call(record, params) unless object_block.nil?
|
||||||
|
record.send(object_method_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_relationship?(record, serialization_params)
|
||||||
|
if conditional_proc.present?
|
||||||
|
conditional_proc.call(record, serialization_params)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ids_hash_from_record_and_relationship(record, params = {})
|
||||||
|
return ids_hash(
|
||||||
|
fetch_id(record, params)
|
||||||
|
) unless polymorphic
|
||||||
|
|
||||||
|
return unless associated_object = fetch_associated_object(record, params)
|
||||||
|
|
||||||
|
return associated_object.map do |object|
|
||||||
|
id_hash_from_record object, polymorphic
|
||||||
|
end if associated_object.respond_to? :map
|
||||||
|
|
||||||
|
id_hash_from_record associated_object, polymorphic
|
||||||
|
end
|
||||||
|
|
||||||
|
def id_hash_from_record(record, record_types)
|
||||||
|
# memoize the record type within the record_types dictionary, then assigning to record_type:
|
||||||
|
associated_record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
|
||||||
|
id_hash(record.id, associated_record_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ids_hash(ids)
|
||||||
|
return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
|
||||||
|
id_hash(ids, record_type) # ids variable is just a single id here
|
||||||
|
end
|
||||||
|
|
||||||
|
def id_hash(id, record_type, default_return=false)
|
||||||
|
if id.present?
|
||||||
|
{ id: id.to_s, type: record_type }
|
||||||
|
else
|
||||||
|
default_return ? { id: nil, type: record_type } : nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_id(record, params)
|
||||||
|
unless object_block.nil?
|
||||||
|
object = object_block.call(record, params)
|
||||||
|
|
||||||
|
return object.map(&:id) if object.respond_to? :map
|
||||||
|
return object.try(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
record.public_send(id_method_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -34,51 +34,15 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def ids_hash(ids, record_type)
|
|
||||||
return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
|
|
||||||
id_hash(ids, record_type) # ids variable is just a single id here
|
|
||||||
end
|
|
||||||
|
|
||||||
def id_hash_from_record(record, record_types)
|
|
||||||
# memoize the record type within the record_types dictionary, then assigning to record_type:
|
|
||||||
record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
|
|
||||||
id_hash(record.id, record_type)
|
|
||||||
end
|
|
||||||
|
|
||||||
def ids_hash_from_record_and_relationship(record, relationship, params = {})
|
|
||||||
polymorphic = relationship[:polymorphic]
|
|
||||||
|
|
||||||
return ids_hash(
|
|
||||||
fetch_id(record, relationship, params),
|
|
||||||
relationship[:record_type]
|
|
||||||
) unless polymorphic
|
|
||||||
|
|
||||||
return unless associated_object = fetch_associated_object(record, relationship, params)
|
|
||||||
|
|
||||||
return associated_object.map do |object|
|
|
||||||
id_hash_from_record object, polymorphic
|
|
||||||
end if associated_object.respond_to? :map
|
|
||||||
|
|
||||||
id_hash_from_record associated_object, polymorphic
|
|
||||||
end
|
|
||||||
|
|
||||||
def links_hash(record, params = {})
|
def links_hash(record, params = {})
|
||||||
data_links.each_with_object({}) do |(key, method), link_hash|
|
data_links.each_with_object({}) do |(_k, link), hash|
|
||||||
link_hash[key] = if method.is_a?(Proc)
|
link.serialize(record, params, hash)
|
||||||
method.arity == 1 ? method.call(record) : method.call(record, params)
|
|
||||||
else
|
|
||||||
record.public_send(method)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def attributes_hash(record, params = {})
|
def attributes_hash(record, params = {})
|
||||||
attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash|
|
attributes_to_serialize.each_with_object({}) do |(_k, attribute), hash|
|
||||||
attr_hash[key] = if method.is_a?(Proc)
|
attribute.serialize(record, params, hash)
|
||||||
method.arity == 1 ? method.call(record) : method.call(record, params)
|
|
||||||
else
|
|
||||||
record.public_send(method)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -86,11 +50,7 @@ module FastJsonapi
|
|||||||
relationships = relationships_to_serialize if relationships.nil?
|
relationships = relationships_to_serialize if relationships.nil?
|
||||||
|
|
||||||
relationships.each_with_object({}) do |(_k, relationship), hash|
|
relationships.each_with_object({}) do |(_k, relationship), hash|
|
||||||
name = relationship[:key]
|
relationship.serialize(record, params, hash)
|
||||||
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
|
|
||||||
hash[name] = {
|
|
||||||
data: ids_hash_from_record_and_relationship(record, relationship, params) || empty_case
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -147,12 +107,14 @@ module FastJsonapi
|
|||||||
items = parse_include_item(include_item)
|
items = parse_include_item(include_item)
|
||||||
items.each do |item|
|
items.each do |item|
|
||||||
next unless relationships_to_serialize && relationships_to_serialize[item]
|
next unless relationships_to_serialize && relationships_to_serialize[item]
|
||||||
raise NotImplementedError if @relationships_to_serialize[item][:polymorphic].is_a?(Hash)
|
relationship_item = relationships_to_serialize[item]
|
||||||
record_type = @relationships_to_serialize[item][:record_type]
|
next unless relationship_item.include_relationship?(record, params)
|
||||||
serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
|
raise NotImplementedError if relationship_item.polymorphic.is_a?(Hash)
|
||||||
relationship_type = @relationships_to_serialize[item][:relationship_type]
|
record_type = relationship_item.record_type
|
||||||
|
serializer = relationship_item.serializer.to_s.constantize
|
||||||
|
relationship_type = relationship_item.relationship_type
|
||||||
|
|
||||||
included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params)
|
included_objects = relationship_item.fetch_associated_object(record, params)
|
||||||
next if included_objects.blank?
|
next if included_objects.blank?
|
||||||
included_objects = [included_objects] unless relationship_type == :has_many
|
included_objects = [included_objects] unless relationship_type == :has_many
|
||||||
|
|
||||||
@ -171,22 +133,6 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_associated_object(record, relationship, params)
|
|
||||||
return relationship[:object_block].call(record, params) unless relationship[:object_block].nil?
|
|
||||||
record.send(relationship[:object_method_name])
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_id(record, relationship, params)
|
|
||||||
unless relationship[:object_block].nil?
|
|
||||||
object = relationship[:object_block].call(record, params)
|
|
||||||
|
|
||||||
return object.map(&:id) if object.respond_to? :map
|
|
||||||
return object.try(:id)
|
|
||||||
end
|
|
||||||
|
|
||||||
record.public_send(relationship[:id_method_name])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -312,7 +312,6 @@ describe FastJsonapi::ObjectSerializer do
|
|||||||
movie_type_serializer_class.instance_eval do
|
movie_type_serializer_class.instance_eval do
|
||||||
include FastJsonapi::ObjectSerializer
|
include FastJsonapi::ObjectSerializer
|
||||||
set_key_transform key_transform
|
set_key_transform key_transform
|
||||||
set_type :movie_type
|
|
||||||
attributes :name
|
attributes :name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -321,25 +320,25 @@ describe FastJsonapi::ObjectSerializer do
|
|||||||
context 'when key_transform is dash' do
|
context 'when key_transform is dash' do
|
||||||
let(:key_transform) { :dash }
|
let(:key_transform) { :dash }
|
||||||
|
|
||||||
it_behaves_like 'returning key transformed hash', :'movie-type', :'release-year'
|
it_behaves_like 'returning key transformed hash', :'movie-type', :'dash-movie-type', :'release-year'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when key_transform is camel' do
|
context 'when key_transform is camel' do
|
||||||
let(:key_transform) { :camel }
|
let(:key_transform) { :camel }
|
||||||
|
|
||||||
it_behaves_like 'returning key transformed hash', :MovieType, :ReleaseYear
|
it_behaves_like 'returning key transformed hash', :MovieType, :CamelMovieType, :ReleaseYear
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when key_transform is camel_lower' do
|
context 'when key_transform is camel_lower' do
|
||||||
let(:key_transform) { :camel_lower }
|
let(:key_transform) { :camel_lower }
|
||||||
|
|
||||||
it_behaves_like 'returning key transformed hash', :movieType, :releaseYear
|
it_behaves_like 'returning key transformed hash', :movieType, :camelLowerMovieType, :releaseYear
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when key_transform is underscore' do
|
context 'when key_transform is underscore' do
|
||||||
let(:key_transform) { :underscore }
|
let(:key_transform) { :underscore }
|
||||||
|
|
||||||
it_behaves_like 'returning key transformed hash', :movie_type, :release_year
|
it_behaves_like 'returning key transformed hash', :movie_type, :underscore_movie_type, :release_year
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -113,7 +113,7 @@ describe FastJsonapi::ObjectSerializer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'includes child attributes' do
|
it 'includes child attributes' do
|
||||||
expect(EmployeeSerializer.attributes_to_serialize[:location]).to eq(:location)
|
expect(EmployeeSerializer.attributes_to_serialize[:location].method).to eq(:location)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'doesnt change parent class attributes' do
|
it 'doesnt change parent class attributes' do
|
||||||
|
@ -309,4 +309,142 @@ describe FastJsonapi::ObjectSerializer do
|
|||||||
expect(serializable_hash[:included][0][:links][:self]).to eq url
|
expect(serializable_hash[:included][0][:links][:self]).to eq url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when is_collection option present' do
|
||||||
|
subject { MovieSerializer.new(resource, is_collection_options).serializable_hash }
|
||||||
|
|
||||||
|
context 'autodetect' do
|
||||||
|
let(:is_collection_options) { {} }
|
||||||
|
|
||||||
|
context 'collection if no option present' do
|
||||||
|
let(:resource) { [movie] }
|
||||||
|
it { expect(subject[:data]).to be_a(Array) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'single if no option present' do
|
||||||
|
let(:resource) { movie }
|
||||||
|
it { expect(subject[:data]).to be_a(Hash) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'force is_collection to true' do
|
||||||
|
let(:is_collection_options) { { is_collection: true } }
|
||||||
|
|
||||||
|
context 'collection will pass' do
|
||||||
|
let(:resource) { [movie] }
|
||||||
|
it { expect(subject[:data]).to be_a(Array) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'single will raise error' do
|
||||||
|
let(:resource) { movie }
|
||||||
|
it { expect { subject }.to raise_error(NoMethodError, /method(.*)each/) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'force is_collection to false' do
|
||||||
|
let(:is_collection_options) { { is_collection: false } }
|
||||||
|
|
||||||
|
context 'collection will fail without id' do
|
||||||
|
let(:resource) { [movie] }
|
||||||
|
it { expect { subject }.to raise_error(FastJsonapi::MandatoryField, /id is a mandatory field/) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'single will pass' do
|
||||||
|
let(:resource) { movie }
|
||||||
|
it { expect(subject[:data]).to be_a(Hash) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when optional attributes are determined by record data' do
|
||||||
|
it 'returns optional attribute when attribute is included' do
|
||||||
|
movie.release_year = 2001
|
||||||
|
json = MovieOptionalRecordDataSerializer.new(movie).serialized_json
|
||||||
|
serializable_hash = JSON.parse(json)
|
||||||
|
expect(serializable_hash['data']['attributes']['release_year']).to eq movie.release_year
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't return optional attribute when attribute is not included" do
|
||||||
|
movie.release_year = 1970
|
||||||
|
json = MovieOptionalRecordDataSerializer.new(movie).serialized_json
|
||||||
|
serializable_hash = JSON.parse(json)
|
||||||
|
expect(serializable_hash['data']['attributes'].has_key?('release_year')).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when optional attributes are determined by params data' do
|
||||||
|
it 'returns optional attribute when attribute is included' do
|
||||||
|
movie.director = 'steven spielberg'
|
||||||
|
json = MovieOptionalParamsDataSerializer.new(movie, { params: { admin: true }}).serialized_json
|
||||||
|
serializable_hash = JSON.parse(json)
|
||||||
|
expect(serializable_hash['data']['attributes']['director']).to eq 'steven spielberg'
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't return optional attribute when attribute is not included" do
|
||||||
|
movie.director = 'steven spielberg'
|
||||||
|
json = MovieOptionalParamsDataSerializer.new(movie, { params: { admin: false }}).serialized_json
|
||||||
|
serializable_hash = JSON.parse(json)
|
||||||
|
expect(serializable_hash['data']['attributes'].has_key?('director')).to be_falsey
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when optional relationships are determined by record data' do
|
||||||
|
it 'returns optional relationship when relationship is included' do
|
||||||
|
json = MovieOptionalRelationshipSerializer.new(movie).serialized_json
|
||||||
|
serializable_hash = JSON.parse(json)
|
||||||
|
expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when relationship is not included" do
|
||||||
|
let(:json) {
|
||||||
|
MovieOptionalRelationshipSerializer.new(movie, options).serialized_json
|
||||||
|
}
|
||||||
|
let(:options) {
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
let(:serializable_hash) {
|
||||||
|
JSON.parse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "doesn't return optional relationship" do
|
||||||
|
movie.actor_ids = []
|
||||||
|
expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include optional relationship" do
|
||||||
|
movie.actor_ids = []
|
||||||
|
options[:include] = [:actors]
|
||||||
|
expect(serializable_hash['included']).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when optional relationships are determined by params data' do
|
||||||
|
it 'returns optional relationship when relationship is included' do
|
||||||
|
json = MovieOptionalRelationshipWithParamsSerializer.new(movie, { params: { admin: true }}).serialized_json
|
||||||
|
serializable_hash = JSON.parse(json)
|
||||||
|
expect(serializable_hash['data']['relationships'].has_key?('owner')).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when relationship is not included" do
|
||||||
|
let(:json) {
|
||||||
|
MovieOptionalRelationshipWithParamsSerializer.new(movie, options).serialized_json
|
||||||
|
}
|
||||||
|
let(:options) {
|
||||||
|
{ params: { admin: false }}
|
||||||
|
}
|
||||||
|
let(:serializable_hash) {
|
||||||
|
JSON.parse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
it "doesn't return optional relationship" do
|
||||||
|
expect(serializable_hash['data']['relationships'].has_key?('owner')).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include optional relationship" do
|
||||||
|
options[:include] = [:owner]
|
||||||
|
expect(serializable_hash['included']).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -17,31 +17,13 @@ describe FastJsonapi::ObjectSerializer do
|
|||||||
expect(result_hash).to be nil
|
expect(result_hash).to be nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do
|
|
||||||
relationship = { name: :groupees, relationship_type: :has_many, object_method_name: :groupees, polymorphic: {} }
|
|
||||||
results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship
|
|
||||||
expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group })
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns correct hash when ids_hash is called' do
|
|
||||||
inputs = [{ids: %w(1 2 3), record_type: :movie}, {ids: %w(x y z), record_type: 'person'}]
|
|
||||||
inputs.each do |hash|
|
|
||||||
results = MovieSerializer.send(:ids_hash, hash[:ids], hash[:record_type])
|
|
||||||
expect(results.map{|h| h[:id]}).to eq hash[:ids]
|
|
||||||
expect(results[0][:type]).to eq hash[:record_type]
|
|
||||||
end
|
|
||||||
|
|
||||||
result = MovieSerializer.send(:ids_hash, [], 'movie')
|
|
||||||
expect(result).to be_empty
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns correct hash when attributes_hash is called' do
|
it 'returns correct hash when attributes_hash is called' do
|
||||||
attributes_hash = MovieSerializer.send(:attributes_hash, movie)
|
attributes_hash = MovieSerializer.send(:attributes_hash, movie)
|
||||||
attribute_names = attributes_hash.keys.sort
|
attribute_names = attributes_hash.keys.sort
|
||||||
expect(attribute_names).to eq MovieSerializer.attributes_to_serialize.keys.sort
|
expect(attribute_names).to eq MovieSerializer.attributes_to_serialize.keys.sort
|
||||||
MovieSerializer.attributes_to_serialize.each do |key, method_name|
|
MovieSerializer.attributes_to_serialize.each do |key, attribute|
|
||||||
value = attributes_hash[key]
|
value = attributes_hash[key]
|
||||||
expect(value).to eq movie.send(method_name)
|
expect(value).to eq movie.send(attribute.method)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -57,7 +39,7 @@ describe FastJsonapi::ObjectSerializer do
|
|||||||
relationships_hash = MovieSerializer.send(:relationships_hash, movie)
|
relationships_hash = MovieSerializer.send(:relationships_hash, movie)
|
||||||
relationship_names = relationships_hash.keys.sort
|
relationship_names = relationships_hash.keys.sort
|
||||||
relationships_hashes = MovieSerializer.relationships_to_serialize.values
|
relationships_hashes = MovieSerializer.relationships_to_serialize.values
|
||||||
expected_names = relationships_hashes.map{|relationship| relationship[:key]}.sort
|
expected_names = relationships_hashes.map{|relationship| relationship.key}.sort
|
||||||
expect(relationship_names).to eq expected_names
|
expect(relationship_names).to eq expected_names
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -278,6 +278,34 @@ RSpec.shared_context 'movie class' do
|
|||||||
set_type :account
|
set_type :account
|
||||||
belongs_to :supplier
|
belongs_to :supplier
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class MovieOptionalRecordDataSerializer
|
||||||
|
include FastJsonapi::ObjectSerializer
|
||||||
|
set_type :movie
|
||||||
|
attributes :name
|
||||||
|
attribute :release_year, if: Proc.new { |record| record.release_year >= 2000 }
|
||||||
|
end
|
||||||
|
|
||||||
|
class MovieOptionalParamsDataSerializer
|
||||||
|
include FastJsonapi::ObjectSerializer
|
||||||
|
set_type :movie
|
||||||
|
attributes :name
|
||||||
|
attribute :director, if: Proc.new { |record, params| params && params[:admin] == true }
|
||||||
|
end
|
||||||
|
|
||||||
|
class MovieOptionalRelationshipSerializer
|
||||||
|
include FastJsonapi::ObjectSerializer
|
||||||
|
set_type :movie
|
||||||
|
attributes :name
|
||||||
|
has_many :actors, if: Proc.new { |record| record.actors.any? }
|
||||||
|
end
|
||||||
|
|
||||||
|
class MovieOptionalRelationshipWithParamsSerializer
|
||||||
|
include FastJsonapi::ObjectSerializer
|
||||||
|
set_type :movie
|
||||||
|
attributes :name
|
||||||
|
belongs_to :owner, record_type: :user, if: Proc.new { |record, params| params && params[:admin] == true }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
RSpec.shared_examples 'returning correct relationship hash' do |serializer, id_method_name, record_type|
|
RSpec.shared_examples 'returning correct relationship hash' do |serializer, id_method_name, record_type|
|
||||||
it 'returns correct relationship hash' do
|
it 'returns correct relationship hash' do
|
||||||
expect(relationship).to be_instance_of(Hash)
|
expect(relationship).to be_instance_of(FastJsonapi::Relationship)
|
||||||
expect(relationship.keys).to all(be_instance_of(Symbol))
|
# expect(relationship.keys).to all(be_instance_of(Symbol))
|
||||||
expect(relationship[:serializer]).to be serializer
|
expect(relationship.serializer).to be serializer
|
||||||
expect(relationship[:id_method_name]).to be id_method_name
|
expect(relationship.id_method_name).to be id_method_name
|
||||||
expect(relationship[:record_type]).to be record_type
|
expect(relationship.record_type).to be record_type
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.shared_examples 'returning key transformed hash' do |movie_type, release_year|
|
RSpec.shared_examples 'returning key transformed hash' do |movie_type, serializer_type, release_year|
|
||||||
it 'returns correctly transformed hash' do
|
it 'returns correctly transformed hash' do
|
||||||
expect(hash[:data][0][:attributes]).to have_key(release_year)
|
expect(hash[:data][0][:attributes]).to have_key(release_year)
|
||||||
expect(hash[:data][0][:relationships]).to have_key(movie_type)
|
expect(hash[:data][0][:relationships]).to have_key(movie_type)
|
||||||
expect(hash[:data][0][:relationships][movie_type][:data][:type]).to eq(movie_type)
|
expect(hash[:data][0][:relationships][movie_type][:data][:type]).to eq(movie_type)
|
||||||
expect(hash[:included][0][:type]).to eq(movie_type)
|
expect(hash[:included][0][:type]).to eq(serializer_type)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user