Merge pull request #18 from fast-jsonapi/dev

1.6.0
This commit is contained in:
Kevin Pheasey 2019-11-04 17:38:13 -05:00 committed by GitHub
commit 6a08165347
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 304 additions and 115 deletions

View File

@ -4,5 +4,12 @@ rvm:
- 2.3.6 - 2.3.6
- 2.4.3 - 2.4.3
- 2.5.0 - 2.5.0
- 2.6
before_install:
- "travis_retry gem update --system 2.7.9"
- "travis_retry gem install bundler -v '1.17.3'"
install: BUNDLER_VERSION=1.17.3 bundle install --path=vendor/bundle --retry=3 --jobs=3
script: script:
- bundle exec rspec - bundle exec rspec

View File

@ -4,13 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [1.6.0] - 2019-11-04
### Added ### Added
- Allow relationship links to be delcared as a method ([#2](https://github.com/fast-jsonapi/fast_jsonapi/pull/2)) - Allow relationship links to be delcared as a method ([#2](https://github.com/fast-jsonapi/fast_jsonapi/pull/2))
- Test against Ruby 2.6 ([#1](https://github.com/fast-jsonapi/fast_jsonapi/pull/1)) - Test against Ruby 2.6 ([#1](https://github.com/fast-jsonapi/fast_jsonapi/pull/1))
- Include `data` key when lazy-loaded relationships are included ([#10](https://github.com/fast-jsonapi/fast_jsonapi/pull/10))
- Conditional links [#15](https://github.com/fast-jsonapi/fast_jsonapi/pull/15)
- Include params on set_id block [#16](https://github.com/fast-jsonapi/fast_jsonapi/pull/16)
### Changed ### Changed
- Optimize SerializationCore.get_included_records calculates remaining_items only once ([#4](https://github.com/fast-jsonapi/fast_jsonapi/pull/4)) - Optimize SerializationCore.get_included_records calculates remaining_items only once ([#4](https://github.com/fast-jsonapi/fast_jsonapi/pull/4))
- Optimize SerializtionCore.parse_include_item by mapping in place ([#5](https://github.com/fast-jsonapi/fast_jsonapi/pull/5)) - Optimize SerializtionCore.parse_include_item by mapping in place ([#5](https://github.com/fast-jsonapi/fast_jsonapi/pull/5))
- Define ObjectSerializer.set_key_transform mapping as a constant ([#7](https://github.com/fast-jsonapi/fast_jsonapi/pull/7)) - Define ObjectSerializer.set_key_transform mapping as a constant ([#7](https://github.com/fast-jsonapi/fast_jsonapi/pull/7))
- Optimize SerializtionCore.remaining_items by taking from original array ([#9](https://github.com/fast-jsonapi/fast_jsonapi/pull/9))
- Optimize ObjectSerializer.deep_symbolize by using each_with_object instead of Hash[map] ([#6](https://github.com/fast-jsonapi/fast_jsonapi/pull/6))
[Unreleased]: https://github.com/fast-jsonapi/fast_jsonapi/compare/dev...HEAD [Unreleased]: https://github.com/fast-jsonapi/fast_jsonapi/compare/dev...HEAD
[1.6.0]: https://github.com/fast-jsonapi/fast_jsonapi/compare/1.5...1.6.0

View File

@ -4,6 +4,10 @@
A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects. A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.
Note: this gem deals only with implementing the JSON:API spec. If your API
responses are not formatted according to the JSON:API spec, this library will
not work for you.
# Performance Comparison # Performance Comparison
We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the [performance document](https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md) for any questions related to methodology. We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the [performance document](https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md) for any questions related to methodology.
@ -101,6 +105,17 @@ movie.actor_ids = [1, 2, 3]
movie.owner_id = 3 movie.owner_id = 3
movie.movie_type_id = 1 movie.movie_type_id = 1
movie movie
movies =
2.times.map do |i|
m = Movie.new
m.id = i + 1
m.name = "test movie #{i}"
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
``` ```
### Object Serialization ### Object Serialization
@ -263,6 +278,12 @@ class MovieSerializer
end end
``` ```
Relationship links can also be configured to be defined as a method on the object.
```ruby
has_many :actors, links: :actor_relationship_links
```
This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the `lazy_load_data` option: This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the `lazy_load_data` option:
```ruby ```ruby
@ -304,7 +325,7 @@ options[:links] = {
prev: '...' prev: '...'
} }
options[:include] = [:actors, :'actors.agency', :'actors.agency.state'] options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new([movie, movie], options).serialized_json MovieSerializer.new(movies, options).serialized_json
``` ```
### Collection Serialization ### Collection Serialization
@ -316,15 +337,15 @@ options[:links] = {
next: '...', next: '...',
prev: '...' prev: '...'
} }
hash = MovieSerializer.new([movie, movie], options).serializable_hash hash = MovieSerializer.new(movies, options).serializable_hash
json_string = MovieSerializer.new([movie, movie], options).serialized_json json_string = MovieSerializer.new(movies, options).serialized_json
``` ```
#### Control Over Collection Serialization #### Control Over Collection Serialization
You can use `is_collection` option to have better 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 this option is not provided or `nil` autodetect logic is used to try understand
if provided resource is a single object or collection. if provided resource is a single object or collection.
Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but
@ -360,13 +381,19 @@ related to a current authenticated user. The `options[:params]` value covers the
cases by allowing you to pass in a hash of additional parameters necessary for cases by allowing you to pass in a hash of additional parameters necessary for
your use case. your use case.
Leveraging the new params is easy, when you define a custom attribute or relationship with a Leveraging the new params is easy, when you define a custom id, attribute or
block you opt-in to using params by adding it as a block parameter. relationship with a block you opt-in to using params by adding it as a block
parameter.
```ruby ```ruby
class MovieSerializer class MovieSerializer
include FastJsonapi::ObjectSerializer include FastJsonapi::ObjectSerializer
set_id do |movie, params|
# in here, params is a hash containing the `:admin` key
params[:admin] ? movie.owner_id : "movie-#{movie.id}"
end
attributes :name, :year attributes :name, :year
attribute :can_view_early do |movie, params| attribute :can_view_early do |movie, params|
# in here, params is a hash containing the `:current_user` key # in here, params is a hash containing the `:current_user` key
@ -518,7 +545,7 @@ Option | Purpose | Example
------------ | ------------- | ------------- ------------ | ------------- | -------------
set_type | Type name of Object | ```set_type :movie ``` set_type | Type name of Object | ```set_type :movie ```
key | Key of Object | ```belongs_to :owner, key: :user ``` key | Key of Object | ```belongs_to :owner, key: :user ```
set_id | ID of Object | ```set_id :owner_id ``` or ```set_id { |record| "#{record.name.downcase}-#{record.id}" }``` set_id | ID of Object | ```set_id :owner_id ``` or ```set_id { \|record, params\| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" }```
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds``` cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds```
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | ```has_many :locations, id_method_name: :place_ids ``` id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | ```has_many :locations, id_method_name: :place_ids ```
object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ``` object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ```
@ -541,7 +568,7 @@ Skylight relies on `ActiveSupport::Notifications` to track these two core method
require 'fast_jsonapi/instrumentation' require 'fast_jsonapi/instrumentation'
``` ```
The two instrumented notifcations are supplied by these two constants: The two instrumented notifications are supplied by these two constants:
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION` * `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION` * `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`

View File

@ -1,29 +1,5 @@
require 'fast_jsonapi/scalar'
module FastJsonapi module FastJsonapi
class Attribute class Attribute < Scalar; end
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 end

View File

@ -1,18 +1,5 @@
require 'fast_jsonapi/scalar'
module FastJsonapi module FastJsonapi
class Link class Link < Scalar; end
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 end

View File

@ -17,6 +17,12 @@ module FastJsonapi
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash' SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json' SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
TRANSFORMS_MAPPING = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}.freeze
included do included do
# Set record_type based on the name of the serializer class # Set record_type based on the name of the serializer class
@ -43,7 +49,7 @@ module FastJsonapi
return serializable_hash unless @resource return serializable_hash unless @resource
serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @params) serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @includes, @params)
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
serializable_hash serializable_hash
end end
@ -55,7 +61,7 @@ module FastJsonapi
included = [] included = []
fieldset = @fieldsets[self.class.record_type.to_sym] fieldset = @fieldsets[self.class.record_type.to_sym]
@resource.each do |record| @resource.each do |record|
data << self.class.record_hash(record, fieldset, @params) data << self.class.record_hash(record, fieldset, @includes, @params)
included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
end end
@ -86,16 +92,16 @@ module FastJsonapi
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)
if options[:include].present? if options[:include].present?
@includes = options[:include].delete_if(&:blank?).map(&:to_sym) @includes = options[:include].reject(&:blank?).map(&:to_sym)
self.class.validate_includes!(@includes) self.class.validate_includes!(@includes)
end end
end end
def deep_symbolize(collection) def deep_symbolize(collection)
if collection.is_a? Hash if collection.is_a? Hash
Hash[collection.map do |k, v| collection.each_with_object({}) do |(k, v), hsh|
[k.to_sym, deep_symbolize(v)] hsh[k.to_sym] = deep_symbolize(v)
end] end
elsif collection.is_a? Array elsif collection.is_a? Array
collection.map { |i| deep_symbolize(i) } collection.map { |i| deep_symbolize(i) }
else else
@ -130,20 +136,14 @@ module FastJsonapi
return @reflected_record_type if defined?(@reflected_record_type) return @reflected_record_type if defined?(@reflected_record_type)
@reflected_record_type ||= begin @reflected_record_type ||= begin
if self.name.end_with?('Serializer') if self.name && self.name.end_with?('Serializer')
self.name.split('::').last.chomp('Serializer').underscore.to_sym self.name.split('::').last.chomp('Serializer').underscore.to_sym
end end
end end
end end
def set_key_transform(transform_name) def set_key_transform(transform_name)
mapping = { self.transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}
self.transform_method = mapping[transform_name.to_sym]
# ensure that the record type is correctly transformed # ensure that the record type is correctly transformed
if record_type if record_type
@ -285,21 +285,26 @@ module FastJsonapi
{} {}
end end
def link(link_name, link_method_name = nil, &block) # def link(link_name, link_method_name = nil, &block)
def link(*params, &block)
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?
options = params.last.is_a?(Hash) ? params.pop : {}
link_name = params.first
link_method_name = params[-1]
key = run_key_transform(link_name) key = run_key_transform(link_name)
self.data_links[key] = Link.new( self.data_links[key] = Link.new(
key: key, key: key,
method: block || link_method_name method: block || link_method_name,
options: options
) )
end end
def validate_includes!(includes) def validate_includes!(includes)
return if includes.blank? return if includes.blank?
includes.detect do |include_item| includes.each do |include_item|
klass = self klass = self
parse_include_item(include_item).each do |parsed_include| parse_include_item(include_item).each do |parsed_include|
relationships_to_serialize = klass.relationships_to_serialize || {} relationships_to_serialize = klass.relationships_to_serialize || {}

View File

@ -34,12 +34,12 @@ module FastJsonapi
@lazy_load_data = lazy_load_data @lazy_load_data = lazy_load_data
end end
def serialize(record, serialization_params, output_hash) def serialize(record, included, serialization_params, output_hash)
if include_relationship?(record, serialization_params) if include_relationship?(record, serialization_params)
empty_case = relationship_type == :has_many ? [] : nil empty_case = relationship_type == :has_many ? [] : nil
output_hash[key] = {} output_hash[key] = {}
unless lazy_load_data unless (lazy_load_data && !included)
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
end end
add_links_hash(record, serialization_params, output_hash) if links.present? add_links_hash(record, serialization_params, output_hash) if links.present?
@ -104,8 +104,12 @@ module FastJsonapi
end end
def add_links_hash(record, params, output_hash) def add_links_hash(record, params, output_hash)
output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash| if links.is_a?(Symbol)
Link.new(key: key, method: method).serialize(record, params, hash)\ output_hash[key][:links] = record.public_send(links)
else
output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash)\
end
end end
end end

View File

@ -0,0 +1,29 @@
module FastJsonapi
class Scalar
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 conditionally_allowed?(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 conditionally_allowed?(record, serialization_params)
if conditional_proc.present?
conditional_proc.call(record, serialization_params)
else
true
end
end
end
end

View File

@ -44,17 +44,21 @@ module FastJsonapi
def attributes_hash(record, fieldset = nil, params = {}) def attributes_hash(record, fieldset = nil, params = {})
attributes = attributes_to_serialize attributes = attributes_to_serialize
attributes = attributes.slice(*fieldset) if fieldset.present? attributes = attributes.slice(*fieldset) if fieldset.present?
attributes = {} if fieldset == []
attributes.each_with_object({}) do |(_k, attribute), hash| attributes.each_with_object({}) do |(_k, attribute), hash|
attribute.serialize(record, params, hash) attribute.serialize(record, params, hash)
end end
end end
def relationships_hash(record, relationships = nil, fieldset = nil, params = {}) def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
relationships = relationships_to_serialize if relationships.nil? relationships = relationships_to_serialize if relationships.nil?
relationships = relationships.slice(*fieldset) if fieldset.present? relationships = relationships.slice(*fieldset) if fieldset.present?
relationships = {} if fieldset == []
relationships.each_with_object({}) do |(_k, relationship), hash| relationships.each_with_object({}) do |(key, relationship), hash|
relationship.serialize(record, params, hash) included = includes_list.present? && includes_list.include?(key)
relationship.serialize(record, included, params, hash)
end end
end end
@ -62,31 +66,31 @@ module FastJsonapi
meta_to_serialize.call(record, params) meta_to_serialize.call(record, params)
end end
def record_hash(record, fieldset, params = {}) def record_hash(record, fieldset, includes_list, params = {})
if cached if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do 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 = id_hash(id_from_record(record, params), record_type, true)
temp_hash[:attributes] = attributes_hash(record, fieldset, 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] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, params) if cachable_relationships_to_serialize.present? temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
temp_hash[:links] = links_hash(record, params) if data_links.present? temp_hash[:links] = links_hash(record, params) if data_links.present?
temp_hash temp_hash
end end
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, params)) if uncachable_relationships_to_serialize.present? record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present? record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash record_hash
else else
record_hash = id_hash(id_from_record(record), record_type, true) record_hash = id_hash(id_from_record(record, params), record_type, true)
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_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[:relationships] = relationships_hash(record, nil, fieldset, includes_list, params) if relationships_to_serialize.present?
record_hash[:links] = links_hash(record, params) if data_links.present? record_hash[:links] = links_hash(record, params) if data_links.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present? record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash record_hash
end end
end end
def id_from_record(record) def id_from_record(record, params)
return record_id.call(record) if record_id.is_a?(Proc) return record_id.call(record, params) if record_id.is_a?(Proc)
return record.send(record_id) if record_id return record.send(record_id) if record_id
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id) raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
record.id record.id
@ -99,15 +103,14 @@ module FastJsonapi
def parse_include_item(include_item) def parse_include_item(include_item)
return [include_item.to_sym] unless include_item.to_s.include?('.') return [include_item.to_sym] unless include_item.to_s.include?('.')
include_item.to_s.split('.').map { |item| item.to_sym }
include_item.to_s.split('.').map!(&:to_sym)
end end
def remaining_items(items) def remaining_items(items)
return unless items.size > 1 return unless items.size > 1
items_copy = items.dup [items[1..-1].join('.').to_sym]
items_copy.delete_at(0)
[items_copy.join('.').to_sym]
end end
# includes handler # includes handler
@ -116,6 +119,8 @@ module FastJsonapi
includes_list.sort.each_with_object([]) do |include_item, included_records| includes_list.sort.each_with_object([]) do |include_item, included_records|
items = parse_include_item(include_item) items = parse_include_item(include_item)
remaining_items = remaining_items(items)
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]
relationship_item = relationships_to_serialize[item] relationship_item = relationships_to_serialize[item]
@ -136,17 +141,17 @@ module FastJsonapi
serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize
end end
if remaining_items(items) if remaining_items.present?
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets, params) serializer_records = serializer.get_included_records(inc_obj, remaining_items, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty? included_records.concat(serializer_records) unless serializer_records.empty?
end end
code = "#{record_type}_#{serializer.id_from_record(inc_obj)}" code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
next if known_included_objects.key?(code) next if known_included_objects.key?(code)
known_included_objects[code] = inc_obj known_included_objects[code] = inc_obj
included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], params) included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], includes_list, params)
end end
end end
end end

View File

@ -1,3 +1,3 @@
module FastJsonapi module FastJsonapi
VERSION = "1.5" VERSION = '1.6.0'
end end

View File

@ -10,7 +10,8 @@ describe FastJsonapi::ObjectSerializer do
options[:meta] = { total: 2 } options[:meta] = { total: 2 }
options[:include] = [:actors] options[:include] = [:actors]
@serializer = MovieSerializer.new([movie, movie], options) movies = build_movies(2)
@serializer = MovieSerializer.new(movies, options)
end end
context 'serializable_hash' do context 'serializable_hash' do

View File

@ -24,7 +24,8 @@ describe FastJsonapi::ObjectSerializer do
options[:meta] = { total: 2 } options[:meta] = { total: 2 }
options[:include] = [:actors] options[:include] = [:actors]
@serializer = MovieSerializer.new([movie, movie], options) movies = build_movies(2)
@serializer = MovieSerializer.new(movies, options)
end end
context 'serializable_hash' do context 'serializable_hash' do

View File

@ -16,7 +16,8 @@ describe FastJsonapi::ObjectSerializer do
options[:links] = { self: 'self' } options[:links] = { self: 'self' }
options[:include] = [:actors] options[:include] = [:actors]
serializable_hash = CachingMovieSerializer.new([movie, movie], options).serializable_hash movies = build_movies(2)
serializable_hash = CachingMovieSerializer.new(movies, options).serializable_hash
expect(serializable_hash[:data].length).to eq 2 expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3 expect(serializable_hash[:data][0][:relationships].length).to eq 3

View File

@ -193,7 +193,10 @@ describe FastJsonapi::ObjectSerializer do
end end
describe '#set_id' do describe '#set_id' do
subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash } let(:params) { {} }
subject(:serializable_hash) do
MovieSerializer.new(resource, { params: params }).serializable_hash
end
context 'method name' do context 'method name' do
before do before do
@ -213,7 +216,7 @@ describe FastJsonapi::ObjectSerializer do
end end
context 'when an array of records is given' do context 'when an array of records is given' do
let(:resource) { [movie, movie] } let(:resource) { build_movies(2) }
it 'returns correct hash which id equals owner_id' do it 'returns correct hash which id equals owner_id' do
expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id
@ -223,8 +226,12 @@ describe FastJsonapi::ObjectSerializer do
end end
context 'with block' do context 'with block' do
let(:params) { { prefix: 'movie' } }
before do before do
MovieSerializer.set_id { |record| "movie-#{record.owner_id}" } MovieSerializer.set_id do |record, params|
"#{params[:prefix]}-#{record.owner_id}"
end
end end
after do after do
@ -240,7 +247,7 @@ describe FastJsonapi::ObjectSerializer do
end end
context 'when an array of records is given' do context 'when an array of records is given' do
let(:resource) { [movie, movie] } let(:resource) { build_movies(2) }
it 'returns correct hash which id equals movie-id' do it 'returns correct hash which id equals movie-id' do
expect(serializable_hash[:data][0][:id]).to eq "movie-#{movie.owner_id}" expect(serializable_hash[:data][0][:id]).to eq "movie-#{movie.owner_id}"
@ -400,10 +407,30 @@ describe FastJsonapi::ObjectSerializer do
expect(action_serializable_hash[:data][:links][:url]).to eq "/action-movie/#{movie.id}" expect(action_serializable_hash[:data][:links][:url]).to eq "/action-movie/#{movie.id}"
end end
end end
describe 'optional links' do
subject(:downloadable_serializable_hash) { OptionalDownloadableMovieSerializer.new(movie, params).serializable_hash }
context 'when the link is provided' do
let(:params) { { params: { signed_url: signed_url } } }
let(:signed_url) { 'http://example.com/download_link?signature=abcdef' }
it 'includes the link' do
expect(downloadable_serializable_hash[:data][:links][:download]).to eq signed_url
end
end
context 'when the link is not provided' do
let(:params) { { params: {} } }
it 'does not include the link' do
expect(downloadable_serializable_hash[:data][:links]).to_not have_key(:download)
end
end
end
end end
describe '#key_transform' do describe '#key_transform' do
subject(:hash) { movie_serializer_class.new([movie, movie], include: [:movie_type]).serializable_hash } subject(:hash) { movie_serializer_class.new(build_movies(2), include: [:movie_type]).serializable_hash }
let(:movie_serializer_class) { "#{key_transform}_movie_serializer".classify.constantize } let(:movie_serializer_class) { "#{key_transform}_movie_serializer".classify.constantize }

View File

@ -22,6 +22,18 @@ describe FastJsonapi::ObjectSerializer do
expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign] expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign]
end end
it 'returns no fields when none are specified' do
hash = MovieSerializer.new(movie, fields: { movie: [] }).serializable_hash
expect(hash[:data][:attributes].keys).to eq []
end
it 'returns no relationships when none are specified' do
hash = MovieSerializer.new(movie, fields: { movie: [] }).serializable_hash
expect(hash[:data][:relationships].keys).to eq []
end
it 'only returns specified fields for included relationships' do it 'only returns specified fields for included relationships' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash
@ -45,4 +57,25 @@ describe FastJsonapi::ObjectSerializer do
expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie] expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie]
end end
context 'with no included fields specified' do
let(:fields) do
{
movie: %i[name actors advertising_campaign],
actor: []
}
end
it 'returns no fields for included relationships when none are specified' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash
expect(hash[:included][2][:attributes].keys).to eq []
end
it 'returns no relationships when none are specified' do
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash
expect(hash[:included][2][:relationships].keys).to eq []
end
end
end end

View File

@ -67,5 +67,46 @@ describe FastJsonapi::ObjectSerializer do
expect(actor_hash).not_to have_key(:data) expect(actor_hash).not_to have_key(:data)
end end
end end
context "including lazy loaded relationships" do
before(:context) do
class LazyLoadingMovieSerializer < MovieSerializer
has_many :actors, lazy_load_data: true, links: {
related: :actors_relationship_url
}
end
end
let(:serializer) { LazyLoadingMovieSerializer.new(movie, include: [:actors]) }
let(:actor_hash) { hash[:data][:relationships][:actors] }
it "includes the :data key" do
expect(actor_hash).to be_present
expect(actor_hash).to have_key(:data)
end
end
context "relationship links defined by a method on the object" do
before(:context) do
class Movie
def relationship_links
{ self: "http://movies.com/#{id}/relationships/actors" }
end
end
class LinksPassingMovieSerializer < MovieSerializer
has_many :actors, links: :relationship_links
end
end
let(:serializer) { LinksPassingMovieSerializer.new(movie) }
let(:links) { hash[:data][:relationships][:actors][:links] }
let(:relationship_url) { "http://movies.com/#{movie.id}/relationships/actors" }
it "generates relationship links in the object" do
expect(links).to be_present
expect(links[:self]).to eq(relationship_url)
end
end
end end
end end

View File

@ -4,13 +4,15 @@ describe FastJsonapi::ObjectSerializer do
include_context 'movie class' include_context 'movie class'
include_context 'group class' include_context 'group class'
let(:movies) { build_movies(2) }
context 'when testing instance methods of object serializer' do context 'when testing instance methods of object serializer' do
it 'returns correct hash when serializable_hash is called' do it 'returns correct hash when serializable_hash is called' do
options = {} options = {}
options[:meta] = { total: 2 } options[:meta] = { total: 2 }
options[:links] = { self: 'self' } options[:links] = { self: 'self' }
options[:include] = [:actors] options[:include] = [:actors]
serializable_hash = MovieSerializer.new([movie, movie], options).serializable_hash serializable_hash = MovieSerializer.new(movies, options).serializable_hash
expect(serializable_hash[:data].length).to eq 2 expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 4 expect(serializable_hash[:data][0][:relationships].length).to eq 4
@ -58,7 +60,7 @@ describe FastJsonapi::ObjectSerializer do
it 'returns correct number of records when serialized_json is called for an array' do it 'returns correct number of records when serialized_json is called for an array' do
options = {} options = {}
options[:meta] = { total: 2 } options[:meta] = { total: 2 }
json = MovieSerializer.new([movie, movie], options).serialized_json json = MovieSerializer.new(movies, options).serialized_json
serializable_hash = JSON.parse(json) serializable_hash = JSON.parse(json)
expect(serializable_hash['data'].length).to eq 2 expect(serializable_hash['data'].length).to eq 2
expect(serializable_hash['meta']).to be_instance_of(Hash) expect(serializable_hash['meta']).to be_instance_of(Hash)
@ -124,7 +126,7 @@ describe FastJsonapi::ObjectSerializer do
end end
it 'returns multiple records' do it 'returns multiple records' do
json_hash = MovieSerializer.new([movie, movie]).as_json json_hash = MovieSerializer.new(movies).as_json
expect(json_hash['data'].length).to eq 2 expect(json_hash['data'].length).to eq 2
end end
@ -139,6 +141,13 @@ describe FastJsonapi::ObjectSerializer do
options = {} options = {}
options[:meta] = { total: 2 } options[:meta] = { total: 2 }
options[:include] = [:blah_blah] options[:include] = [:blah_blah]
expect { MovieSerializer.new(movies, options).serializable_hash }.to raise_error(ArgumentError)
end
it 'returns errors when serializing with non-existent and existent includes keys' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors, :blah_blah]
expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError) expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError)
end end
@ -148,13 +157,19 @@ describe FastJsonapi::ObjectSerializer do
expect { MovieSerializer.new(movie, options) }.not_to raise_error expect { MovieSerializer.new(movie, options) }.not_to raise_error
end end
it 'does not throw an error with non-empty string array includes keys' do
options = {}
options[:include] = ['actors', 'owner']
expect { MovieSerializer.new(movie, options) }.not_to raise_error
end
it 'returns keys when serializing with empty string/nil array includes key' do it 'returns keys when serializing with empty string/nil array includes key' do
options = {} options = {}
options[:meta] = { total: 2 } options[:meta] = { total: 2 }
options[:include] = [''] options[:include] = ['']
expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta] expect(MovieSerializer.new(movies, options).serializable_hash.keys).to eq [:data, :meta]
options[:include] = [nil] options[:include] = [nil]
expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta] expect(MovieSerializer.new(movies, options).serializable_hash.keys).to eq [:data, :meta]
end end
end end
@ -314,13 +329,6 @@ describe FastJsonapi::ObjectSerializer do
expect(BlahBlahSerializer.record_type).to be :blah_blah expect(BlahBlahSerializer.record_type).to be :blah_blah
end end
it 'shouldnt set default_type for a serializer that doesnt follow convention' do
class BlahBlahSerializerBuilder
include FastJsonapi::ObjectSerializer
end
expect(BlahBlahSerializerBuilder.record_type).to be_nil
end
it 'should set default_type for a namespaced serializer' do it 'should set default_type for a namespaced serializer' do
module V1 module V1
class BlahSerializer class BlahSerializer
@ -329,6 +337,20 @@ describe FastJsonapi::ObjectSerializer do
end end
expect(V1::BlahSerializer.record_type).to be :blah expect(V1::BlahSerializer.record_type).to be :blah
end end
it 'shouldnt set default_type for a serializer that doesnt follow convention' do
class BlahBlahSerializerBuilder
include FastJsonapi::ObjectSerializer
end
expect(BlahBlahSerializerBuilder.record_type).to be_nil
end
it 'shouldnt set default_type for an anonymous serializer' do
serializer_class = Class.new do
include FastJsonapi::ObjectSerializer
end
expect(serializer_class.record_type).to be_nil
end
end end
context 'when serializing included, serialize any links' do context 'when serializing included, serialize any links' do
@ -473,6 +495,16 @@ describe FastJsonapi::ObjectSerializer do
options[:include] = [:actors] options[:include] = [:actors]
expect(serializable_hash['included']).to be_blank expect(serializable_hash['included']).to be_blank
end end
end
end
context 'when include has frozen array' do
let(:options) { { include: [:actors].freeze }}
let(:json) { MovieOptionalRelationshipSerializer.new(movie, options).serialized_json }
it 'does not raise and error' do
expect(json['included']).to_not be_blank
end end
end end

View File

@ -52,7 +52,7 @@ describe FastJsonapi::ObjectSerializer do
end end
it 'returns correct hash when record_hash is called' do it 'returns correct hash when record_hash is called' do
record_hash = MovieSerializer.send(:record_hash, movie, nil) record_hash = MovieSerializer.send(:record_hash, movie, nil, nil)
expect(record_hash[:id]).to eq movie.id.to_s expect(record_hash[:id]).to eq movie.id.to_s
expect(record_hash[:type]).to eq MovieSerializer.record_type expect(record_hash[:type]).to eq MovieSerializer.record_type
expect(record_hash).to have_key(:attributes) if MovieSerializer.attributes_to_serialize.present? expect(record_hash).to have_key(:attributes) if MovieSerializer.attributes_to_serialize.present?

View File

@ -197,6 +197,12 @@ RSpec.shared_context 'movie class' do
link(:url) { |object| "/horror-movie/#{object.id}" } link(:url) { |object| "/horror-movie/#{object.id}" }
end end
class OptionalDownloadableMovieSerializer < MovieSerializer
link(:download, if: Proc.new { |record, params| params && params[:signed_url] }) do |movie, params|
params[:signed_url]
end
end
class MovieWithoutIdStructSerializer class MovieWithoutIdStructSerializer
include FastJsonapi::ObjectSerializer include FastJsonapi::ObjectSerializer
attributes :name, :release_year attributes :name, :release_year
@ -385,6 +391,7 @@ RSpec.shared_context 'movie class' do
ActionMovieSerializer ActionMovieSerializer
GenreMovieSerializer GenreMovieSerializer
HorrorMovieSerializer HorrorMovieSerializer
OptionalDownloadableMovieSerializer
Movie Movie
MovieSerializer MovieSerializer
Actor Actor