Merge pull request #265 from Netflix/dev

Dev
This commit is contained in:
Shishir Kakaraddi 2018-07-03 21:14:18 -07:00 committed by GitHub
commit 49193ab8f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 483 additions and 145 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View 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
View 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

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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