Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #404

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

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

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.
Expand Down Expand Up @@ -100,6 +104,17 @@ movie.actor_ids = [1, 2, 3]
movie.owner_id = 3
movie.movie_type_id = 1
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
Expand Down Expand Up @@ -298,7 +313,7 @@ options[:links] = {
prev: '...'
}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new([movie, movie], options).serialized_json
MovieSerializer.new(movies, options).serialized_json
```

### Collection Serialization
Expand All @@ -310,15 +325,15 @@ options[:links] = {
next: '...',
prev: '...'
}
hash = MovieSerializer.new([movie, movie], options).serializable_hash
json_string = MovieSerializer.new([movie, movie], options).serialized_json
hash = MovieSerializer.new(movies, options).serializable_hash
json_string = MovieSerializer.new(movies, 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 this option is not provided or `nil` autodetect 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
Expand Down Expand Up @@ -450,7 +465,7 @@ Option | Purpose | Example
------------ | ------------- | -------------
set_type | Type name of Object | ```set_type :movie ```
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\| "#{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```
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 ```
Expand All @@ -473,7 +488,7 @@ Skylight relies on `ActiveSupport::Notifications` to track these two core method
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::SERIALIZED_JSON_NOTIFICATION`

Expand Down
6 changes: 3 additions & 3 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def process_options(options)
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)

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)
end
end
Expand Down Expand Up @@ -130,7 +130,7 @@ def reflected_record_type
return @reflected_record_type if defined?(@reflected_record_type)

@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
end
end
Expand Down Expand Up @@ -299,7 +299,7 @@ def link(link_name, link_method_name = nil, &block)
def validate_includes!(includes)
return if includes.blank?

includes.detect do |include_item|
includes.each do |include_item|
klass = self
parse_include_item(include_item).each do |parsed_include|
relationships_to_serialize = klass.relationships_to_serialize || {}
Expand Down
3 changes: 3 additions & 0 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ def links_hash(record, params = {})
def attributes_hash(record, fieldset = nil, params = {})
attributes = attributes_to_serialize
attributes = attributes.slice(*fieldset) if fieldset.present?
attributes = {} if fieldset == []

attributes.each_with_object({}) do |(_k, attribute), hash|
attribute.serialize(record, params, hash)
end
Expand All @@ -52,6 +54,7 @@ def attributes_hash(record, fieldset = 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 = {} if fieldset == []

relationships.each_with_object({}) do |(_k, relationship), hash|
relationship.serialize(record, params, hash)
Expand Down
2 changes: 1 addition & 1 deletion lib/fast_jsonapi/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module FastJsonapi
VERSION = "1.4"
VERSION = "1.5"
end
3 changes: 2 additions & 1 deletion spec/lib/instrumentation/as_notifications_negative_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
options[:meta] = { total: 2 }
options[:include] = [:actors]

@serializer = MovieSerializer.new([movie, movie], options)
movies = build_movies(2)
@serializer = MovieSerializer.new(movies, options)
end

context 'serializable_hash' do
Expand Down
3 changes: 2 additions & 1 deletion spec/lib/instrumentation/as_notifications_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
options[:meta] = { total: 2 }
options[:include] = [:actors]

@serializer = MovieSerializer.new([movie, movie], options)
movies = build_movies(2)
@serializer = MovieSerializer.new(movies, options)
end

context 'serializable_hash' do
Expand Down
3 changes: 2 additions & 1 deletion spec/lib/object_serializer_caching_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
options[:links] = { self: 'self' }

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][0][:relationships].length).to eq 3
Expand Down
6 changes: 3 additions & 3 deletions spec/lib/object_serializer_class_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@
end

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
expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id
Expand All @@ -240,7 +240,7 @@
end

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
expect(serializable_hash[:data][0][:id]).to eq "movie-#{movie.owner_id}"
Expand Down Expand Up @@ -403,7 +403,7 @@ def year_since_release_calculator(release_year)
end

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 }

Expand Down
33 changes: 33 additions & 0 deletions spec/lib/object_serializer_fields_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@
expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign]
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
hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash

Expand All @@ -45,4 +57,25 @@

expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie]
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
54 changes: 43 additions & 11 deletions spec/lib/object_serializer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
include_context 'movie class'
include_context 'group class'

let(:movies) { build_movies(2) }

context 'when testing instance methods of object serializer' do
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:links] = { self: 'self' }
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][0][:relationships].length).to eq 4
Expand Down Expand Up @@ -58,7 +60,7 @@
it 'returns correct number of records when serialized_json is called for an array' do
options = {}
options[:meta] = { total: 2 }
json = MovieSerializer.new([movie, movie], options).serialized_json
json = MovieSerializer.new(movies, options).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data'].length).to eq 2
expect(serializable_hash['meta']).to be_instance_of(Hash)
Expand Down Expand Up @@ -124,7 +126,7 @@
end

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
end

Expand All @@ -139,6 +141,13 @@
options = {}
options[:meta] = { total: 2 }
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)
end

Expand All @@ -148,13 +157,19 @@
expect { MovieSerializer.new(movie, options) }.not_to raise_error
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
options = {}
options[:meta] = { total: 2 }
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]
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

Expand Down Expand Up @@ -314,20 +329,27 @@ class BlahBlahSerializer
expect(BlahBlahSerializer.record_type).to be :blah_blah
end

it 'should set default_type for a namespaced serializer' do
module V1
class BlahSerializer
include FastJsonapi::ObjectSerializer
end
end
expect(V1::BlahSerializer.record_type).to be :blah
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
module V1
class BlahSerializer
include FastJsonapi::ObjectSerializer
end
it 'shouldnt set default_type for an anonymous serializer' do
serializer_class = Class.new do
include FastJsonapi::ObjectSerializer
end
expect(V1::BlahSerializer.record_type).to be :blah
expect(serializer_class.record_type).to be_nil
end
end

Expand Down Expand Up @@ -473,6 +495,16 @@ class BlahSerializer
options[:include] = [:actors]
expect(serializable_hash['included']).to be_blank
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

Expand Down