Merge pull request #273 from Netflix/release-1.3

Release 1.3
This commit is contained in:
Shishir Kakaraddi 2018-07-16 22:01:17 -07:00 committed by GitHub
commit 5ff3fa97da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 646 additions and 163 deletions

View File

@ -30,6 +30,9 @@ Fast JSON API serialized 250 records in 3.01 ms
* [Collection Serialization](#collection-serialization)
* [Caching](#caching)
* [Params](#params)
* [Conditional Attributes](#conditional-attributes)
* [Conditional Relationships](#conditional-relationships)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Contributing](#contributing)
@ -205,6 +208,18 @@ class MovieSerializer
end
```
Attributes can also use a different name by passing the original method or accessor with a proc shortcut:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
attributes :name
attribute :released_in_year, &:year
end
```
### Links Per Object
Links are defined in FastJsonapi using the `link` method. By default, link are read directly from the model property of the same name.In this example, `public_url` is expected to be a property of the object being serialized.
@ -259,6 +274,26 @@ hash = MovieSerializer.new([movie, movie], options).serializable_hash
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
Requires a `cache_key` method be defined on model:
@ -284,7 +319,6 @@ block you opt-in to using params by adding it as a block parameter.
```ruby
class MovieSerializer
class MovieSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :year
@ -308,6 +342,68 @@ serializer.serializable_hash
Custom attributes and relationships that only receive the resource are still possible by defining
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
```
### Sparse Fieldsets
Attributes and relationships can be selectively returned per record type by using the `fields` option.
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :year
end
serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } })
serializer.serializable_hash
```
### Customizable Options
Option | Purpose | Example

View File

@ -1,20 +1,18 @@
# frozen_string_literal: true
if defined?(::ActiveRecord)
::ActiveRecord::Associations::Builder::HasOne.class_eval do
# 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/singular_association.rb#L11
def self.define_accessors(mixin, reflection)
super
name = reflection.name
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_id
# if an attribute is already defined with this methods name we should just use it
return read_attribute(__method__) if has_attribute?(__method__)
association(:#{name}).reader.try(:id)
end
CODE
end
::ActiveRecord::Associations::Builder::HasOne.class_eval do
# 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/singular_association.rb#L11
def self.define_accessors(mixin, reflection)
super
name = reflection.name
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_id
# if an attribute is already defined with this methods name we should just use it
return read_attribute(__method__) if has_attribute?(__method__)
association(:#{name}).reader.try(:id)
end
CODE
end
end

View File

@ -2,5 +2,9 @@
module FastJsonapi
require 'fast_jsonapi/object_serializer'
require 'extensions/has_one'
if defined?(::Rails)
require 'fast_jsonapi/railtie'
elsif defined?(::ActiveRecord)
require 'extensions/has_one'
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.abs == 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/concern'
require 'active_support/inflector'
require 'fast_jsonapi/attribute'
require 'fast_jsonapi/relationship'
require 'fast_jsonapi/link'
require 'fast_jsonapi/serialization_core'
module FastJsonapi
@ -25,7 +28,7 @@ module FastJsonapi
end
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
end
@ -38,8 +41,8 @@ module FastJsonapi
return serializable_hash unless @resource
serializable_hash[:data] = self.class.record_hash(@resource, @params)
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @params) if @includes.present?
serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @params)
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
serializable_hash
end
@ -48,9 +51,10 @@ module FastJsonapi
data = []
included = []
fieldset = @fieldsets[self.class.record_type.to_sym]
@resource.each do |record|
data << self.class.record_hash(record, @params)
included.concat self.class.get_included_records(record, @includes, @known_included_objects, @params) if @includes.present?
data << self.class.record_hash(record, fieldset, @params)
included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
end
serializable_hash[:data] = data
@ -67,11 +71,14 @@ module FastJsonapi
private
def process_options(options)
@fieldsets = deep_symbolize(options[:fields].presence || {})
return if options.blank?
@known_included_objects = {}
@meta = options[:meta]
@links = options[:links]
@is_collection = options[:is_collection]
@params = options[:params] || {}
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
@ -81,8 +88,22 @@ module FastJsonapi
end
end
def is_collection?(resource)
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
def deep_symbolize(collection)
if collection.is_a? Hash
Hash[collection.map do |k, v|
[k.to_sym, deep_symbolize(v)]
end]
elsif collection.is_a? Array
collection.map { |i| deep_symbolize(i) }
else
collection.to_sym
end
end
def is_collection?(resource, force_is_collection = nil)
return force_is_collection unless force_is_collection.nil?
resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
end
class_methods do
@ -98,6 +119,7 @@ module FastJsonapi
subclass.race_condition_ttl = race_condition_ttl
subclass.data_links = data_links
subclass.cached = cached
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
end
def reflected_record_type
@ -118,6 +140,9 @@ module FastJsonapi
underscore: :underscore
}
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
def run_key_transform(input)
@ -149,48 +174,51 @@ module FastJsonapi
def attributes(*attributes_list, &block)
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?
attributes_list.each do |attr_name|
method_name = attr_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
alias_method :attribute, :attributes
def add_relationship(name, relationship)
def add_relationship(relationship)
self.relationships_to_serialize = {} if 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?
if !relationship[:cached]
self.uncachable_relationships_to_serialize[name] = relationship
if !relationship.cached
self.uncachable_relationships_to_serialize[relationship.name] = relationship
else
self.cachable_relationships_to_serialize[name] = relationship
self.cachable_relationships_to_serialize[relationship.name] = relationship
end
self.relationships_to_serialize[name] = relationship
end
self.relationships_to_serialize[relationship.name] = relationship
end
def has_many(relationship_name, options = {}, &block)
name = relationship_name.to_sym
hash = create_relationship_hash(relationship_name, :has_many, options, block)
add_relationship(name, hash)
relationship = create_relationship(relationship_name, :has_many, options, block)
add_relationship(relationship)
end
def has_one(relationship_name, options = {}, &block)
name = relationship_name.to_sym
hash = create_relationship_hash(relationship_name, :has_one, options, block)
add_relationship(name, hash)
relationship = create_relationship(relationship_name, :has_one, options, block)
add_relationship(relationship)
end
def belongs_to(relationship_name, options = {}, &block)
name = relationship_name.to_sym
hash = create_relationship_hash(relationship_name, :belongs_to, options, block)
add_relationship(name, hash)
relationship = create_relationship(relationship_name, :belongs_to, options, block)
add_relationship(relationship)
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
if relationship_type == :has_many
base_serialization_key = base_key.to_s.singularize
@ -201,7 +229,7 @@ module FastJsonapi
base_key_sym = name
id_postfix = '_id'
end
{
Relationship.new(
key: options[:key] || run_key_transform(base_key),
name: name,
id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
@ -210,9 +238,10 @@ module FastJsonapi
object_block: block,
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
relationship_type: relationship_type,
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
}
cached: options[:cached],
polymorphic: fetch_polymorphic_option(options),
conditional_proc: options[:if]
)
end
def compute_serializer_name(serializer_key)
@ -233,7 +262,11 @@ module FastJsonapi
self.data_links = {} if self.data_links.nil?
link_method_name = link_name if link_method_name.nil?
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
def validate_includes!(includes)
@ -244,8 +277,8 @@ module FastJsonapi
parse_include_item(include_item).each do |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 NotImplementedError if relationship_to_include[:polymorphic].is_a?(Hash)
klass = relationship_to_include[:serializer].to_s.constantize
raise NotImplementedError if relationship_to_include.polymorphic.is_a?(Hash)
klass = relationship_to_include.serializer.to_s.constantize
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,73 +34,36 @@ module FastJsonapi
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 = {})
data_links.each_with_object({}) do |(key, method), link_hash|
link_hash[key] = if method.is_a?(Proc)
method.arity == 1 ? method.call(record) : method.call(record, params)
else
record.public_send(method)
end
data_links.each_with_object({}) do |(_k, link), hash|
link.serialize(record, params, hash)
end
end
def attributes_hash(record, params = {})
attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash|
attr_hash[key] = if method.is_a?(Proc)
method.arity == 1 ? method.call(record) : method.call(record, params)
else
record.public_send(method)
end
def attributes_hash(record, fieldset = nil, params = {})
attributes = attributes_to_serialize
attributes = attributes.slice(*fieldset) if fieldset.present?
attributes.each_with_object({}) do |(_k, attribute), hash|
attribute.serialize(record, params, hash)
end
end
def relationships_hash(record, relationships = nil, params = {})
def relationships_hash(record, relationships = nil, fieldset = nil, params = {})
relationships = relationships_to_serialize if relationships.nil?
relationships = relationships.slice(*fieldset) if fieldset.present?
relationships.each_with_object({}) do |(_k, relationship), hash|
name = relationship[:key]
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
hash[name] = {
data: ids_hash_from_record_and_relationship(record, relationship, params) || empty_case
}
relationship.serialize(record, params, hash)
end
end
def record_hash(record, params = {})
def record_hash(record, fieldset, params = {})
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
temp_hash = id_hash(id_from_record(record), record_type, true)
temp_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present?
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
temp_hash[:relationships] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, params) if cachable_relationships_to_serialize.present?
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, params) if cachable_relationships_to_serialize.present?
temp_hash[:links] = links_hash(record, params) if data_links.present?
temp_hash
end
@ -108,8 +71,8 @@ module FastJsonapi
record_hash
else
record_hash = id_hash(id_from_record(record), record_type, true)
record_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, params) if relationships_to_serialize.present?
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, fieldset, params) if relationships_to_serialize.present?
record_hash[:links] = links_hash(record, params) if data_links.present?
record_hash
end
@ -140,25 +103,27 @@ module FastJsonapi
end
# includes handler
def get_included_records(record, includes_list, known_included_objects, params = {})
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
return unless includes_list.present?
includes_list.sort.each_with_object([]) do |include_item, included_records|
items = parse_include_item(include_item)
items.each do |item|
next unless relationships_to_serialize && relationships_to_serialize[item]
raise NotImplementedError if @relationships_to_serialize[item][:polymorphic].is_a?(Hash)
record_type = @relationships_to_serialize[item][:record_type]
serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
relationship_type = @relationships_to_serialize[item][:relationship_type]
relationship_item = relationships_to_serialize[item]
next unless relationship_item.include_relationship?(record, params)
raise NotImplementedError if relationship_item.polymorphic.is_a?(Hash)
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?
included_objects = [included_objects] unless relationship_type == :has_many
included_objects.each do |inc_obj|
if remaining_items(items)
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects)
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets)
included_records.concat(serializer_records) unless serializer_records.empty?
end
@ -166,27 +131,12 @@ module FastJsonapi
next if known_included_objects.key?(code)
known_included_objects[code] = inc_obj
included_records << serializer.record_hash(inc_obj, params)
included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], params)
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.id
end
record.public_send(relationship[:id_method_name])
end
end
end
end

View File

@ -1,3 +1,3 @@
module FastJsonapi
VERSION = "1.2"
VERSION = "1.3"
end

View File

@ -230,6 +230,23 @@ describe FastJsonapi::ObjectSerializer do
expect(serializable_hash[:data][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})"
end
end
context 'with &:proc' do
before do
movie.release_year = 2008
MovieSerializer.attribute :released_in_year, &:release_year
MovieSerializer.attribute :name, &:local_name
end
after do
MovieSerializer.attributes_to_serialize.delete(:released_in_year)
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:attributes][:name]).to eq "english #{movie.name}"
expect(serializable_hash[:data][:attributes][:released_in_year]).to eq movie.release_year
end
end
end
describe '#link' do
@ -312,7 +329,6 @@ describe FastJsonapi::ObjectSerializer do
movie_type_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_key_transform key_transform
set_type :movie_type
attributes :name
end
end
@ -321,25 +337,25 @@ describe FastJsonapi::ObjectSerializer do
context 'when key_transform is dash' do
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
context 'when key_transform is camel' do
let(:key_transform) { :camel }
it_behaves_like 'returning key transformed hash', :MovieType, :ReleaseYear
it_behaves_like 'returning key transformed hash', :MovieType, :CamelMovieType, :ReleaseYear
end
context 'when key_transform is camel_lower' do
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
context 'when key_transform is underscore' do
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

View File

@ -0,0 +1,48 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
let(:fields) do
{
movie: %i[name actors advertising_campaign],
actor: %i[name agency]
}
end
it 'only returns specified fields' do
hash = MovieSerializer.new(movie, fields: fields).serializable_hash
expect(hash[:data][:attributes].keys.sort).to eq %i[name]
end
it 'only returns specified relationships' do
hash = MovieSerializer.new(movie, fields: fields).serializable_hash
expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign]
end
it 'only returns specified fields for included relationships' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash
expect(hash[:included].first[:attributes].keys.sort).to eq %i[name]
end
it 'only returns specified relationships for included relationships' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash
expect(hash[:included].first[:relationships].keys.sort).to eq %i[agency]
end
it 'returns all fields for included relationships when no explicit fields have been specified' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash
expect(hash[:included][3][:attributes].keys.sort).to eq %i[id name]
end
it 'returns all fields for included relationships when no explicit fields have been specified' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash
expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie]
end
end

View File

@ -95,6 +95,11 @@ describe FastJsonapi::ObjectSerializer do
has_one :account
end
it 'sets the correct record type' do
expect(EmployeeSerializer.reflected_record_type).to eq :employee
expect(EmployeeSerializer.record_type).to eq :employee
end
context 'when testing inheritance of attributes' do
it 'includes parent attributes' do
@ -113,7 +118,7 @@ describe FastJsonapi::ObjectSerializer do
end
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
it 'doesnt change parent class attributes' do

View File

@ -97,6 +97,13 @@ describe FastJsonapi::ObjectSerializer do
expect(serializable_hash['data']['relationships']['owner']['data']).to be nil
end
it 'returns correct json when belongs_to returns nil and there is a block for the relationship' do
movie.owner_id = nil
json = MovieSerializer.new(movie, {include: [:owner]}).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['owner']['data']).to be nil
end
it 'returns correct json when has_one returns nil' do
supplier.account_id = nil
json = SupplierSerializer.new(supplier).serialized_json
@ -302,4 +309,142 @@ describe FastJsonapi::ObjectSerializer do
expect(serializable_hash[:included][0][:links][:self]).to eq url
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

View File

@ -17,31 +17,13 @@ describe FastJsonapi::ObjectSerializer do
expect(result_hash).to be nil
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
attributes_hash = MovieSerializer.send(:attributes_hash, movie)
attribute_names = attributes_hash.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]
expect(value).to eq movie.send(method_name)
expect(value).to eq movie.send(attribute.method)
end
end
@ -57,7 +39,7 @@ describe FastJsonapi::ObjectSerializer do
relationships_hash = MovieSerializer.send(:relationships_hash, movie)
relationship_names = relationships_hash.keys.sort
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
end
@ -82,7 +64,7 @@ describe FastJsonapi::ObjectSerializer do
known_included_objects = {}
included_records = []
[movie, movie].each do |record|
included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects, nil)
included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects, {}, nil)
end
expect(included_records.size).to eq 3
end

View File

@ -43,10 +43,21 @@ RSpec.shared_context 'movie class' do
ac
end
def owner
return unless owner_id
ow = Owner.new
ow.id = owner_id
ow
end
def cache_key
"#{id}"
end
def local_name(locale = :english)
"#{locale} #{name}"
end
def url
"http://movies.com/#{id}"
end
@ -146,6 +157,14 @@ RSpec.shared_context 'movie class' do
attr_accessor :id
end
class Owner
attr_accessor :id
end
class OwnerSerializer
include FastJsonapi::ObjectSerializer
end
# serializers
class MovieSerializer
include FastJsonapi::ObjectSerializer
@ -153,7 +172,9 @@ RSpec.shared_context 'movie class' do
# director attr is not mentioned intentionally
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :owner, record_type: :user do |object, params|
object.owner
end
belongs_to :movie_type
has_one :advertising_campaign
end
@ -261,6 +282,34 @@ RSpec.shared_context 'movie class' do
set_type :account
belongs_to :supplier
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

View File

@ -1,18 +1,18 @@
RSpec.shared_examples 'returning correct relationship hash' do |serializer, id_method_name, record_type|
it 'returns correct relationship hash' do
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:serializer]).to be serializer
expect(relationship[:id_method_name]).to be id_method_name
expect(relationship[:record_type]).to be record_type
expect(relationship).to be_instance_of(FastJsonapi::Relationship)
# expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship.serializer).to be serializer
expect(relationship.id_method_name).to be id_method_name
expect(relationship.record_type).to be record_type
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
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][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