Compare commits

..

No commits in common. "master" and "1.1.1" have entirely different histories.

79 changed files with 2770 additions and 2457 deletions

5
.document Normal file
View File

@ -0,0 +1,5 @@
lib/**/*.rb
bin/*
-
features/**/*.feature
LICENSE.txt

View File

@ -1,40 +0,0 @@
name: CI
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: [2.4, 2.7, '3.0', 3.1, truffleruby-head]
steps:
- uses: actions/checkout@master
- name: Sets up the Ruby version
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: Sets up the environment
run: |
sudo apt-get install libsqlite3-dev
gem install -q bundler
bundle install
- name: Runs code QA and tests
run: bundle exec rake
- name: Publish to Rubygems
continue-on-error: true
if: ${{ github.ref == 'refs/heads/master' }}
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec
gem push *.gem
env:
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}

7
.gitignore vendored
View File

@ -39,10 +39,3 @@ test.db
# For those who install gems locally to a vendor dir
/vendor
# Don't checkin Gemfile.lock
# See: https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
Gemfile.lock
# Gem builds
/*.gem

View File

@ -1,102 +0,0 @@
require:
- rubocop-performance
- rubocop-rspec
AllCops:
NewCops: enable
SuggestExtensions: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/SymbolArray:
Enabled: false
Style/WordArray:
Enabled: false
Style/SymbolProc:
Exclude:
- 'spec/fixtures/*.rb'
Lint/DuplicateMethods:
Exclude:
- 'spec/fixtures/*.rb'
RSpec/FilePath:
Enabled: false
RSpec/DescribedClass:
Enabled: false
RSpec/ExampleLength:
Enabled: false
RSpec/MultipleExpectations:
Enabled: false
RSpec/NestedGroups:
Enabled: false
Performance/TimesMap:
Exclude:
- 'spec/**/**.rb'
Gemspec/RequiredRubyVersion:
Enabled: false
# TODO: Fix these...
Style/Documentation:
Enabled: false
Style/GuardClause:
Exclude:
- 'lib/**/**.rb'
Style/ConditionalAssignment:
Exclude:
- 'lib/**/**.rb'
Style/IfUnlessModifier:
Exclude:
- 'lib/**/**.rb'
Lint/AssignmentInCondition:
Exclude:
- 'lib/**/**.rb'
Metrics:
Exclude:
- 'lib/**/**.rb'
Metrics/BlockLength:
Enabled: false
Layout/LineLength:
Exclude:
- 'lib/**/**.rb'
Naming/PredicateName:
Exclude:
- 'lib/**/**.rb'
Naming/AccessorMethodName:
Exclude:
- 'lib/**/**.rb'
Style/CaseLikeIf:
Exclude:
- 'lib/fast_jsonapi/object_serializer.rb'
Style/OptionalBooleanParameter:
Exclude:
- 'lib/fast_jsonapi/serialization_core.rb'
- 'lib/fast_jsonapi/relationship.rb'
Lint/DuplicateBranch:
Exclude:
- 'lib/fast_jsonapi/relationship.rb'
Style/DocumentDynamicEvalDefinition:
Exclude:
- 'lib/extensions/has_one.rb'

8
.travis.yml Normal file
View File

@ -0,0 +1,8 @@
language: ruby
rvm:
- 2.2.9
- 2.3.6
- 2.4.3
- 2.5.0
script:
- bundle exec rspec

View File

@ -1,79 +0,0 @@
# Changelog
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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- ...
## [2.2.0] - 2021-03-11
### Added
- Proper error is raised on unsupported includes (#125)
### Changed
- Documentation updates (#137 #139 #143 #146)
### Fixed
- Empty relationships are no longer added to serialized doc (#116)
- Ruby v3 compatibility (#160)
## [2.1.0] - 2020-08-30
### Added
- Optional meta field to relationships (#99 #100)
- Support for `params` on cache keys (#117)
### Changed
- Performance instrumentation (#110 #39)
- Improved collection detection (#112)
### Fixed
- Ensure caching correctly incorporates fieldset information into the cache key to prevent incorrect fieldset caching (#90)
- Performance optimizations for nested includes (#103)
## [2.0.0] - 2020-06-22
The project was renamed to `jsonapi-serializer`! (#94)
### Changed
- Remove `ObjectSerializer#serialized_json` (#91)
## [1.7.2] - 2020-05-18
### Fixed
- Relationship#record_type_for does not assign static record type for polymorphic relationships (#83)
## [1.7.1] - 2020-05-01
### Fixed
- ObjectSerializer#serialized_json accepts arguments for to_json (#80)
## [1.7.0] - 2020-04-29
### Added
- Serializer option support for procs (#32)
- JSON serialization API method is now implementable (#44)
### Changed
- Support for polymorphic `id_method_name` (#17)
- Relationships support for `&:proc` syntax (#58)
- Conditional support for procs (#59)
- Attribute support for procs (#67)
- Refactor caching support (#52)
- `is_collection?` is safer for objects (#18)
### Removed
- `serialized_json` is now deprecated (#44)
## [1.6.0] - 2019-11-04
### Added
- 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))
- 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
- 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))
- 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))

26
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,26 @@
# Contributing to Fast JSON API
We are following the Gitflow workflow. The active development branch is [dev](https://github.com/Netflix/fast_jsonapi/tree/dev), the stable branch is [master](https://github.com/Netflix/fast_jsonapi/tree/master).
Contributions will be accepted to the [dev](https://github.com/Netflix/fast_jsonapi/tree/dev) only.
## How to provide a patch for a new feature
1. If it is a major feature, please create an [Issue]( https://github.com/Netflix/fast_jsonapi/issues ) and discuss with the project leaders.
2. If in step 1 you get an acknowledge from the project leaders, use the
following procedure to submit a patch:
a. Fork Fast JSON API on github ( http://help.github.com/fork-a-repo/ )
b. Create a topic branch (git checkout -b my_branch)
c. Push to your branch (git push origin my_branch)
d. Initiate a pull request on github ( http://help.github.com/send-pull-requests/ )
e. Done :)
3. Run the tests. We only take pull requests with passing tests.
For minor fixes just open a pull request to the [dev]( https://github.com/Netflix/fast_jsonapi/tree/dev ) branch on Github.

117
Gemfile.lock Normal file
View File

@ -0,0 +1,117 @@
PATH
remote: .
specs:
fast_jsonapi (1.1.1)
activesupport (>= 4.2)
GEM
remote: https://rubygems.org/
specs:
actionpack (5.1.4)
actionview (= 5.1.4)
activesupport (= 5.1.4)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.4)
activesupport (= 5.1.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.7)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activemodel (5.1.4)
activesupport (= 5.1.4)
activerecord (5.1.4)
activemodel (= 5.1.4)
activesupport (= 5.1.4)
arel (~> 8.0)
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (8.0.0)
benchmark-perf (0.2.1)
builder (3.2.3)
byebug (9.1.0)
case_transform (0.2)
activesupport
concurrent-ruby (1.0.5)
crass (1.0.3)
diff-lcs (1.3)
erubi (1.7.0)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
jsonapi-deserializable (0.2.0)
jsonapi-rb (0.5.0)
jsonapi-deserializable (~> 0.2.0)
jsonapi-serializable (~> 0.3.0)
jsonapi-renderer (0.2.0)
jsonapi-serializable (0.3.0)
jsonapi-renderer (~> 0.2.0)
jsonapi-serializers (1.0.0)
activesupport
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mini_portile2 (2.3.0)
minitest (5.10.3)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
oj (3.4.0)
rack (2.0.3)
rack-test (0.8.2)
rack (>= 1.0, < 3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-benchmark (0.3.0)
benchmark-perf (~> 0.2.0)
rspec (>= 3.0.0, < 4.0.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
skylight (1.5.0)
activesupport (>= 3.0.0)
sqlite3 (1.3.13)
thread_safe (0.3.6)
tzinfo (1.2.4)
thread_safe (~> 0.1)
PLATFORMS
ruby
DEPENDENCIES
active_model_serializers (~> 0.10.7)
activerecord (>= 4.2)
bundler (~> 1.0)
byebug
fast_jsonapi!
jsonapi-rb (~> 0.5.0)
jsonapi-serializers (~> 1.0.0)
oj (~> 3.3)
rspec (~> 3.5.0)
rspec-benchmark (~> 0.3.0)
skylight (~> 1.3)
sqlite3 (~> 1.3)
BUNDLED WITH
1.16.1

View File

@ -1,6 +1,6 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@ -192,7 +192,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

1
OSSMETADATA Normal file
View File

@ -0,0 +1 @@
osslifecycle=active

607
README.md
View File

@ -1,29 +1,20 @@
# JSON:API Serialization Library
# Fast JSON API
## :warning: :construction: v2 (the `master` branch) is in maintenance mode! :construction: :warning:
[![Build Status](https://travis-ci.org/Netflix/fast_jsonapi.svg?branch=master)](https://travis-ci.org/Netflix/fast_jsonapi)
We'll gladly accept bugfixes and security-related fixes for v2 (the `master` branch), but at this stage, contributions for new features/improvements are welcome only for v3. Please feel free to leave comments in the [v3 Pull Request](https://github.com/jsonapi-serializer/jsonapi-serializer/pull/141).
---
A fast [JSON:API](https://jsonapi.org/) serializer for Ruby Objects.
Previously this project was called **fast_jsonapi**, we forked the project
and renamed it to **jsonapi/serializer** in order to keep it alive.
We would like to thank the Netflix team for the initial work and to all our
contributors and users for the continuous support!
A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.
# Performance Comparison
We compare serialization times with `ActiveModelSerializer` and alternative
implementations as part of performance tests available at
[jsonapi-serializer/comparisons](https://github.com/jsonapi-serializer/comparisons).
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 want to ensure that with every
change on this library, serialization time stays significantly faster than
the performance provided by the alternatives. Please read the performance
article in the `docs` folder for any questions related to methodology.
## Benchmark times for 250 records
```bash
$ rspec
Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms
```
# Table of Contents
@ -38,15 +29,6 @@ article in the `docs` folder for any questions related to methodology.
* [Key Transforms](#key-transforms)
* [Collection Serialization](#collection-serialization)
* [Caching](#caching)
* [Params](#params)
* [Conditional Attributes](#conditional-attributes)
* [Conditional Relationships](#conditional-relationships)
* [Specifying a Relationship Serializer](#specifying-a-relationship-serializer)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods)
* [Performance Instrumentation](#performance-instrumentation)
* [Deserialization](#deserialization)
* [Migrating from Netflix/fast_jsonapi](#migrating-from-netflixfast_jsonapi)
* [Contributing](#contributing)
@ -63,7 +45,7 @@ article in the `docs` folder for any questions related to methodology.
Add this line to your application's Gemfile:
```ruby
gem 'jsonapi-serializer'
gem 'fast_jsonapi'
```
Execute:
@ -78,7 +60,7 @@ $ bundle install
You can use the bundled generator if you are using the library inside of
a Rails project:
rails g serializer Movie name year
rails g Serializer Movie name year
This will create a new serializer in `app/serializers/movie_serializer.rb`
@ -94,8 +76,7 @@ end
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
set_id :owner_id # optional
attributes :name, :year
@ -115,17 +96,6 @@ 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
@ -137,7 +107,7 @@ hash = MovieSerializer.new(movie).serializable_hash
#### Return Serialized JSON
```ruby
json_string = MovieSerializer.new(movie).serializable_hash.to_json
json_string = MovieSerializer.new(movie).serialized_json
```
#### Serialized Output
@ -176,16 +146,12 @@ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
#### The Optionality of `set_type`
By default fast_jsonapi will try to figure the type based on the name of the serializer class. For example `class MovieSerializer` will automatically have a type of `:movie`. If your serializer class name does not follow this format, you have to manually state the `set_type` at the serializer.
### Key Transforms
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
# Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel
end
@ -200,13 +166,13 @@ set_key_transform :underscore # "some_key" => "some_key"
```
### Attributes
Attributes are defined using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized:
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
attribute :name
end
@ -216,7 +182,7 @@ Custom attributes that must be serialized but do not exist on the model can be d
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
attributes :name, :year
@ -230,7 +196,7 @@ The block syntax can also be used to override the property on the object:
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
attribute :name do |object|
"#{object.name} Part 2"
@ -238,529 +204,104 @@ 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 JSONAPI::Serializer
attributes :name
attribute :released_in_year, &:year
end
```
### Links Per Object
Links are defined using the `link` method. By default, links 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.
You can configure the method to use on the object for example a link with key `self` will get set to the value returned by a method called `url` on the movie object.
You can also use a block to define a url as shown in `custom_url`. You can access params in these blocks as well as shown in `personalized_url`
```ruby
class MovieSerializer
include JSONAPI::Serializer
link :public_url
link :self, :url
link :custom_url do |object|
"https://movies.com/#{object.name}-(#{object.year})"
end
link :personalized_url do |object, params|
"https://movies.com/#{object.name}-#{params[:user].reference_code}"
end
end
```
#### Links on a Relationship
You can specify [relationship links](https://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](https://jsonapi.org/format/#document-resource-object-related-resource-links))
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
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:
```ruby
has_many :actors, lazy_load_data: true, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
```
### Meta Per Resource
For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.
```ruby
class MovieSerializer
include JSONAPI::Serializer
meta do |movie|
{
years_since_release: Date.current.year - movie.year
}
end
end
```
#### Meta on a Relationship
You can specify [relationship meta](https://jsonapi.org/format/#document-resource-object-relationships) by using the `meta:` option on the serializer. Relationship meta in JSON API is useful if you wish to provide non-standard meta-information about the relationship.
Meta can be defined either by passing a static hash or by using Proc to the `meta` key. In the latter case, 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 JSONAPI::Serializer
has_many :actors, meta: Proc.new do |movie_record, params|
{ count: movie_record.actors.length }
end
end
```
### Compound Document
Support for top-level and nested included associations through `options[:include]`.
Support for top-level included member through ` options[:include] `.
```ruby
options = {}
options[:meta] = { total: 2 }
options[:links] = {
self: '...',
next: '...',
prev: '...'
}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new(movies, options).serializable_hash.to_json
options[:include] = [:actors]
MovieSerializer.new([movie, movie], options).serialized_json
```
### Collection Serialization
```ruby
options[:meta] = { total: 2 }
options[:links] = {
self: '...',
next: '...',
prev: '...'
}
hash = MovieSerializer.new(movies, options).serializable_hash
json_string = MovieSerializer.new(movies, options).serializable_hash.to_json
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` 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
**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
To enable caching, use `cache_options store: <cache_store>`:
```ruby
class MovieSerializer
include JSONAPI::Serializer
# use rails cache with a separate namespace and fixed expiry
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
```
`store` is required can be anything that implements a
`#fetch(record, **options, &block)` method:
- `record` is the record that is currently serialized
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports
- `&block` should be executed to fetch new data if cache is empty
So for the example above it will call the cache instance like this:
```ruby
Rails.cache.fetch(record, namespace: 'jsonapi-serializer', expires_in: 1.hour) { ... }
```
#### Caching and Sparse Fieldsets
If caching is enabled and fields are provided to the serializer, the fieldset will be appended to the cache key's namespace.
For example, given the following serializer definition and instance:
```ruby
class ActorSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
serializer = ActorSerializer.new(actor, { fields: { actor: [:first_name] } })
```
The following cache namespace will be generated: `'jsonapi-serializer-fieldset:first_name'`.
### Params
In some cases, attribute values might require more information than what is
available on the record, for example, access privileges or other information
related to a current authenticated user. The `options[:params]` value covers these
cases by allowing you to pass in a hash of additional parameters necessary for
your use case.
Leveraging the new params is easy, when you define a custom id, attribute or
relationship with a block you opt-in to using params by adding it as a block
parameter.
```ruby
class MovieSerializer
include JSONAPI::Serializer
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
attribute :can_view_early do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user].is_employee? ? true : false
end
belongs_to :primary_agent do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user]
end
end
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, {params: {current_user: current_user}})
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 JSONAPI::Serializer
attributes :name, :year
attribute :release_year, if: Proc.new { |record|
# Release year will only be serialized if it's greater than 1990
record.release_year > 1990
}
attribute :director, if: Proc.new { |record, params|
# The director will be serialized only if the :admin key of params is true
params && params[:admin] == true
}
# Custom attribute `name_year` will only be serialized if both `name` and `year` fields are present
attribute :name_year, if: Proc.new { |record|
record.name.present? && record.year.present?
} do |object|
"#{object.name} - #{object.year}"
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 JSONAPI::Serializer
# 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
```
### Specifying a Relationship Serializer
In many cases, the relationship can automatically detect the serializer to use.
```ruby
class MovieSerializer
include JSONAPI::Serializer
# resolves to StudioSerializer
belongs_to :studio
# resolves to ActorSerializer
has_many :actors
end
```
At other times, such as when a property name differs from the class name, you may need to explicitly state the serializer to use. You can do so by specifying a different symbol or the serializer class itself (which is the recommended usage):
```ruby
class MovieSerializer
include JSONAPI::Serializer
# resolves to MovieStudioSerializer
belongs_to :studio, serializer: :movie_studio
# resolves to PerformerSerializer
has_many :actors, serializer: PerformerSerializer
end
```
For more advanced cases, such as polymorphic relationships and Single Table Inheritance, you may need even greater control to select the serializer based on the specific object or some specified serialization parameters. You can do by defining the serializer as a `Proc`:
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, serializer: Proc.new do |record, params|
if record.comedian?
ComedianSerializer
elsif params[:use_drama_serializer]
DramaSerializer
else
ActorSerializer
end
end
end
```
### Sparse Fieldsets
Attributes and relationships can be selectively returned per record type by using the `fields` option.
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
cache_options enabled: true, cache_length: 12.hours
attributes :name, :year
end
serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } })
serializer.serializable_hash
```
### Using helper methods
You can mix-in code from another ruby module into your serializer class to reuse functions across your app.
Since a serializer is evaluated in a the context of a `class` rather than an `instance` of a class, you need to make sure that your methods act as `class` methods when mixed in.
##### Using ActiveSupport::Concern
``` ruby
module AvatarHelper
extend ActiveSupport::Concern
class_methods do
def avatar_url(user)
user.image.url
end
end
end
class UserSerializer
include JSONAPI::Serializer
include AvatarHelper # mixes in your helper method as class method
set_type :user
attributes :name, :email
attribute :avatar do |user|
avatar_url(user)
end
end
```
##### Using Plain Old Ruby
``` ruby
module AvatarHelper
def avatar_url(user)
user.image.url
end
end
class UserSerializer
include JSONAPI::Serializer
extend AvatarHelper # mixes in your helper method as class method
set_type :user
attributes :name, :email
attribute :avatar do |user|
avatar_url(user)
end
end
```
### Customizable Options
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, params\| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" }`
cache_options | Hash with store to enable caching and optional further cache options | `cache_options store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes`
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`
record_type | Set custom Object Type for a relationship | `belongs_to :owner, record_type: :user`
serializer | Set custom Serializer for a relationship | `has_many :actors, serializer: :custom_actor`, `has_many :actors, serializer: MyApp::Api::V1::ActorSerializer`, or `has_many :actors, serializer -> (object, params) { (return a serializer class) }`
polymorphic | Allows different record types for a polymorphic association | `has_many :targets, polymorphic: true`
polymorphic | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
set_type | Type name of Object | ```set_type :movie ```
set_id | ID of Object | ```set_id :owner_id ```
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours```
id_method_name | Set custom method name to get ID of an 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 ```
record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user```
serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor```
### Performance Instrumentation
### Instrumentation
Performance instrumentation is available by using the
`active_support/notifications`.
To enable it, include the module in your serializer class:
`fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer:
```ruby
require 'jsonapi/serializer'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
require 'fast_jsonapi/instrumentation/skylight'
```
[Skylight](https://www.skylight.io/) integration is also available and
supported by us, follow the Skylight documentation to enable it.
Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:
```ruby
require 'fast_jsonapi/instrumentation'
```
The two instrumented notifcations are supplied by these two constants:
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`
It is also possible to instrument one method without the other by using one of the following require statements:
```ruby
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'
```
Same goes for the Skylight integration:
```ruby
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
```
## Contributing
Please see [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md) for more details on contributing
### Running Tests
The project has and requires unit tests, functional tests and performance
tests. To run tests use the following command:
We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional tests and performance tests. To run tests use the following command:
```bash
rspec
```
## Deserialization
We currently do not support deserialization, but we recommend to use any of the next gems:
To run tests without the performance tests (for quicker test runs):
### [JSONAPI.rb](https://github.com/stas/jsonapi.rb)
This gem provides the next features alongside deserialization:
- Collection meta
- Error handling
- Includes and sparse fields
- Filtering and sorting
- Pagination
## Migrating from Netflix/fast_jsonapi
If you come from [Netflix/fast_jsonapi](https://github.com/Netflix/fast_jsonapi), here is the instructions to switch.
### Modify your Gemfile
```diff
- gem 'fast_jsonapi'
+ gem 'jsonapi-serializer'
```bash
rspec spec --tag ~performance:true
```
### Replace all constant references
To run tests only performance tests:
```diff
class MovieSerializer
- include FastJsonapi::ObjectSerializer
+ include JSONAPI::Serializer
end
```bash
rspec spec --tag performance:true
```
### Replace removed methods
### We're Hiring!
```diff
- json_string = MovieSerializer.new(movie).serialized_json
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
Join the Netflix Studio Engineering team and help us build gems like this!
### Replace require references
```diff
- require 'fast_jsonapi'
+ require 'jsonapi/serializer'
```
### Update your cache options
See [docs](https://github.com/jsonapi-serializer/jsonapi-serializer#caching).
```diff
- cache_options enabled: true, cache_length: 12.hours
+ cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
```
## Contributing
Please follow the instructions we provide as part of the issue and
pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and
contributors are expected to adhere to the
[Contributor Covenant](https://contributor-covenant.org) code of conduct.
* [Senior Ruby Engineer](https://jobs.netflix.com/jobs/864893)
* [Senior Platform Engineer](https://jobs.netflix.com/jobs/865783)

View File

@ -1,15 +0,0 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'
desc('Codestyle check and linter')
RuboCop::RakeTask.new('rubocop') do |task|
task.fail_on_error = true
task.patterns = [
'lib/**/*.rb',
'spec/**/*.rb'
]
end
RSpec::Core::RakeTask.new(:spec)
task(default: [:rubocop, :spec])

1
VERSION Normal file
View File

@ -0,0 +1 @@
1.0.18

View File

@ -1,16 +0,0 @@
## Expected Behavior
## Actual Behavior
## Steps to Reproduce the Problem
1.
2.
3.
## Specifications
- Version:
- Ruby version:

View File

@ -1,17 +0,0 @@
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a
relevant issue. -->
## What is the new behavior?
<!-- Please describe the behavior or changes that are being added here. -->
## Checklist
Please make sure the following requirements are complete:
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been reviewed and added / updated if needed (for bug fixes /
features)
- [ ] All automated checks pass (CI/CD)

View File

@ -0,0 +1,35 @@
{
"data": [
{
"id": "232",
"type": "movie",
"attributes": {
"name": "test movie",
"year": null
},
"relationships": {
"actors": {
"data": [
{
"id": "1",
"type": "actor"
},
{
"id": "2",
"type": "actor"
}
]
},
"owner": {
"data": {
"id": "3",
"type": "user"
}
}
}
}
],
"meta": {
"total": 2
}
}

View File

@ -1,27 +0,0 @@
# JSON Serialization Support
Support for JSON serialization is no longer provided as part of the API of
`fast_jsonapi`. This decision (see #12) is based on the idea that developers
know better what library for JSON serialization works best for their project.
To bring back the old functionality, define the `to_json` or `serialized_json`
methods with the relevant JSON library call. Here's an example on how to get
it working with the popular `oj` gem:
```ruby
require 'oj'
require 'fast_jsonapi'
class BaseSerializer
include JSONAPI::Serializer
def to_json
Oj.dump(serializable_hash)
end
alias_method :serialized_json, :to_json
end
class MovieSerializer < BaseSerializer
# ...
end
```

View File

@ -0,0 +1,30 @@
{
"data": {
"id": "232",
"type": "movie",
"attributes": {
"name": "test movie",
"year": null
},
"relationships": {
"actors": {
"data": [
{
"id": "1",
"type": "actor"
},
{
"id": "2",
"type": "actor"
}
]
},
"owner": {
"data": {
"id": "3",
"type": "user"
}
}
}
}
}

View File

@ -1,82 +0,0 @@
# Performance using Fast JSON API
We have been getting a few questions about Fast JSON API's performance
statistics and the methodology used to measure the performance. This article is
an attempt at addressing this aspect of the gem.
## Prologue
With use cases like infinite scroll on complex models and bulk update on index
pages, we started observing performance degradation on our Rails APIs. Our
first step was to enable instrumentation and then tune for performance. We
realized that, on average, more than 50% of the time was being spent on AMS
serialization. At the same time, we had a couple of APIs that were simply
proxying requests on top of a non-Rails, non-JSON API endpoint. Guess what? The
non-Rails endpoints were giving us serialized JSON back in a fraction of the
time spent by AMS.
This led us to explore AMS documentation in depth in an effort to try a variety
of techniques such as caching, using OJ for JSON string generation etc. It
didn't yield the consistent results we were hoping to get. We loved the
developer experience of using AMS, but wanted better performance for our use
cases.
We came up with patterns that we can rely upon such as:
* We always use [JSON:API](https://jsonapi.org/) for our APIs
* We almost always serialize a homogenous list of objects (Example: An array of
movies)
On the other hand:
* AMS is designed to serialize JSON in several different formats, not just
JSON:API
* AMS can also handle lists that are not homogenous
This led us to build our own object serialization library that would be faster
because it would be tailored to our requirements. The usage of `fast_jsonapi`
internally on production environments resulted in significant performance
gains.
## Benchmark Setup
The benchmark setup is simple with classes for `Movie, Actor, MovieType, User`
on `movie_context.rb` for `fast_jsonapi` serializers and on `ams_context.rb`
for AMS serializers. We benchmark the serializers with 1, 25, 250, 1000 movies,
then we output the result.
We also ensure that JSON string output is equivalent to ensure neither library
is doing excess work compared to the other. Please checkout
`spec/object_serializer_performance_spec.rb`
## Benchmark Results
We benchmarked results for creating a Ruby Hash. This approach removes the
effect of chosen JSON string generation engines like OJ, Yajl etc. Benchmarks
indicate that `fast_jsonapi` consistently performs around 25 times faster
than AMS in generating a ruby hash.
We applied a similar benchmark on the operation to serialize the objects to a
JSON string. This approach helps with ensuring some important criterias, such
as:
* OJ is used as the JSON engine for benchmarking both AMS and `fast_jsonapi`
* The benchmark is easy to understand
* The benchmark helps to improve performance
* The benchmark influences design decisions for the gem
This gem is currently used in several APIs at Netflix and has reduced the
response times by more than half on many of these APIs. We truly appreciate the
Ruby and Rails communities and wanted to contribute in an effort to help
improve the performance of your APIs too.
## Epilogue
`fast_jsonapi` is not a replacement for AMS. AMS is a great gem, and it does
many things and is very flexible. We still use it for non JSON:API
serialization and deserialization. What started off as an internal performance
exercise evolved into `fast_jsonapi` and created an opportunity to give
something back to the awesome **Ruby and Rails communities**.
We are excited to share it with all of you since we believe that there will be
**no** end to this need for speed on APIs. :)

34
fast_jsonapi.gemspec Normal file
View File

@ -0,0 +1,34 @@
Gem::Specification.new do |gem|
gem.name = "fast_jsonapi"
gem.version = "1.1.1"
gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version=
gem.metadata = { "allowed_push_host" => "https://rubygems.org" } if gem.respond_to? :metadata=
gem.require_paths = ["lib"]
gem.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"]
gem.date = "2018-02-01"
gem.description = "JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects"
gem.email = ""
gem.extra_rdoc_files = [
"LICENSE.txt",
"README.md"
]
gem.files = Dir["lib/**/*"]
gem.homepage = "http://github.com/Netflix/fast_jsonapi"
gem.licenses = ["Apache-2.0"]
gem.rubygems_version = "2.5.1"
gem.summary = "fast JSON API(jsonapi.org) serializer"
gem.add_runtime_dependency(%q<activesupport>, [">= 4.2"])
gem.add_development_dependency(%q<activerecord>, [">= 4.2"])
gem.add_development_dependency(%q<skylight>, ["~> 1.3"])
gem.add_development_dependency(%q<rspec>, ["~> 3.5.0"])
gem.add_development_dependency(%q<oj>, ["~> 3.3"])
gem.add_development_dependency(%q<rspec-benchmark>, ["~> 0.3.0"])
gem.add_development_dependency(%q<bundler>, ["~> 1.0"])
gem.add_development_dependency(%q<byebug>, [">= 0"])
gem.add_development_dependency(%q<active_model_serializers>, ["~> 0.10.7"])
gem.add_development_dependency(%q<sqlite3>, ["~> 1.3"])
gem.add_development_dependency(%q<jsonapi-rb>, ["~> 0.5.0"])
gem.add_development_dependency(%q<jsonapi-serializers>, ["~> 1.0.0"])
end

View File

@ -1,37 +0,0 @@
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'jsonapi/serializer/version'
Gem::Specification.new do |gem|
gem.name = 'jsonapi-serializer'
gem.version = JSONAPI::Serializer::VERSION
gem.authors = ['JSON:API Serializer Community']
gem.email = ''
gem.summary = 'Fast JSON:API serialization library'
gem.description = 'Fast, simple and easy to use '\
'JSON:API serialization library (also known as fast_jsonapi).'
gem.homepage = 'https://github.com/jsonapi-serializer/jsonapi-serializer'
gem.licenses = ['Apache-2.0']
gem.files = Dir['lib/**/*']
gem.require_paths = ['lib']
gem.extra_rdoc_files = ['LICENSE.txt', 'README.md']
gem.add_runtime_dependency('activesupport', '>= 4.2')
gem.add_development_dependency('activerecord')
gem.add_development_dependency('bundler')
gem.add_development_dependency('byebug')
gem.add_development_dependency('ffaker')
gem.add_development_dependency('jsonapi-rspec', '>= 0.0.5')
gem.add_development_dependency('rake')
gem.add_development_dependency('rspec')
gem.add_development_dependency('rubocop')
gem.add_development_dependency('rubocop-performance')
gem.add_development_dependency('rubocop-rspec')
gem.add_development_dependency('simplecov')
gem.add_development_dependency('sqlite3')
gem.metadata['rubygems_mfa_required'] = 'true'
end

View File

@ -1,6 +1,9 @@
# frozen_string_literal: true
::ActiveRecord::Associations::Builder::HasOne.class_eval do
begin
require 'active_record'
::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
@ -9,10 +12,11 @@
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
rescue LoadError
# active_record can't be loaded so we shouldn't try to monkey-patch it.
end

View File

@ -1,12 +1,6 @@
# frozen_string_literal: true
require 'jsonapi/serializer/errors'
module FastJsonapi
require 'fast_jsonapi/object_serializer'
if defined?(::Rails)
require 'fast_jsonapi/railtie'
elsif defined?(::ActiveRecord)
require 'extensions/has_one'
end
end

View File

@ -1,5 +0,0 @@
require 'fast_jsonapi/scalar'
module FastJsonapi
class Attribute < Scalar; end
end

View File

@ -1,21 +0,0 @@
module FastJsonapi
class << self
# Calls either a Proc or a Lambda, making sure to never pass more parameters to it than it can receive
#
# @param [Proc] proc the Proc or Lambda to call
# @param [Array<Object>] *params any number of parameters to be passed to the Proc
# @return [Object] the result of the Proc call with the supplied parameters
def call_proc(proc, *params)
# The parameters array for a lambda created from a symbol (&:foo) differs
# from explictly defined procs/lambdas, so we can't deduce the number of
# parameters from the array length (and differs between Ruby 2.x and 3).
# In the case of negative arity -- unlimited/unknown argument count --
# just send the object to act as the method receiver.
if proc.arity.negative?
proc.call(params.first)
else
proc.call(*params.take(proc.parameters.length))
end
end
end
end

View File

@ -1,7 +1,2 @@
require 'jsonapi/serializer/instrumentation'
warn(
'DEPRECATION: Performance instrumentation is no longer automatic. See: ' \
'https://github.com/jsonapi-serializer/jsonapi-serializer' \
'#performance-instrumentation'
)
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'

View File

@ -0,0 +1,15 @@
require 'active_support/notifications'
module FastJsonapi
module ObjectSerializer
alias_method :serializable_hash_without_instrumentation, :serializable_hash
def serializable_hash
ActiveSupport::Notifications.instrument(SERIALIZABLE_HASH_NOTIFICATION, { name: self.class.name }) do
serializable_hash_without_instrumentation
end
end
end
end

View File

@ -0,0 +1,15 @@
require 'active_support/notifications'
module FastJsonapi
module ObjectSerializer
alias_method :serialized_json_without_instrumentation, :serialized_json
def serialized_json
ActiveSupport::Notifications.instrument(SERIALIZED_JSON_NOTIFICATION, { name: self.class.name }) do
serialized_json_without_instrumentation
end
end
end
end

View File

@ -1,3 +1,2 @@
require 'skylight'
warn('DEPRECATION: Skylight support was moved into the `skylight` gem.')
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'

View File

@ -0,0 +1,22 @@
require 'skylight'
require 'fast_jsonapi/instrumentation/serializable_hash'
module FastJsonapi
module Instrumentation
module Skylight
module Normalizers
class SerializableHash < Skylight::Normalizers::Normalizer
register FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION
CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION}".freeze
def normalize(trace, name, payload)
[ CAT, payload[:name], nil ]
end
end
end
end
end
end

View File

@ -0,0 +1,22 @@
require 'skylight'
require 'fast_jsonapi/instrumentation/serializable_hash'
module FastJsonapi
module Instrumentation
module Skylight
module Normalizers
class SerializedJson < Skylight::Normalizers::Normalizer
register FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION
CAT = "view.#{FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION}".freeze
def normalize(trace, name, payload)
[ CAT, payload[:name], nil ]
end
end
end
end
end
end

View File

@ -1,5 +0,0 @@
require 'fast_jsonapi/scalar'
module FastJsonapi
class Link < Scalar; end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
# Usage:
# class Movie
# def to_json(payload)
# FastJsonapi::MultiToJson.to_json(payload)
# end
# end
module FastJsonapi
module MultiToJson
# Result object pattern is from https://johnnunemaker.com/resilience-in-ruby/
# e.g. https://github.com/github/github-ds/blob/fbda5389711edfb4c10b6c6bad19311dfcb1bac1/lib/github/result.rb
class Result
def initialize(*rescued_exceptions)
@rescued_exceptions = if rescued_exceptions.empty?
[StandardError]
else
rescued_exceptions
end
@value = yield
@error = nil
rescue *rescued_exceptions => e
@error = e
end
def ok?
@error.nil?
end
def value!
if ok?
@value
else
raise @error
end
end
def rescue
return self if ok?
Result.new(*@rescued_exceptions) { yield(@error) }
end
end
def self.logger(device=nil)
return @logger = Logger.new(device) if device
@logger ||= Logger.new(IO::NULL)
end
# Encoder-compatible with default MultiJSON adapters and defaults
def self.to_json_method
encode_method = String.new(%(def _fast_to_json(object)\n ))
encode_method << Result.new(LoadError) {
require 'oj'
%(::Oj.dump(object, mode: :compat, time_format: :ruby, use_to_json: true))
}.rescue {
require 'yajl'
%(::Yajl::Encoder.encode(object))
}.rescue {
require 'jrjackson' unless defined?(::JrJackson)
%(::JrJackson::Json.dump(object))
}.rescue {
require 'json'
%(JSON.fast_generate(object, create_additions: false, quirks_mode: true))
}.rescue {
require 'gson'
%(::Gson::Encoder.new({}).encode(object))
}.rescue {
require 'active_support/json/encoding'
%(::ActiveSupport::JSON.encode(object))
}.rescue {
warn "No JSON encoder found. Falling back to `object.to_json`"
%(object.to_json)
}.value!
encode_method << "\nend"
end
def self.to_json(object)
_fast_to_json(object)
rescue NameError
define_to_json(FastJsonapi::MultiToJson)
_fast_to_json(object)
end
def self.define_to_json(receiver)
cl = caller_locations[0]
method_body = to_json_method
logger.debug { "Defining #{receiver}._fast_to_json as #{method_body.inspect}" }
receiver.instance_eval method_body, cl.absolute_path, cl.lineno
end
def self.reset_to_json!
undef :_fast_to_json if method_defined?(:_fast_to_json)
logger.debug { "Undefining #{receiver}._fast_to_json" }
end
end
end

View File

@ -1,14 +1,8 @@
# frozen_string_literal: true
require 'active_support'
require 'active_support/time'
require 'active_support/core_ext/object'
require 'active_support/concern'
require 'active_support/inflector'
require 'active_support/core_ext/numeric/time'
require 'fast_jsonapi/helpers'
require 'fast_jsonapi/attribute'
require 'fast_jsonapi/relationship'
require 'fast_jsonapi/link'
require 'fast_jsonapi/serialization_core'
module FastJsonapi
@ -16,12 +10,8 @@ module FastJsonapi
extend ActiveSupport::Concern
include SerializationCore
TRANSFORMS_MAPPING = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}.freeze
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'.freeze
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'.freeze
included do
# Set record_type based on the name of the serializer class
@ -35,23 +25,20 @@ module FastJsonapi
end
def serializable_hash
if self.class.is_collection?(@resource, @is_collection)
return hash_for_collection
end
return hash_for_collection if is_collection?(@resource)
hash_for_one_record
end
alias to_hash serializable_hash
alias_method :to_hash, :serializable_hash
def hash_for_one_record
serializable_hash = { data: nil }
serializable_hash[:meta] = @meta if @meta.present?
serializable_hash[:links] = @links if @links.present?
return serializable_hash unless @resource
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[:data] = self.class.record_hash(@resource)
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects) if @includes.present?
serializable_hash
end
@ -60,97 +47,72 @@ module FastJsonapi
data = []
included = []
fieldset = @fieldsets[self.class.record_type.to_sym]
@resource.each do |record|
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?
data << self.class.record_hash(record)
included.concat self.class.get_included_records(record, @includes, @known_included_objects) if @includes.present?
end
serializable_hash[:data] = data
serializable_hash[:included] = included if @includes.present?
serializable_hash[:meta] = @meta if @meta.present?
serializable_hash[:links] = @links if @links.present?
serializable_hash
end
def serialized_json
self.class.to_json(serializable_hash)
end
private
def process_options(options)
@fieldsets = deep_symbolize(options[:fields].presence || {})
@params = {}
return if options.blank?
@known_included_objects = Set.new
@known_included_objects = {}
@meta = options[:meta]
@links = options[:links]
@is_collection = options[:is_collection]
@params = options[:params] || {}
raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash)
if options[:include].present?
@includes = options[:include].reject(&:blank?).map(&:to_sym)
self.class.validate_includes!(@includes)
@includes = options[:include].delete_if(&:blank?).map(&:to_sym)
validate_includes!(@includes)
end
end
def deep_symbolize(collection)
if collection.is_a? Hash
collection.each_with_object({}) do |(k, v), hsh|
hsh[k.to_sym] = deep_symbolize(v)
def validate_includes!(includes)
return if includes.blank?
existing_relationships = self.class.relationships_to_serialize.keys.to_set
unless existing_relationships.superset?(includes.to_set)
raise ArgumentError, "One of keys from #{includes} is not specified as a relationship on the serializer"
end
elsif collection.is_a? Array
collection.map { |i| deep_symbolize(i) }
else
collection.to_sym
end
def is_collection?(resource)
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
end
class_methods do
# Detects a collection/enumerable
#
# @return [TrueClass] on a successful detection
def is_collection?(resource, force_is_collection = nil)
return force_is_collection unless force_is_collection.nil?
resource.is_a?(Enumerable) && !resource.respond_to?(:each_pair)
end
def inherited(subclass)
super(subclass)
subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
subclass.transform_method = transform_method
subclass.data_links = data_links.dup if data_links.present?
subclass.cache_store_instance = cache_store_instance
subclass.cache_store_options = cache_store_options
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
subclass.meta_to_serialize = meta_to_serialize
subclass.record_id = record_id
end
def reflected_record_type
return @reflected_record_type if defined?(@reflected_record_type)
@reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer'))
@reflected_record_type ||= begin
if self.name.end_with?('Serializer')
self.name.split('::').last.chomp('Serializer').underscore.to_sym
end
end
end
def set_key_transform(transform_name)
self.transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
# ensure that the record type is correctly transformed
if record_type
set_type(record_type)
# TODO: Remove dead code
elsif reflected_record_type
set_type(reflected_record_type)
end
mapping = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}
@transform_method = mapping[transform_name.to_sym]
end
def run_key_transform(input)
if transform_method.present?
if @transform_method.present?
input.to_s.send(*@transform_method).to_sym
else
input.to_sym
@ -166,186 +128,109 @@ module FastJsonapi
self.record_type = run_key_transform(type_name)
end
def set_id(id_name = nil, &block)
self.record_id = block || id_name
def set_id(id_name)
self.record_id = id_name
end
def cache_options(cache_options)
# FIXME: remove this if block once deprecated cache_options are not supported anymore
unless cache_options.key?(:store)
# fall back to old, deprecated behaviour because no store was passed.
# we assume the user explicitly wants new behaviour if he passed a
# store because this is the new syntax.
deprecated_cache_options(cache_options)
return
end
self.cache_store_instance = cache_options[:store]
self.cache_store_options = cache_options.except(:store)
end
# FIXME: remove this method once deprecated cache_options are not supported anymore
def deprecated_cache_options(cache_options)
warn('DEPRECATION WARNING: `store:` is a required cache option, we will default to `Rails.cache` for now. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.')
%i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
warn("DEPRECATION WARNING: `#{key}` is a deprecated cache option and will have no effect soon. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.")
end
self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
self.cache_store_options = {
expires_in: cache_options[:cache_length] || 5.minutes,
race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
}
self.cached = cache_options[:enabled] || false
self.cache_length = cache_options[:cache_length] || 5.minutes
end
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 attributes_to_serialize.nil?
# to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
block = attributes_list.pop if attributes_list.last.is_a?(Proc)
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] = Attribute.new(
key: key,
method: block || method_name,
options: options
)
attributes_to_serialize[key] = block || method_name
end
end
alias_method :attribute, :attributes
def add_relationship(relationship)
def add_relationship(name, 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?
# TODO: Remove this undocumented option.
# Delegate the caching to the serializer exclusively.
if relationship.cached
cachable_relationships_to_serialize[relationship.name] = relationship
if !relationship[:cached]
self.uncachable_relationships_to_serialize[name] = relationship
else
uncachable_relationships_to_serialize[relationship.name] = relationship
self.cachable_relationships_to_serialize[name] = relationship
end
relationships_to_serialize[relationship.name] = relationship
self.relationships_to_serialize[name] = relationship
end
def has_many(relationship_name, options = {}, &block)
relationship = create_relationship(relationship_name, :has_many, options, block)
add_relationship(relationship)
end
def has_one(relationship_name, options = {}, &block)
relationship = create_relationship(relationship_name, :has_one, options, block)
add_relationship(relationship)
end
def belongs_to(relationship_name, options = {}, &block)
relationship = create_relationship(relationship_name, :belongs_to, options, block)
add_relationship(relationship)
end
def meta(meta_name = nil, &block)
self.meta_to_serialize = block || meta_name
end
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
id_postfix = '_ids'
else
base_serialization_key = base_key
id_postfix = '_id'
end
polymorphic = fetch_polymorphic_option(options)
Relationship.new(
owner: self,
key: options[:key] || run_key_transform(base_key),
def has_many(relationship_name, options = {})
name = relationship_name.to_sym
singular_name = relationship_name.to_s.singularize
serializer_key = options[:serializer] || singular_name.to_sym
key = options[:key] || run_key_transform(relationship_name)
record_type = options[:record_type] || run_key_transform(singular_name)
relationship = {
key: key,
name: name,
id_method_name: compute_id_method_name(
options[:id_method_name],
"#{base_serialization_key}#{id_postfix}".to_sym,
polymorphic,
options[:serializer],
block
),
record_type: options[:record_type],
id_method_name: options[:id_method_name] || (singular_name + '_ids').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
object_block: block,
serializer: options[:serializer],
relationship_type: relationship_type,
cached: options[:cached],
polymorphic: polymorphic,
conditional_proc: options[:if],
transform_method: @transform_method,
meta: options[:meta],
links: options[:links],
lazy_load_data: options[:lazy_load_data]
)
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_many,
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
}
add_relationship(name, relationship)
end
def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
if block.present? || serializer.is_a?(Proc) || polymorphic
custom_id_method_name || :id
else
custom_id_method_name || id_method_name_from_relationship
end
def belongs_to(relationship_name, options = {})
name = relationship_name.to_sym
serializer_key = options[:serializer] || relationship_name.to_sym
key = options[:key] || run_key_transform(relationship_name)
record_type = options[:record_type] || run_key_transform(relationship_name)
add_relationship(name, {
key: key,
name: name,
id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :belongs_to,
cached: options[:cached] || true,
polymorphic: fetch_polymorphic_option(options)
})
end
def serializer_for(name)
def has_one(relationship_name, options = {})
name = relationship_name.to_sym
serializer_key = options[:serializer] || name
key = options[:key] || run_key_transform(relationship_name)
record_type = options[:record_type] || run_key_transform(relationship_name)
add_relationship(name, {
key: key,
name: name,
id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_one,
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
})
end
def compute_serializer_name(serializer_key)
namespace = self.name.gsub(/()?\w+Serializer$/, '')
serializer_name = "#{name.to_s.demodulize.classify}Serializer"
serializer_class_name = namespace + serializer_name
begin
serializer_class_name.constantize
rescue NameError
raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \
"Attempted to find '#{serializer_class_name}'. " \
'Consider specifying the serializer directly through options[:serializer].'
end
serializer_name = serializer_key.to_s.classify + 'Serializer'
return (namespace + serializer_name).to_sym if namespace.present?
(serializer_key.to_s.classify + 'Serializer').to_sym
end
def fetch_polymorphic_option(options)
option = options[:polymorphic]
return false unless option.present?
return option if option.respond_to? :keys
{}
end
# def link(link_name, link_method_name = nil, &block)
def link(*params, &block)
self.data_links = {} if data_links.nil?
options = params.last.is_a?(Hash) ? params.pop : {}
link_name = params.first
link_method_name = params[-1]
key = run_key_transform(link_name)
data_links[key] = Link.new(
key: key,
method: block || link_method_name,
options: options
)
end
def validate_includes!(includes)
return if includes.blank?
parse_includes_list(includes).each_key do |include_item|
relationship_to_include = relationships_to_serialize[include_item]
raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_include
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
end
end
end
end
end

View File

@ -1,11 +0,0 @@
# 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

@ -1,236 +0,0 @@
module FastJsonapi
class Relationship
attr_reader :owner, :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :meta, :lazy_load_data
def initialize(
owner:,
key:,
name:,
id_method_name:,
record_type:,
object_method_name:,
object_block:,
serializer:,
relationship_type:,
polymorphic:,
conditional_proc:,
transform_method:,
links:,
meta:,
cached: false,
lazy_load_data: false
)
@owner = owner
@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
@transform_method = transform_method
@links = links || {}
@meta = meta || {}
@lazy_load_data = lazy_load_data
@record_types_for = {}
@serializers_for_name = {}
end
def serialize(record, included, serialization_params, output_hash)
if include_relationship?(record, serialization_params)
empty_case = relationship_type == :has_many ? [] : nil
output_hash[key] = {}
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case unless lazy_load_data && !included
add_meta_hash(record, serialization_params, output_hash) if meta.present?
add_links_hash(record, serialization_params, output_hash) if links.present?
end
end
def fetch_associated_object(record, params)
return FastJsonapi.call_proc(object_block, record, params) unless object_block.nil?
record.send(object_method_name)
end
def include_relationship?(record, serialization_params)
if conditional_proc.present?
FastJsonapi.call_proc(conditional_proc, record, serialization_params)
else
true
end
end
def serializer_for(record, serialization_params)
# TODO: Remove this, dead code...
if @static_serializer
@static_serializer
elsif polymorphic
name = polymorphic[record.class] if polymorphic.is_a?(Hash)
name ||= record.class.name
serializer_for_name(name)
elsif serializer.is_a?(Proc)
FastJsonapi.call_proc(serializer, record, serialization_params)
elsif object_block
serializer_for_name(record.class.name)
else
# TODO: Remove this, dead code...
raise "Unknown serializer for object #{record.inspect}"
end
end
def static_serializer
initialize_static_serializer unless @initialized_static_serializer
@static_serializer
end
def static_record_type
initialize_static_serializer unless @initialized_static_serializer
@static_record_type
end
private
def ids_hash_from_record_and_relationship(record, params = {})
initialize_static_serializer unless @initialized_static_serializer
return ids_hash(fetch_id(record, params), @static_record_type) if @static_record_type
return unless associated_object = fetch_associated_object(record, params)
if associated_object.respond_to? :map
return associated_object.map do |object|
id_hash_from_record object, params
end
end
id_hash_from_record associated_object, params
end
def id_hash_from_record(record, params)
associated_record_type = record_type_for(record, params)
id_hash(record.public_send(id_method_name), associated_record_type)
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(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)
if object_block.present?
object = FastJsonapi.call_proc(object_block, record, params)
return object.map { |item| item.public_send(id_method_name) } if object.respond_to? :map
return object.try(id_method_name)
end
record.public_send(id_method_name)
end
def add_links_hash(record, params, output_hash)
output_hash[key][:links] = if links.is_a?(Symbol)
record.public_send(links)
else
links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash)
end
end
end
def add_meta_hash(record, params, output_hash)
output_hash[key][:meta] = if meta.is_a?(Proc)
FastJsonapi.call_proc(meta, record, params)
else
meta
end
end
def run_key_transform(input)
if transform_method.present?
input.to_s.send(*transform_method).to_sym
else
input.to_sym
end
end
def initialize_static_serializer
return if @initialized_static_serializer
@static_serializer = compute_static_serializer
@static_record_type = compute_static_record_type
@initialized_static_serializer = true
end
def compute_static_serializer
if polymorphic
# polymorphic without a specific serializer --
# the serializer is determined on a record-by-record basis
nil
elsif serializer.is_a?(Symbol) || serializer.is_a?(String)
# a serializer was explicitly specified by name -- determine the serializer class
serializer_for_name(serializer)
elsif serializer.is_a?(Proc)
# the serializer is a Proc to be executed per object -- not static
nil
elsif serializer
# something else was specified, e.g. a specific serializer class -- return it
serializer
elsif object_block
# an object block is specified without a specific serializer --
# assume the objects might be different and infer the serializer by their class
nil
else
# no serializer information was provided -- infer it from the relationship name
serializer_name = name.to_s
serializer_name = serializer_name.singularize if relationship_type.to_sym == :has_many
serializer_for_name(serializer_name)
end
end
def serializer_for_name(name)
@serializers_for_name[name] ||= owner.serializer_for(name)
end
def record_type_for(record, serialization_params)
# if the record type is static, return it
return @static_record_type if @static_record_type
# if not, use the record type of the serializer, and memoize the transformed version
serializer = serializer_for(record, serialization_params)
@record_types_for[serializer] ||= run_key_transform(serializer.record_type)
end
def compute_static_record_type
if polymorphic
nil
elsif record_type
run_key_transform(record_type)
elsif @static_serializer
run_key_transform(@static_serializer.record_type)
end
end
end
end

View File

@ -1,29 +0,0 @@
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)
if method.is_a?(Proc)
output_hash[key] = FastJsonapi.call_proc(method, record, serialization_params)
else
output_hash[key] = record.public_send(method)
end
end
end
def conditionally_allowed?(record, serialization_params)
if conditional_proc.present?
FastJsonapi.call_proc(conditional_proc, record, serialization_params)
else
true
end
end
end
end

View File

@ -1,12 +1,9 @@
# frozen_string_literal: true
require 'active_support'
require 'active_support/concern'
require 'digest/sha1'
require 'fast_jsonapi/multi_to_json'
module FastJsonapi
MandatoryField = Class.new(StandardError)
module SerializationCore
extend ActiveSupport::Concern
@ -16,184 +13,107 @@ module FastJsonapi
:relationships_to_serialize,
:cachable_relationships_to_serialize,
:uncachable_relationships_to_serialize,
:transform_method,
:record_type,
:record_id,
:cache_store_instance,
:cache_store_options,
:data_links,
:meta_to_serialize
:cache_length,
:cached
end
end
class_methods do
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
def id_hash(id, record_type)
return { id: id.to_s, type: record_type } if id.present?
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: record.id.to_s, type: record_type }
end
def ids_hash_from_record_and_relationship(record, relationship)
polymorphic = relationship[:polymorphic]
return ids_hash(
record.public_send(relationship[:id_method_name]),
relationship[:record_type]
) unless polymorphic
object_method_name = relationship.fetch(:object_method_name, relationship[:name])
return unless associated_object = record.send(object_method_name)
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 attributes_hash(record)
attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash|
attr_hash[key] = method.is_a?(Proc) ? method.call(record) : record.public_send(method)
end
end
def links_hash(record, params = {})
data_links.each_with_object({}) do |(_k, link), hash|
link.serialize(record, params, hash)
end
end
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
end
def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
def relationships_hash(record, relationships = nil)
relationships = relationships_to_serialize if relationships.nil?
relationships = relationships.slice(*fieldset) if fieldset.present?
relationships = {} if fieldset == []
relationships.each_with_object({}) do |(key, relationship), hash|
included = includes_list.present? && includes_list.include?(key)
relationship.serialize(record, included, params, hash)
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) || empty_case
}
end
end
def meta_hash(record, params = {})
FastJsonapi.call_proc(meta_to_serialize, record, params)
end
def record_hash(record, fieldset, includes_list, params = {})
if cache_store_instance
cache_opts = record_cache_options(cache_store_options, fieldset, includes_list, params)
record_hash = cache_store_instance.fetch(record, **cache_opts) do
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[: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?
def record_hash(record)
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do
id = record_id ? record.send(record_id) : record.id
temp_hash = id_hash(id, record_type) || { id: nil, type: record_type }
temp_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
temp_hash[:relationships] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present?
temp_hash
end
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[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize)) if uncachable_relationships_to_serialize.present?
record_hash
else
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[: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?
end
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
id = record_id ? record.send(record_id) : record.id
record_hash = id_hash(id, record_type) || { id: nil, type: record_type }
record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record) if relationships_to_serialize.present?
record_hash
end
# Cache options helper. Use it to adapt cache keys/rules.
#
# If a fieldset is specified, it modifies the namespace to include the
# fields from the fieldset.
#
# @param options [Hash] default cache options
# @param fieldset [Array, nil] passed fieldset values
# @param includes_list [Array, nil] passed included values
# @param params [Hash] the serializer params
#
# @return [Hash] processed options hash
# rubocop:disable Lint/UnusedMethodArgument
def record_cache_options(options, fieldset, includes_list, params)
return options unless fieldset
options = options ? options.dup : {}
options[:namespace] ||= 'jsonapi-serializer'
fieldset_key = fieldset.join('_')
# Use a fixed-length fieldset key if the current length is more than
# the length of a SHA1 digest
if fieldset_key.length > 40
fieldset_key = Digest::SHA1.hexdigest(fieldset_key)
end
options[:namespace] = "#{options[:namespace]}-fieldset:#{fieldset_key}"
options
end
# rubocop:enable Lint/UnusedMethodArgument
def id_from_record(record, params)
return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc)
return record.send(record_id) if record_id
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
record.id
end
# It chops out the root association (first part) from each include.
#
# It keeps an unique list and collects all of the rest of the include
# value to hand it off to the next related to include serializer.
#
# This method will turn that include array into a Hash that looks like:
#
# {
# authors: Set.new([
# 'books',
# 'books.genre',
# 'books.genre.books',
# 'books.genre.books.authors',
# 'books.genre.books.genre'
# ]),
# genre: Set.new(['books'])
# }
#
# Because the serializer only cares about the root associations
# included, it only needs the first segment of each include
# (for books, it's the "authors" and "genre") and it doesn't need to
# waste cycles parsing the rest of the include value. That will be done
# by the next serializer in line.
#
# @param includes_list [List] to be parsed
# @return [Hash]
def parse_includes_list(includes_list)
includes_list.each_with_object({}) do |include_item, include_sets|
include_base, include_remainder = include_item.to_s.split('.', 2)
include_sets[include_base.to_sym] ||= Set.new
include_sets[include_base.to_sym] << include_remainder if include_remainder
end
# Override #to_json for alternative implementation
def to_json(payload)
FastJsonapi::MultiToJson.to_json(payload) if payload.present?
end
# includes handler
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
return unless includes_list.present?
return [] unless relationships_to_serialize
includes_list = parse_includes_list(includes_list)
includes_list.each_with_object([]) do |include_item, included_records|
relationship_item = relationships_to_serialize[include_item.first]
next unless relationship_item&.include_relationship?(record, params)
included_objects = Array(relationship_item.fetch_associated_object(record, params))
next if included_objects.empty?
static_serializer = relationship_item.static_serializer
static_record_type = relationship_item.static_record_type
def get_included_records(record, includes_list, known_included_objects)
includes_list.each_with_object([]) do |item, included_records|
object_method_name = @relationships_to_serialize[item][:object_method_name]
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]
included_objects = record.send(object_method_name)
next if included_objects.blank?
included_objects = [included_objects] unless relationship_type == :has_many
included_objects.each do |inc_obj|
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type
if include_item.last.any?
serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end
code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
next if known_included_objects.include?(code)
known_included_objects << code
included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params)
code = "#{record_type}_#{inc_obj.id}"
next if known_included_objects.key?(code)
known_included_objects[code] = inc_obj
included_records << serializer.record_hash(inc_obj)
end
end
end

View File

@ -1,3 +0,0 @@
module FastJsonapi
VERSION = JSONAPI::Serializer::VERSION
end

View File

@ -1,6 +1,6 @@
<% module_namespacing do -%>
class <%= class_name %>Serializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
attributes <%= attributes_names.join(", ") %>
end
<% end -%>

View File

@ -1,12 +0,0 @@
require 'fast_jsonapi'
module JSONAPI
module Serializer
# TODO: Move and cleanup the old implementation...
def self.included(base)
base.class_eval do
include FastJsonapi::ObjectSerializer
end
end
end
end

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
module JSONAPI
module Serializer
class Error < StandardError; end
class UnsupportedIncludeError < Error
attr_reader :include_item, :klass
def initialize(include_item, klass)
super()
@include_item = include_item
@klass = klass
end
def message
"#{include_item} is not specified as a relationship on #{klass}"
end
end
end
end

View File

@ -1,28 +0,0 @@
require 'active_support'
require 'active_support/notifications'
module JSONAPI
module Serializer
# Support for instrumentation
module Instrumentation
# Performance instrumentation namespace
NOTIFICATION_NAMESPACE = 'render.jsonapi-serializer.'.freeze
# Patch methods to use instrumentation...
%w[
serializable_hash
get_included_records
relationships_hash
].each do |method_name|
define_method(method_name) do |*args|
ActiveSupport::Notifications.instrument(
NOTIFICATION_NAMESPACE + method_name,
{ name: self.class.name, serializer: self.class }
) do
super(*args)
end
end
end
end
end
end

View File

@ -1,5 +0,0 @@
module JSONAPI
module Serializer
VERSION = '2.2.0'.freeze
end
end

View File

@ -0,0 +1,44 @@
# Performance using Fast JSON API
We have been getting a few questions on Github about [Fast JSON APIs](https://github.com/Netflix/fast_jsonapi) performance statistics and the methodology used to measure the performance. This article is an attempt at addressing this aspect of the gem.
## Prologue
With use cases like infinite scroll on complex models and bulk update on index pages, we started observing performance degradation on our Rails APIs. Our first step was to enable instrumentation and then tune for performance. We realized that, on average, more than 50% of the time was being spent on AMS serialization. At the same time, we had a couple of APIs that were simply proxying requests on top of a non-Rails, non-JSON API endpoint. Guess what? The non-Rails endpoints were giving us serialized JSON back in a fraction of the time spent by AMS.
This led us to explore AMS documentation in depth in an effort to try a variety of techniques such as caching, using OJ for JSON string generation etc. It didnt yield the consistent results we were hoping to get. We loved the developer experience of using AMS, but wanted better performance for our use cases.
We came up with patterns that we can rely upon such as:
* We always use [JSON:API](http://jsonapi.org/) for our APIs
* We almost always serialize a homogenous list of objects (Example: An array of movies)
On the other hand:
* AMS is designed to serialize JSON in several different formats, not just JSON:API
* AMS can also handle lists that are not homogenous
This led us to build our own object serialization library that would be faster because it would be tailored to our requirements. The usage of fast_jsonapi internally on production environments resulted in significant performance gains.
## Benchmark Setup
The benchmark setup is simple with classes for ``` Movie, Actor, MovieType, User ``` on ```movie_context.rb``` for fast_jsonapi serializers and on ```ams_context.rb``` for AMS serializers. We benchmark the serializers with ```1, 25, 250, 1000``` movies, then we output the result. We also ensure that JSON string output is equivalent to ensure neither library is doing excess work compared to the other. Please checkout [object_serializer_performance_spec](https://github.com/Netflix/fast_jsonapi/blob/master/spec/lib/object_serializer_performance_spec.rb).
## Benchmark Results
We benchmarked results for creating a Ruby Hash. This approach removes the effect of chosen JSON string generation engines like OJ, Yajl etc. Benchmarks indicate that fast_jsonapi consistently performs around ```25 times``` faster than AMS in generating a ruby hash.
We applied a similar benchmark on the operation to serialize the objects to a JSON string. This approach helps with ensuring some important criterias, such as:
* OJ is used as the JSON engine for benchmarking both AMS and fast_jsonapi
* The benchmark is easy to understand
* The benchmark helps to improve performance
* The benchmark influences design decisions for the gem
This gem is currently used in several APIs at Netflix and has reduced the response times by more than half on many of these APIs. We truly appreciate the Ruby and Rails communities and wanted to contribute in an effort to help improve the performance of your APIs too.
## Epilogue
[Fast JSON API](https://github.com/Netflix/fast_jsonapi) is not a replacement for AMS. AMS is a great gem, and it does many things and is very flexible. We still use it for non JSON:API serialization and deserialization. What started off as an internal performance exercise evolved into fast_jsonapi and created an opportunity to give something back to the awesome **Ruby and Rails communities**.
We are excited to share it with all of you since we believe that there will be **no** end to this need for speed on APIs. :)

View File

@ -1,40 +0,0 @@
require 'active_support'
require 'active_support/cache'
class User
attr_accessor :uid, :first_name, :last_name, :email
def self.fake(id = nil)
faked = new
faked.uid = id || SecureRandom.uuid
faked.first_name = FFaker::Name.first_name
faked.last_name = FFaker::Name.last_name
faked.email = FFaker::Internet.email
faked
end
end
class NoSerializerUser < User
end
class UserSerializer
include JSONAPI::Serializer
set_id :uid
attributes :first_name, :last_name, :email
meta do |obj|
{
email_length: obj.email.size
}
end
end
module Cached
class UserSerializer < ::UserSerializer
cache_options(
store: ActiveSupport::Cache::MemoryStore.new,
namespace: 'test'
)
end
end

View File

@ -1,80 +0,0 @@
require 'active_support'
require 'active_support/cache'
require 'jsonapi/serializer/instrumentation'
class Actor < User
attr_accessor :movies, :movie_ids
def self.fake(id = nil)
faked = super(id)
faked.movies = []
faked.movie_ids = []
faked
end
def movie_urls
{
movie_url: movies[0]&.url
}
end
end
class ActorSerializer < UserSerializer
set_type :actor
attribute :email, if: ->(_object, params) { params[:conditionals_off].nil? }
has_many(
:played_movies,
serializer: :movie,
links: :movie_urls,
if: ->(_object, params) { params[:conditionals_off].nil? }
) do |object|
object.movies
end
end
class CamelCaseActorSerializer
include JSONAPI::Serializer
set_key_transform :camel
set_id :uid
set_type :user_actor
attributes :first_name
link :movie_url do |obj|
obj.movie_urls.values[0]
end
has_many(
:played_movies,
serializer: :movie
) do |object|
object.movies
end
end
class BadMovieSerializerActorSerializer < ActorSerializer
has_many :played_movies, serializer: :bad, object_method_name: :movies
end
module Cached
class ActorSerializer < ::ActorSerializer
# TODO: Fix this, the serializer gets cached on inherited classes...
has_many :played_movies, serializer: :movie do |object|
object.movies
end
cache_options(
store: ActiveSupport::Cache::MemoryStore.new,
namespace: 'test'
)
end
end
module Instrumented
class ActorSerializer < ::ActorSerializer
include ::JSONAPI::Serializer::Instrumentation
end
end

127
spec/fixtures/movie.rb vendored
View File

@ -1,127 +0,0 @@
class Movie
attr_accessor(
:id,
:name,
:year,
:actor_or_user,
:actors,
:actor_ids,
:polymorphics,
:owner,
:owner_id
)
def self.fake(id = nil)
faked = new
faked.id = id || SecureRandom.uuid
faked.name = FFaker::Movie.title
faked.year = FFaker::Vehicle.year
faked.actors = []
faked.actor_ids = []
faked.polymorphics = []
faked
end
def url(obj = nil)
@url ||= FFaker::Internet.http_url
return @url if obj.nil?
"#{@url}?#{obj.hash}"
end
def owner=(ownr)
@owner = ownr
@owner_id = ownr.uid
end
def actors=(acts)
@actors = acts
@actor_ids = actors.map do |actor|
actor.movies << self
actor.uid
end
end
end
class MovieSerializer
include JSONAPI::Serializer
set_type :movie
attribute :released_in_year, &:year
attributes :name
attribute :release_year do |object, _params|
object.year
end
link :self, :url
belongs_to :owner, serializer: UserSerializer
belongs_to :actor_or_user,
id_method_name: :uid,
polymorphic: {
Actor => :actor,
User => :user
}
has_many(
:actors,
meta: proc { |record, _| { count: record.actors.length } },
links: {
actors_self: :url,
related: ->(obj) { obj.url(obj) }
}
)
has_one(
:creator,
object_method_name: :owner,
id_method_name: :uid,
serializer: ->(object, _params) { UserSerializer if object.is_a?(User) }
)
has_many(
:actors_and_users,
id_method_name: :uid,
polymorphic: {
Actor => :actor,
User => :user
}
) do |obj|
obj.polymorphics
end
has_many(
:dynamic_actors_and_users,
id_method_name: :uid,
polymorphic: true
) do |obj|
obj.polymorphics
end
has_many(
:auto_detected_actors_and_users,
id_method_name: :uid
) do |obj|
obj.polymorphics
end
end
module Cached
class MovieSerializer < ::MovieSerializer
cache_options(
store: ActorSerializer.cache_store_instance,
namespace: 'test'
)
has_one(
:creator,
id_method_name: :uid,
serializer: :actor,
# TODO: Remove this undocumented option.
# Delegate the caching to the serializer exclusively.
cached: false
) do |obj|
obj.owner
end
end
end

View File

@ -1,63 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) do
act = Actor.fake
act.movies = [Movie.fake]
act
end
let(:params) { {} }
let(:serialized) do
ActorSerializer.new(actor, params).serializable_hash.as_json
end
describe 'attributes' do
it do
expect(serialized['data']).to have_id(actor.uid)
expect(serialized['data']).to have_type('actor')
expect(serialized['data'])
.to have_jsonapi_attributes('first_name', 'last_name', 'email').exactly
expect(serialized['data']).to have_attribute('first_name')
.with_value(actor.first_name)
expect(serialized['data']).to have_attribute('last_name')
.with_value(actor.last_name)
expect(serialized['data']).to have_attribute('email')
.with_value(actor.email)
end
context 'with nil identifier' do
before { actor.uid = nil }
it { expect(serialized['data']).to have_id(nil) }
end
context 'with `if` conditions' do
let(:params) { { params: { conditionals_off: 'yes' } } }
it do
expect(serialized['data']).not_to have_attribute('email')
end
end
context 'with include and fields' do
let(:params) do
{
include: [:played_movies],
fields: { movie: [:release_year], actor: [:first_name] }
}
end
it do
expect(serialized['data'])
.to have_jsonapi_attributes(:first_name).exactly
expect(serialized['included']).to include(
have_type('movie')
.and(have_id(actor.movies[0].id))
.and(have_jsonapi_attributes('release_year').exactly)
)
end
end
end
end

View File

@ -1,80 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) do
faked = Actor.fake
movie = Movie.fake
movie.owner = User.fake
movie.actors = [faked]
faked.movies = [movie]
faked
end
let(:cache_store) { Cached::ActorSerializer.cache_store_instance }
describe 'with caching' do
it do
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
Cached::ActorSerializer.new(
[actor, actor], include: ['played_movies', 'played_movies.owner']
).serializable_hash
expect(cache_store.delete(actor, namespace: 'test')).to be(true)
expect(cache_store.delete(actor.movies[0], namespace: 'test')).to be(true)
expect(
cache_store.delete(actor.movies[0].owner, namespace: 'test')
).to be(false)
end
context 'without relationships' do
let(:user) { User.fake }
let(:serialized) { Cached::UserSerializer.new(user).serializable_hash.as_json }
it do
expect(serialized['data']).not_to have_key('relationships')
end
end
end
describe 'with caching and different fieldsets' do
context 'when fieldset is provided' do
it 'includes the fieldset in the namespace' do
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
Cached::ActorSerializer.new(
[actor], fields: { actor: %i[first_name] }
).serializable_hash
# Expect cached keys to match the passed fieldset
expect(cache_store.read(actor, namespace: 'test-fieldset:first_name')[:attributes].keys).to eq(%i[first_name])
Cached::ActorSerializer.new(
[actor]
).serializable_hash
# Expect cached keys to match all valid actor fields (no fieldset)
expect(cache_store.read(actor, namespace: 'test')[:attributes].keys).to eq(%i[first_name last_name email])
expect(cache_store.delete(actor, namespace: 'test')).to be(true)
expect(cache_store.delete(actor, namespace: 'test-fieldset:first_name')).to be(true)
end
end
context 'when long fieldset is provided' do
let(:actor_keys) { %i[first_name last_name more_fields yet_more_fields so_very_many_fields] }
let(:digest_key) { Digest::SHA1.hexdigest(actor_keys.join('_')) }
it 'includes the hashed fieldset in the namespace' do
Cached::ActorSerializer.new(
[actor], fields: { actor: actor_keys }
).serializable_hash
expect(cache_store.read(actor, namespace: "test-fieldset:#{digest_key}")[:attributes].keys).to eq(
%i[first_name last_name]
)
expect(cache_store.delete(actor, namespace: "test-fieldset:#{digest_key}")).to be(true)
end
end
end
end

View File

@ -1,25 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) { Actor.fake }
let(:params) { {} }
describe 'with errors' do
it do
expect do
BadMovieSerializerActorSerializer.new(
actor, include: ['played_movies']
)
end.to raise_error(
NameError, /cannot resolve a serializer class for 'bad'/
)
end
it do
expect { ActorSerializer.new(actor, include: ['bad_include']) }
.to raise_error(
JSONAPI::Serializer::UnsupportedIncludeError, /bad_include is not specified as a relationship/
)
end
end
end

View File

@ -1,29 +0,0 @@
require 'spec_helper'
# Needed to subscribe to `active_support/notifications`
require 'concurrent'
RSpec.describe JSONAPI::Serializer do
let(:serializer) do
Instrumented::ActorSerializer.new(Actor.fake)
end
it do
payload = event_name = nil
notification_name =
"#{::JSONAPI::Serializer::Instrumentation::NOTIFICATION_NAMESPACE}serializable_hash"
ActiveSupport::Notifications.subscribe(
notification_name
) do |ev_name, _s, _f, _i, ev_payload|
event_name = ev_name
payload = ev_payload
end
expect(serializer.serializable_hash).not_to be_nil
expect(event_name).to eq('render.jsonapi-serializer.serializable_hash')
expect(payload[:name]).to eq(serializer.class.name)
expect(payload[:serializer]).to eq(serializer.class)
end
end

View File

@ -1,19 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) { Actor.fake }
let(:params) { {} }
let(:serialized) do
CamelCaseActorSerializer.new(actor, params).serializable_hash.as_json
end
describe 'camel case key tranformation' do
it do
expect(serialized['data']).to have_id(actor.uid)
expect(serialized['data']).to have_type('UserActor')
expect(serialized['data']).to have_attribute('FirstName')
expect(serialized['data']).to have_relationship('PlayedMovies')
expect(serialized['data']).to have_link('MovieUrl').with_value(nil)
end
end
end

View File

@ -1,47 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:movie) do
faked = Movie.fake
faked.actors = [Actor.fake]
faked
end
let(:params) { {} }
let(:serialized) do
MovieSerializer.new(movie, params).serializable_hash.as_json
end
describe 'links' do
it do
expect(serialized['data']).to have_link('self').with_value(movie.url)
expect(serialized['data']['relationships']['actors'])
.to have_link('actors_self').with_value(movie.url)
expect(serialized['data']['relationships']['actors'])
.to have_link('related').with_value(movie.url(movie))
end
context 'with included records' do
let(:serialized) do
ActorSerializer.new(movie.actors[0]).serializable_hash.as_json
end
it do
expect(serialized['data']['relationships']['played_movies'])
.to have_link('movie_url').with_value(movie.url)
end
end
context 'with root link' do
let(:params) do
{
links: { 'root_link' => FFaker::Internet.http_url }
}
end
it do
expect(serialized)
.to have_link('root_link').with_value(params[:links]['root_link'])
end
end
end
end

View File

@ -1,25 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:user) { User.fake }
let(:params) { {} }
let(:serialized) do
UserSerializer.new(user, params).serializable_hash.as_json
end
it do
expect(serialized['data']).to have_meta('email_length' => user.email.size)
end
context 'with root meta' do
let(:params) do
{
meta: { 'code' => FFaker::Internet.password }
}
end
it do
expect(serialized).to have_meta(params[:meta])
end
end
end

View File

@ -1,146 +0,0 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:movie) do
mov = Movie.fake
mov.actors = rand(2..5).times.map { Actor.fake }
mov.owner = User.fake
poly_act = Actor.fake
poly_act.movies = [Movie.fake]
mov.polymorphics = [User.fake, poly_act]
mov.actor_or_user = Actor.fake
mov
end
let(:params) { {} }
let(:serialized) do
MovieSerializer.new(movie, params).serializable_hash.as_json
end
describe 'relationships' do
it do
actors_rel = movie.actors.map { |a| { 'id' => a.uid, 'type' => 'actor' } }
expect(serialized['data'])
.to have_relationship('actors').with_data(actors_rel)
expect(serialized['data'])
.to have_relationship('owner')
.with_data('id' => movie.owner.uid, 'type' => 'user')
expect(serialized['data'])
.to have_relationship('creator')
.with_data('id' => movie.owner.uid, 'type' => 'user')
expect(serialized['data'])
.to have_relationship('actors_and_users')
.with_data(
[
{ 'id' => movie.polymorphics[0].uid, 'type' => 'user' },
{ 'id' => movie.polymorphics[1].uid, 'type' => 'actor' }
]
)
expect(serialized['data'])
.to have_relationship('dynamic_actors_and_users')
.with_data(
[
{ 'id' => movie.polymorphics[0].uid, 'type' => 'user' },
{ 'id' => movie.polymorphics[1].uid, 'type' => 'actor' }
]
)
expect(serialized['data'])
.to have_relationship('auto_detected_actors_and_users')
.with_data(
[
{ 'id' => movie.polymorphics[0].uid, 'type' => 'user' },
{ 'id' => movie.polymorphics[1].uid, 'type' => 'actor' }
]
)
end
describe 'has relationship meta' do
it do
expect(serialized['data']['relationships']['actors'])
.to have_meta('count' => movie.actors.length)
end
end
context 'with include' do
let(:params) do
{ include: [:actors] }
end
it do
movie.actors.each do |actor|
expect(serialized['included']).to include(
have_type('actor')
.and(have_id(actor.uid))
.and(have_relationship('played_movies')
.with_data([{ 'id' => actor.movies[0].id, 'type' => 'movie' }]))
)
end
end
context 'with `if` conditions' do
let(:params) do
{
include: ['actors'],
params: { conditionals_off: 'yes' }
}
end
it do
movie.actors.each do |actor|
expect(serialized['included']).not_to include(
have_type('actor')
.and(have_id(actor.uid))
.and(have_relationship('played_movies'))
)
end
end
end
context 'with has_many polymorphic' do
let(:params) do
{ include: ['actors_and_users.played_movies'] }
end
it do
expect(serialized['included']).to include(
have_type('user').and(have_id(movie.polymorphics[0].uid))
)
expect(serialized['included']).to include(
have_type('movie').and(have_id(movie.polymorphics[1].movies[0].id))
)
expect(serialized['included']).to include(
have_type('actor')
.and(have_id(movie.polymorphics[1].uid))
.and(
have_relationship('played_movies').with_data(
[{
'id' => movie.polymorphics[1].movies[0].id,
'type' => 'movie'
}]
)
)
)
end
end
context 'with belongs_to polymorphic' do
let(:params) do
{ include: ['actor_or_user'] }
end
it do
expect(serialized['included']).to include(
have_type('actor').and(have_id(movie.actor_or_user.uid))
)
end
end
end
end
end

View File

@ -0,0 +1,73 @@
require 'spec_helper'
require 'active_record'
require 'sqlite3'
describe 'active record' do
# Setup DB
before(:all) do
@db_file = "test.db"
# Open a database
db = SQLite3::Database.new @db_file
# Create tables
db.execute_batch <<-SQL
create table suppliers (
name varchar(30),
id int primary key
);
create table accounts (
name varchar(30),
id int primary key,
supplier_id int,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
);
SQL
# Insert records
@account_id = 2
@supplier_id = 1
@supplier_id_without_account = 3
db.execute_batch <<-SQL
insert into suppliers values ('Supplier1', #{@supplier_id}),
('SupplierWithoutAccount', #{@supplier_id_without_account});
insert into accounts values ('Dollar Account', #{@account_id}, #{@supplier_id});
SQL
end
# Setup Active Record
before(:all) do
class Supplier < ActiveRecord::Base
has_one :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
end
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => @db_file
)
end
context 'has one patch' do
it 'has account_id method for a supplier' do
expect(Supplier.first.respond_to?(:account_id)).to be true
expect(Supplier.first.account_id).to eq @account_id
end
it 'has account_id method return nil if account not present' do
expect(Supplier.find(@supplier_id_without_account).account_id).to eq nil
end
end
# Clean up DB
after(:all) do
File.delete(@db_file) if File.exist?(@db_file)
end
end

View File

@ -0,0 +1,56 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'instrument' do
before(:each) do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
@serializer = MovieSerializer.new([movie, movie], options)
end
context 'serializable_hash' do
it 'should send not notifications' do
events = []
ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
serialized_hash = @serializer.serializable_hash
expect(events.length).to eq(0)
expect(serialized_hash.key?(:data)).to eq(true)
expect(serialized_hash.key?(:meta)).to eq(true)
expect(serialized_hash.key?(:included)).to eq(true)
end
end
context 'serialized_json' do
it 'should send not notifications' do
events = []
ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
json = @serializer.serialized_json
expect(events.length).to eq(0)
expect(json.length).to be > 50
end
end
end
end

View File

@ -0,0 +1,82 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'instrument' do
before(:all) do
require 'fast_jsonapi/instrumentation'
end
after(:all) do
[ :serialized_json, :serializable_hash ].each do |m|
alias_command = "alias_method :#{m}, :#{m}_without_instrumentation"
FastJsonapi::ObjectSerializer.class_eval(alias_command)
remove_command = "remove_method :#{m}_without_instrumentation"
FastJsonapi::ObjectSerializer.class_eval(remove_command)
end
end
before(:each) do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
@serializer = MovieSerializer.new([movie, movie], options)
end
context 'serializable_hash' do
it 'should send notifications' do
events = []
ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
serialized_hash = @serializer.serializable_hash
expect(events.length).to eq(1)
event = events.first
expect(event.duration).to be > 0
expect(event.payload).to eq({ name: 'MovieSerializer' })
expect(event.name).to eq(FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION)
expect(serialized_hash.key?(:data)).to eq(true)
expect(serialized_hash.key?(:meta)).to eq(true)
expect(serialized_hash.key?(:included)).to eq(true)
end
end
context 'serialized_json' do
it 'should send notifications' do
events = []
ActiveSupport::Notifications.subscribe(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION) do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
json = @serializer.serialized_json
expect(events.length).to eq(1)
event = events.first
expect(event.duration).to be > 0
expect(event.payload).to eq({ name: 'MovieSerializer' })
expect(event.name).to eq(FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION)
expect(json.length).to be > 50
end
end
end
end

View File

@ -0,0 +1,21 @@
require 'spec_helper'
module FastJsonapi
module MultiToJson
describe Result do
it 'supports chaining of rescues' do
expect do
Result.new(LoadError) do
require '1'
end.rescue do
require '2'
end.rescue do
require '3'
end.rescue do
'4'
end
end.not_to raise_error
end
end
end
end

View File

@ -0,0 +1,68 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when caching has_many' do
before(:each) do
rails = OpenStruct.new
rails.cache = ActiveSupport::Cache::MemoryStore.new
stub_const('Rails', rails)
end
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
serializable_hash = CachingMovieSerializer.new([movie, movie], options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included][0]).to be_instance_of(Hash)
expect(serializable_hash[:included].length).to eq 3
serializable_hash = CachingMovieSerializer.new(movie).serializable_hash
expect(serializable_hash[:data]).to be_instance_of(Hash)
expect(serializable_hash[:meta]).to be nil
expect(serializable_hash[:included]).to be nil
end
it 'uses cached values for the record' do
previous_name = movie.name
previous_actors = movie.actors
CachingMovieSerializer.new(movie).serializable_hash
movie.name = 'should not match'
allow(movie).to receive(:actor_ids).and_return([99])
expect(previous_name).not_to eq(movie.name)
expect(previous_actors).not_to eq(movie.actors)
serializable_hash = CachingMovieSerializer.new(movie).serializable_hash
expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq movie.actors.length
end
it 'uses cached values for has many as specified' do
previous_name = movie.name
previous_actors = movie.actors
CachingMovieWithHasManySerializer.new(movie).serializable_hash
movie.name = 'should not match'
allow(movie).to receive(:actor_ids).and_return([99])
expect(previous_name).not_to eq(movie.name)
expect(previous_actors).not_to eq(movie.actors)
serializable_hash = CachingMovieWithHasManySerializer.new(movie).serializable_hash
expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq previous_actors.length
end
end
end

View File

@ -0,0 +1,76 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when testing class methods of object serializer' do
before(:example) do
MovieSerializer.relationships_to_serialize = {}
end
it 'returns correct relationship hash for a has_many relationship' do
MovieSerializer.has_many :roles
relationship = MovieSerializer.relationships_to_serialize[:roles]
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:id_method_name]).to end_with '_ids'
expect(relationship[:record_type]).to eq 'roles'.singularize.to_sym
end
it 'returns correct relationship hash for a has_many relationship with overrides' do
MovieSerializer.has_many :roles, id_method_name: :roles_only_ids, record_type: :super_role
relationship = MovieSerializer.relationships_to_serialize[:roles]
expect(relationship[:id_method_name]).to be :roles_only_ids
expect(relationship[:record_type]).to be :super_role
end
it 'returns correct relationship hash for a belongs_to relationship' do
MovieSerializer.belongs_to :area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:id_method_name]).to end_with '_id'
expect(relationship[:record_type]).to eq 'area'.singularize.to_sym
end
it 'returns correct relationship hash for a belongs_to relationship with overrides' do
MovieSerializer.has_many :area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship[:id_method_name]).to be :blah_id
expect(relationship[:record_type]).to be :awesome_area
expect(relationship[:serializer]).to be :MyAreaSerializer
end
it 'returns correct relationship hash for a has_one relationship' do
MovieSerializer.has_one :area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:id_method_name]).to end_with '_id'
expect(relationship[:record_type]).to eq 'area'.singularize.to_sym
end
it 'returns correct relationship hash for a has_one relationship with overrides' do
MovieSerializer.has_one :area, id_method_name: :blah_id, record_type: :awesome_area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship[:id_method_name]).to be :blah_id
expect(relationship[:record_type]).to be :awesome_area
end
it 'returns serializer name correctly with namespaces' do
AppName::V1::MovieSerializer.has_many :area, id_method_name: :blah_id
relationship = AppName::V1::MovieSerializer.relationships_to_serialize[:area]
expect(relationship[:serializer]).to be :'AppName::V1::AreaSerializer'
end
it 'sets the correct transform_method when use_hyphen is used' do
MovieSerializer.use_hyphen
warning_message = 'DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead'
expect { MovieSerializer.use_hyphen }.to output.to_stderr
expect(MovieSerializer.instance_variable_get(:@transform_method)).to eq :dasherize
end
end
end

View File

@ -0,0 +1,80 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
include_context 'ams movie class'
before(:context) do
[:dash, :camel, :camel_lower, :underscore].each do |transform_type|
movie_serializer_name = "#{transform_type}_movie_serializer".classify
movie_type_serializer_name = "#{transform_type}_movie_type_serializer".classify
# https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
movie_serializer_class = Object.const_set(
movie_serializer_name,
Class.new {
}
)
# https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/67-instance-eval
movie_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_type :movie
set_key_transform transform_type
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
movie_type_serializer_class = Object.const_set(
movie_type_serializer_name,
Class.new {
}
)
movie_type_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_key_transform transform_type
set_type :movie_type
attributes :name
end
end
end
context 'when using dashes for word separation in the JSON API members' do
it 'returns correct hash when serializable_hash is called' do
serializable_hash = DashMovieSerializer.new([movie, movie]).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:relationships]).to have_key('movie-type'.to_sym)
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:data][0][:attributes]).to have_key("release-year".to_sym)
serializable_hash = DashMovieSerializer.new(movie_struct).serializable_hash
expect(serializable_hash[:data][:relationships].length).to eq 3
expect(serializable_hash[:data][:relationships]).to have_key('movie-type'.to_sym)
expect(serializable_hash[:data][:attributes].length).to eq 2
expect(serializable_hash[:data][:attributes]).to have_key('release-year'.to_sym)
expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s
end
it 'returns type hypenated when trying to serializing a class with multiple words' do
movie_type = MovieType.new
movie_type.id = 3
movie_type.name = "x"
serializable_hash = DashMovieTypeSerializer.new(movie_type).serializable_hash
expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym
end
end
context 'when using other key transforms' do
[:camel, :camel_lower, :underscore, :dash].each do |transform_type|
it "returns same thing as ams when using #{transform_type}" do
ams_movie = build_ams_movies(1).first
movie = build_movies(1).first
movie_serializer_class = "#{transform_type}_movie_serializer".classify.constantize
our_json = movie_serializer_class.new([movie]).serialized_json
ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: transform_type).to_json
expect(our_json.length).to eq (ams_json.length)
end
end
end
end

View File

@ -0,0 +1,220 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer, performance: true do
include_context 'movie class'
include_context 'ams movie class'
include_context 'jsonapi movie class'
include_context 'jsonapi-serializers movie class'
include_context 'group class'
include_context 'ams group class'
include_context 'jsonapi group class'
include_context 'jsonapi-serializers group class'
before(:all) { GC.disable }
after(:all) { GC.enable }
SERIALIZERS = {
fast_jsonapi: {
name: 'Fast Serializer',
hash_method: :serializable_hash,
json_method: :serialized_json
},
ams: {
name: 'AMS serializer',
speed_factor: 25,
hash_method: :as_json
},
jsonapi: {
name: 'jsonapi-rb serializer'
},
jsonapis: {
name: 'jsonapi-serializers'
}
}
context 'when testing performance of serialization' do
it 'should create a hash of 1000 records in less than 50 ms' do
movies = 1000.times.map { |_i| movie }
expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(50).ms
end
it 'should serialize 1000 records to jsonapi in less than 60 ms' do
movies = 1000.times.map { |_i| movie }
expect { MovieSerializer.new(movies).serialized_json }.to perform_under(60).ms
end
it 'should create a hash of 1000 records with includes and meta in less than 75 ms' do
count = 1000
movies = count.times.map { |_i| movie }
options = {}
options[:meta] = { total: count }
options[:include] = [:actors]
expect { MovieSerializer.new(movies, options).serializable_hash }.to perform_under(75).ms
end
it 'should serialize 1000 records to jsonapi with includes and meta in less than 75 ms' do
count = 1000
movies = count.times.map { |_i| movie }
options = {}
options[:meta] = { total: count }
options[:include] = [:actors]
expect { MovieSerializer.new(movies, options).serialized_json }.to perform_under(75).ms
end
end
def print_stats(message, count, data)
puts
puts message
name_length = SERIALIZERS.collect { |s| s[1].fetch(:name, s[0]).length }.max
puts format("%-#{name_length+1}s %-10s %-10s %s", 'Serializer', 'Records', 'Time', 'Speed Up')
report_format = "%-#{name_length+1}s %-10s %-10s"
fast_jsonapi_time = data[:fast_jsonapi][:time]
puts format(report_format, 'Fast serializer', count, fast_jsonapi_time.round(2).to_s + ' ms')
data.reject { |k,v| k == :fast_jsonapi }.each_pair do |k,v|
t = v[:time]
factor = t / fast_jsonapi_time
speed_factor = SERIALIZERS[k].fetch(:speed_factor, 1)
result = factor >= speed_factor ? '✔' : '✘'
puts format("%-#{name_length+1}s %-10s %-10s %sx %s", SERIALIZERS[k][:name], count, t.round(2).to_s + ' ms', factor.round(2), result)
end
end
def run_hash_benchmark(message, movie_count, serializers)
data = Hash[serializers.keys.collect { |k| [ k, { hash: nil, time: nil, speed_factor: nil }] }]
serializers.each_pair do |k,v|
hash_method = SERIALIZERS[k].key?(:hash_method) ? SERIALIZERS[k][:hash_method] : :to_hash
data[k][:time] = Benchmark.measure { data[k][:hash] = v.send(hash_method) }.real * 1000
end
print_stats(message, movie_count, data)
data
end
def run_json_benchmark(message, movie_count, serializers)
data = Hash[serializers.keys.collect { |k| [ k, { json: nil, time: nil, speed_factor: nil }] }]
serializers.each_pair do |k,v|
ams_json = nil
json_method = SERIALIZERS[k].key?(:json_method) ? SERIALIZERS[k][:json_method] : :to_json
data[k][:time] = Benchmark.measure { data[k][:json] = v.send(json_method) }.real * 1000
end
print_stats(message, movie_count, data)
data
end
context 'when comparing with AMS 0.10.x' do
[1, 25, 250, 1000].each do |movie_count|
it "should serialize #{movie_count} records atleast #{SERIALIZERS[:ams][:speed_factor]} times faster than AMS" do
ams_movies = build_ams_movies(movie_count)
movies = build_movies(movie_count)
jsonapi_movies = build_jsonapi_movies(movie_count)
jsonapis_movies = build_js_movies(movie_count)
serializers = {
fast_jsonapi: MovieSerializer.new(movies),
ams: ActiveModelSerializers::SerializableResource.new(ams_movies),
jsonapi: JSONAPISerializer.new(jsonapi_movies),
jsonapis: JSONAPISSerializer.new(jsonapis_movies)
}
message = "Serialize to JSON string #{movie_count} records"
json_benchmarks = run_json_benchmark(message, movie_count, serializers)
message = "Serialize to Ruby Hash #{movie_count} records"
hash_benchmarks = run_hash_benchmark(message, movie_count, serializers)
# json
expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length
json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time]
expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor]
# hash
hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time]
expect(hash_speed_up).to be >= SERIALIZERS[:ams][:speed_factor]
end
end
end
context 'when comparing with AMS 0.10.x and with includes and meta' do
[1, 25, 250, 1000].each do |movie_count|
it "should serialize #{movie_count} records atleast #{SERIALIZERS[:ams][:speed_factor]} times faster than AMS" do
ams_movies = build_ams_movies(movie_count)
movies = build_movies(movie_count)
jsonapi_movies = build_jsonapi_movies(movie_count)
jsonapis_movies = build_js_movies(movie_count)
options = {}
options[:meta] = { total: movie_count }
options[:include] = [:actors, :movie_type]
serializers = {
fast_jsonapi: MovieSerializer.new(movies, options),
ams: ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta]),
jsonapi: JSONAPISerializer.new(jsonapi_movies, include: options[:include], meta: options[:meta]),
jsonapis: JSONAPISSerializer.new(jsonapis_movies, include: options[:include].map { |i| i.to_s.dasherize }, meta: options[:meta])
}
message = "Serialize to JSON string #{movie_count} with includes and meta"
json_benchmarks = run_json_benchmark(message, movie_count, serializers)
message = "Serialize to Ruby Hash #{movie_count} with includes and meta"
hash_benchmarks = run_hash_benchmark(message, movie_count, serializers)
# json
expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length
json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time]
expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor]
# hash
hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time]
expect(hash_speed_up).to be >= SERIALIZERS[:ams][:speed_factor]
end
end
end
context 'when comparing with AMS 0.10.x and with polymorphic has_many' do
[1, 25, 250, 1000].each do |group_count|
it "should serialize #{group_count} records at least #{SERIALIZERS[:ams][:speed_factor]} times faster than AMS" do
ams_groups = build_ams_groups(group_count)
groups = build_groups(group_count)
jsonapi_groups = build_jsonapi_groups(group_count)
jsonapis_groups = build_jsonapis_groups(group_count)
options = {}
serializers = {
fast_jsonapi: GroupSerializer.new(groups, options),
ams: ActiveModelSerializers::SerializableResource.new(ams_groups),
jsonapi: JSONAPISerializerB.new(jsonapi_groups),
jsonapis: JSONAPISSerializerB.new(jsonapis_groups)
}
message = "Serialize to JSON string #{group_count} with polymorphic has_many"
json_benchmarks = run_json_benchmark(message, group_count, serializers)
message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many"
hash_benchmarks = run_hash_benchmark(message, group_count, serializers)
# json
expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length
json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time]
expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor]
# hash
hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time]
expect(hash_speed_up).to be >= SERIALIZERS[:ams][:speed_factor]
end
end
end
end

View File

@ -0,0 +1,30 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when setting id' do
subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash }
before(:all) do
MovieSerializer.set_id :owner_id
end
context 'when one record is given' do
let(:resource) { movie }
it 'returns correct hash which id equals owner_id' do
expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id
end
end
context 'when an array of records is given' do
let(:resource) { [movie, movie] }
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][1][:id].to_i).to eq movie.owner_id
end
end
end
end

View File

@ -0,0 +1,157 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
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[:include] = [:actors]
serializable_hash = MovieSerializer.new([movie, movie], options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included][0]).to be_instance_of(Hash)
expect(serializable_hash[:included].length).to eq 3
serializable_hash = MovieSerializer.new(movie).serializable_hash
expect(serializable_hash[:data]).to be_instance_of(Hash)
expect(serializable_hash[:meta]).to be nil
expect(serializable_hash[:included]).to be nil
end
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
serializable_hash = JSON.parse(json)
expect(serializable_hash['data'].length).to eq 2
expect(serializable_hash['meta']).to be_instance_of(Hash)
end
it 'returns correct id when serialized_json is called for a single object' do
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['id']).to eq movie.id.to_s
end
it 'returns correct json when serializing nil' do
json = MovieSerializer.new(nil).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']).to eq nil
end
it 'returns correct json when record id is nil' do
movie.id = nil
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['id']).to be nil
end
it 'returns correct json when has_many returns []' do
movie.actor_ids = []
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['actors']['data'].length).to eq 0
end
it 'returns correct json when belongs_to returns nil' do
movie.owner_id = nil
json = MovieSerializer.new(movie).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
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['account']['data']).to be nil
end
it 'returns correct json when serializing []' do
json = MovieSerializer.new([]).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']).to eq []
end
describe '#as_json' do
it 'returns a json hash' do
json_hash = MovieSerializer.new(movie).as_json
expect(json_hash['data']['id']).to eq movie.id.to_s
end
it 'returns multiple records' do
json_hash = MovieSerializer.new([movie, movie]).as_json
expect(json_hash['data'].length).to eq 2
end
it 'removes non-relevant attributes' do
movie.director = 'steven spielberg'
json_hash = MovieSerializer.new(movie).as_json
expect(json_hash['data']['director']).to eq(nil)
end
end
it 'returns errors when serializing with non-existent includes key' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:blah_blah]
expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError)
end
it 'does not throw an error with non-empty string array includes key' do
options = {}
options[:include] = ['actors']
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]
options[:include] = [nil]
expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta]
end
end
context 'when testing included do block of object serializer' do
it 'should set default_type based on serializer class name' do
class BlahSerializer
include FastJsonapi::ObjectSerializer
end
expect(BlahSerializer.record_type).to be :blah
end
it 'should set default_type for a multi word class name' do
class BlahBlahSerializer
include FastJsonapi::ObjectSerializer
end
expect(BlahBlahSerializer.record_type).to be :blah_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
end
expect(V1::BlahSerializer.record_type).to be :blah
end
end
end

View File

@ -0,0 +1,31 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when testing object serializer with ruby struct' do
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
serializable_hash = MovieSerializer.new([movie_struct, movie_struct], options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included][0]).to be_instance_of(Hash)
expect(serializable_hash[:included].length).to eq 3
serializable_hash = MovieSerializer.new(movie_struct).serializable_hash
expect(serializable_hash[:data]).to be_instance_of(Hash)
expect(serializable_hash[:meta]).to be nil
expect(serializable_hash[:included]).to be nil
expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s
end
end
end

View File

@ -0,0 +1,13 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when including attribute blocks' do
it 'returns correct hash when serializable_hash is called' do
serializable_hash = MovieSerializerWithAttributeBlock.new([movie]).serializable_hash
expect(serializable_hash[:data][0][:attributes][:name]).to eq movie.name
expect(serializable_hash[:data][0][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})"
end
end
end

View File

@ -0,0 +1,90 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context "movie class"
include_context 'group class'
context 'when testing class methods of serialization core' do
it 'returns correct hash when id_hash is called' do
inputs = [{id: 23, record_type: :movie}, {id: 'x', record_type: 'person'}]
inputs.each do |hash|
result_hash = MovieSerializer.send(:id_hash, hash[:id], hash[:record_type])
expect(result_hash[:id]).to eq hash[:id].to_s
expect(result_hash[:type]).to eq hash[:record_type]
end
result_hash = MovieSerializer.send(:id_hash, nil, 'movie')
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, 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|
value = attributes_hash[key]
expect(value).to eq movie.send(method_name)
end
end
it 'returns the correct empty result when relationships_hash is called' do
movie.actor_ids = []
movie.owner_id = nil
relationships_hash = MovieSerializer.send(:relationships_hash, movie)
expect(relationships_hash[:actors][:data]).to eq([])
expect(relationships_hash[:owner][:data]).to eq(nil)
end
it 'returns correct keys when relationships_hash is called' 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
expect(relationship_names).to eq expected_names
end
it 'returns correct values when relationships_hash is called' do
relationships_hash = MovieSerializer.relationships_hash(movie)
actors_hash = movie.actor_ids.map { |id| {id: id.to_s, type: :actor} }
owner_hash = {id: movie.owner_id.to_s, type: :user}
expect(relationships_hash[:actors][:data]).to match_array actors_hash
expect(relationships_hash[:owner][:data]).to eq owner_hash
end
it 'returns correct hash when record_hash is called' do
record_hash = MovieSerializer.send(:record_hash, movie)
expect(record_hash[:id]).to eq movie.id.to_s
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(:relationships) if MovieSerializer.relationships_to_serialize.present?
end
it 'serializes known included records only once' do
includes_list = [:actors]
known_included_objects = {}
included_records = []
[movie, movie].each do |record|
included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects)
end
expect(included_records.size).to eq 3
end
end
end

View File

@ -0,0 +1,84 @@
RSpec.shared_context 'ams movie class' do
before(:context) do
# models
class AMSModel < ActiveModelSerializers::Model
derive_attributes_from_names_and_fix_accessors
end
class AMSMovie < AMSModel
attributes :id, :name, :release_year, :actors, :owner, :movie_type
end
class AMSActor < AMSModel
attributes :id, :name, :email
end
class AMSUser < AMSModel
attributes :id, :name
end
class AMSMovieType < AMSModel
attributes :id, :name
end
# serializers
class AMSActorSerializer < ActiveModel::Serializer
type 'actor'
attributes :name, :email
end
class AMSUserSerializer < ActiveModel::Serializer
type 'user'
attributes :name
end
class AMSMovieTypeSerializer < ActiveModel::Serializer
type 'movie_type'
attributes :name
end
class AMSMovieSerializer < ActiveModel::Serializer
type 'movie'
attributes :name, :release_year
has_many :actors
has_one :owner
belongs_to :movie_type
end
end
after(:context) do
classes_to_remove = %i[AMSMovie AMSMovieSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:ams_actors) do
3.times.map do |i|
a = AMSActor.new
a.id = i + 1
a.name = "Test #{a.id}"
a.email = "test#{a.id}@test.com"
a
end
end
let(:ams_user) do
ams_user = AMSUser.new
ams_user.id = 3
ams_user
end
let(:ams_movie_type) do
ams_movie_type = AMSMovieType.new
ams_movie_type.id = 1
ams_movie_type.name = 'episode'
ams_movie_type
end
def build_ams_movies(count)
count.times.map do |i|
m = AMSMovie.new
m.id = i + 1
m.name = 'test movie'
m.actors = ams_actors
m.owner = ams_user
m.movie_type = ams_movie_type
m
end
end
end

View File

@ -0,0 +1,87 @@
RSpec.shared_context 'ams group class' do
before(:context) do
# models
class AMSPerson < ActiveModelSerializers::Model
attr_accessor :id, :first_name, :last_name
end
class AMSGroup < ActiveModelSerializers::Model
attr_accessor :id, :name, :groupees
end
# serializers
class AMSPersonSerializer < ActiveModel::Serializer
type 'person'
attributes :first_name, :last_name
end
class AMSGroupSerializer < ActiveModel::Serializer
type 'group'
attributes :name
has_many :groupees
end
end
after(:context) do
classes_to_remove = %i[AMSPerson AMSGroup AMSPersonSerializer AMSGroupSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:ams_groups) do
group_count = 0
person_count = 0
3.times.map do |i|
group = AMSGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = AMSPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = AMSGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
let(:ams_person) do
ams_person = AMSPerson.new
ams_person.id = 3
ams_person
end
def build_ams_groups(count)
group_count = 0
person_count = 0
count.times.map do |i|
group = AMSGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = AMSPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = AMSGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
end

View File

@ -0,0 +1,131 @@
RSpec.shared_context 'group class' do
# Person, Group Classes and serializers
before(:context) do
# models
class Person
attr_accessor :id, :first_name, :last_name
end
class Group
attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects
end
# serializers
class PersonSerializer
include FastJsonapi::ObjectSerializer
set_type :person
attributes :first_name, :last_name
end
class GroupSerializer
include FastJsonapi::ObjectSerializer
set_type :group
attributes :name
has_many :groupees, polymorphic: true
end
end
# Namespaced PersonSerializer
before(:context) do
# namespaced model stub
module AppName
module V1
class PersonSerializer
include FastJsonapi::ObjectSerializer
# to test if compute_serializer_name works
end
end
end
end
# Movie and Actor struct
before(:context) do
PersonStruct = Struct.new(
:id, :first_name, :last_name
)
GroupStruct = Struct.new(
:id, :name, :groupees, :groupee_ids
)
end
after(:context) do
classes_to_remove = %i[
Person
PersonSerializer
Group
GroupSerializer
AppName::V1::PersonSerializer
PersonStruct
GroupStruct
]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:group_struct) do
group = GroupStruct.new
group[:id] = 1
group[:name] = 'Group 1'
group[:groupees] = []
person = PersonStruct.new
person[:id] = 1
person[:last_name] = "Last Name 1"
person[:first_name] = "First Name 1"
child_group = GroupStruct.new
child_group[:id] = 2
child_group[:name] = 'Group 2'
group.groupees = [person, child_group]
group
end
let(:group) do
group = Group.new
group.id = 1
group.name = 'Group 1'
person = Person.new
person.id = 1
person.last_name = "Last Name 1"
person.first_name = "First Name 1"
child_group = Group.new
child_group.id = 2
child_group.name = 'Group 2'
group.groupees = [person, child_group]
group
end
def build_groups(count)
group_count = 0
person_count = 0
count.times.map do |i|
group = Group.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = Person.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = Group.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
end

View File

@ -0,0 +1,123 @@
RSpec.shared_context 'jsonapi-serializers movie class' do
before(:context) do
# models
class JSMovie
attr_accessor :id, :name, :release_year, :actors, :owner, :movie_type
end
class JSActor
attr_accessor :id, :name, :email
end
class JSUser
attr_accessor :id, :name
end
class JSMovieType
attr_accessor :id, :name
end
# serializers
class JSActorSerializer
include JSONAPI::Serializer
attributes :name, :email
def type
'actor'
end
end
class JSUserSerializer
include JSONAPI::Serializer
attributes :name
def type
'user'
end
end
class JSMovieTypeSerializer
include JSONAPI::Serializer
attributes :name
def type
'movie_type'
end
end
class JSMovieSerializer
include JSONAPI::Serializer
attributes :name, :release_year
has_many :actors
has_one :owner
has_one :movie_type
def type
'movie'
end
end
class JSONAPISSerializer
def initialize(data, options = {})
@options = options.merge(is_collection: true)
@data = data
end
def to_json
JSONAPI::Serializer.serialize(@data, @options).to_json
end
def to_hash
JSONAPI::Serializer.serialize(@data, @options)
end
end
end
after(:context) do
classes_to_remove = %i[
JSMovie
JSActor
JSUser
JSMovieType
JSONAPISSerializer
JSActorSerializer
JSUserSerializer
JSMovieTypeSerializer
JSMovieSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:js_actors) do
3.times.map do |i|
a = JSActor.new
a.id = i + 1
a.name = "Test #{a.id}"
a.email = "test#{a.id}@test.com"
a
end
end
let(:js_user) do
ams_user = JSUser.new
ams_user.id = 3
ams_user
end
let(:js_movie_type) do
ams_movie_type = JSMovieType.new
ams_movie_type.id = 1
ams_movie_type.name = 'episode'
ams_movie_type
end
def build_js_movies(count)
count.times.map do |i|
m = JSMovie.new
m.id = i + 1
m.name = 'test movie'
m.actors = js_actors
m.owner = js_user
m.movie_type = js_movie_type
m
end
end
end

View File

@ -0,0 +1,116 @@
RSpec.shared_context 'jsonapi-serializers group class' do
# Person, Group Classes and serializers
before(:context) do
# models
class JSPerson
attr_accessor :id, :first_name, :last_name
end
class JSGroup
attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects
end
# serializers
class JSPersonSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name
def type
'person'
end
end
class JSGroupSerializer
include JSONAPI::Serializer
attributes :name
has_many :groupees
def type
'group'
end
end
class JSONAPISSerializerB
def initialize(data, options = {})
@options = options.merge(is_collection: true)
@data = data
end
def to_json
JSON.fast_generate(to_hash)
end
def to_hash
JSONAPI::Serializer.serialize(@data, @options)
end
end
end
after :context do
classes_to_remove = %i[
JSPerson
JSGroup
JSPersonSerializer
JSGroupSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:jsonapi_groups) do
group_count = 0
person_count = 0
3.times.map do |i|
group = JSGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = JSPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = JSGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
let(:jsonapis_person) do
person = JSPerson.new
person.id = 3
person
end
def build_jsonapis_groups(count)
group_count = 0
person_count = 0
count.times.map do |i|
group = JSGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = JSPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = JSGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
end

View File

@ -0,0 +1,116 @@
RSpec.shared_context 'jsonapi movie class' do
before(:context) do
# models
class JSONAPIMovie
attr_accessor :id, :name, :release_year, :actors, :owner, :movie_type
end
class JSONAPIActor
attr_accessor :id, :name, :email
end
class JSONAPIUser
attr_accessor :id, :name
end
class JSONAPIMovieType
attr_accessor :id, :name
end
# serializers
class JSONAPIMovieSerializer < JSONAPI::Serializable::Resource
type 'movie'
attributes :name, :release_year
has_many :actors
has_one :owner
belongs_to :movie_type
end
class JSONAPIActorSerializer < JSONAPI::Serializable::Resource
type 'actor'
attributes :name, :email
end
class JSONAPIUserSerializer < JSONAPI::Serializable::Resource
type 'user'
attributes :name
end
class JSONAPIMovieTypeSerializer < JSONAPI::Serializable::Resource
type 'movie_type'
attributes :name
end
class JSONAPISerializer
def initialize(data, options = {})
@serializer = JSONAPI::Serializable::Renderer.new
@options = options.merge(class: {
JSONAPIMovie: JSONAPIMovieSerializer,
JSONAPIActor: JSONAPIActorSerializer,
JSONAPIUser: JSONAPIUserSerializer,
JSONAPIMovieType: JSONAPIMovieTypeSerializer
})
@data = data
end
def to_json
@serializer.render(@data, @options).to_json
end
def to_hash
@serializer.render(@data, @options)
end
end
end
after :context do
classes_to_remove = %i[
JSONAPIMovie
JSONAPIActor
JSONAPIUser
JSONAPIMovieType
JSONAPIMovieSerializer
JSONAPIActorSerializer
JSONAPIUserSerializer
JSONAPIMovieTypeSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:jsonapi_actors) do
3.times.map do |i|
j = JSONAPIActor.new
j.id = i + 1
j.name = "Test #{j.id}"
j.email = "test#{j.id}@test.com"
j
end
end
let(:jsonapi_user) do
jsonapi_user = JSONAPIUser.new
jsonapi_user.id = 3
jsonapi_user
end
let(:jsonapi_movie_type) do
jsonapi_movie_type = JSONAPIMovieType.new
jsonapi_movie_type.id = 1
jsonapi_movie_type.name = 'episode'
jsonapi_movie_type
end
def build_jsonapi_movies(count)
count.times.map do |i|
m = JSONAPIMovie.new
m.id = i + 1
m.name = 'test movie'
m.actors = jsonapi_actors
m.owner = jsonapi_user
m.movie_type = jsonapi_movie_type
m
end
end
end

View File

@ -0,0 +1,112 @@
RSpec.shared_context 'jsonapi group class' do
# Person, Group Classes and serializers
before(:context) do
# models
class JSONAPIPerson
attr_accessor :id, :first_name, :last_name
end
class JSONAPIGroup
attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects
end
# serializers
class JSONAPIPersonSerializer < JSONAPI::Serializable::Resource
type 'person'
attributes :first_name, :last_name
end
class JSONAPIGroupSerializer < JSONAPI::Serializable::Resource
type 'group'
attributes :name
has_many :groupees
end
class JSONAPISerializerB
def initialize(data, options = {})
@serializer = JSONAPI::Serializable::Renderer.new
@options = options.merge(class: {
JSONAPIPerson: JSONAPIPersonSerializer,
JSONAPIGroup: JSONAPIGroupSerializer
})
@data = data
end
def to_json
@serializer.render(@data, @options).to_json
end
def to_hash
@serializer.render(@data, @options)
end
end
end
after :context do
classes_to_remove = %i[
JSONAPIPerson
JSONAPIGroup
JSONAPIPersonSerializer
JSONAPIGroupSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:jsonapi_groups) do
group_count = 0
person_count = 0
3.times.map do |i|
group = JSONAPIGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = JSONAPIPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = JSONAPIGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
let(:jsonapi_person) do
person = JSONAPIPerson.new
person.id = 3
person
end
def build_jsonapi_groups(count)
group_count = 0
person_count = 0
count.times.map do |i|
group = JSONAPIGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id
person = JSONAPIPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id
child_group = JSONAPIGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id
group.groupees = [person, child_group]
group
end
end
end

View File

@ -0,0 +1,224 @@
RSpec.shared_context 'movie class' do
# Movie, Actor Classes and serializers
before(:context) do
# models
class Movie
attr_accessor :id,
:name,
:release_year,
:director,
:actor_ids,
:owner_id,
:movie_type_id
def actors
actor_ids.map do |id|
a = Actor.new
a.id = id
a.name = "Test #{a.id}"
a.email = "test#{a.id}@test.com"
a
end
end
def movie_type
mt = MovieType.new
mt.id = movie_type_id
mt.name = 'Episode'
mt
end
def cache_key
"#{id}"
end
end
class Actor
attr_accessor :id, :name, :email
end
class MovieType
attr_accessor :id, :name
end
class Supplier
attr_accessor :id, :account_id
def account
if account_id
a = Account.new
a.id = account_id
a
end
end
end
class Account
attr_accessor :id
end
# serializers
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
# director attr is not mentioned intentionally
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
class CachingMovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
end
class CachingMovieWithHasManySerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
has_many :actors, cached: true
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
end
class ActorSerializer
include FastJsonapi::ObjectSerializer
set_type :actor
attributes :name, :email
end
class MovieTypeSerializer
include FastJsonapi::ObjectSerializer
set_type :movie_type
attributes :name
end
class MovieSerializerWithAttributeBlock
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
attribute :title_with_year do |record|
"#{record.name} (#{record.release_year})"
end
end
class SupplierSerializer
include FastJsonapi::ObjectSerializer
set_type :supplier
has_one :account
end
class AccountSerializer
include FastJsonapi::ObjectSerializer
set_type :account
belongs_to :supplier
end
end
# Namespaced MovieSerializer
before(:context) do
# namespaced model stub
module AppName
module V1
class MovieSerializer
include FastJsonapi::ObjectSerializer
# to test if compute_serializer_name works
end
end
end
end
# Movie and Actor struct
before(:context) do
MovieStruct = Struct.new(
:id,
:name,
:release_year,
:actor_ids,
:actors,
:owner_id,
:owner,
:movie_type_id
)
ActorStruct = Struct.new(:id, :name, :email)
end
after(:context) do
classes_to_remove = %i[
Movie
MovieSerializer
Actor
ActorSerializer
MovieType
MovieTypeSerializer
MovieSerializerWithAttributeBlock
AppName::V1::MovieSerializer
MovieStruct
ActorStruct
HyphenMovieSerializer
]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:movie_struct) do
actors = []
3.times.each do |id|
actors << ActorStruct.new(id, id.to_s, id.to_s)
end
m = MovieStruct.new
m[:id] = 23
m[:name] = 'struct movie'
m[:release_year] = 1987
m[:actor_ids] = [1,2,3]
m[:owner_id] = 3
m[:movie_type_id] = 2
m[:actors] = actors
m
end
let(:movie) do
m = Movie.new
m.id = 232
m.name = 'test movie'
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
let(:supplier) do
s = Supplier.new
s.id = 1
s.account_id = 1
s
end
def build_movies(count)
count.times.map do |i|
m = Movie.new
m.id = i + 1
m.name = 'test movie'
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
end
end

View File

@ -1,30 +1,21 @@
require 'simplecov'
SimpleCov.start do
add_group 'Lib', 'lib'
add_group 'Tests', 'spec'
end
SimpleCov.minimum_coverage 90
require 'active_support'
require 'active_support/core_ext/object/json'
require 'jsonapi/serializer'
require 'ffaker'
require 'rspec'
require 'jsonapi/rspec'
require 'fast_jsonapi'
require 'rspec-benchmark'
require 'byebug'
require 'securerandom'
require 'active_model_serializers'
require 'oj'
require 'jsonapi/serializable'
require 'jsonapi-serializers'
Dir[File.expand_path('spec/fixtures/*.rb')].sort.each { |f| require f }
Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file }
RSpec.configure do |config|
config.include JSONAPI::RSpec
config.mock_with :rspec
config.filter_run_when_matching :focus
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
config.include RSpec::Benchmark::Matchers
if ENV['TRAVIS'] == 'true' || ENV['TRAVIS'] == true
config.filter_run_excluding performance: true
end
end
Oj.optimize_rails
ActiveModel::Serializer.config.adapter = :json_api
ActiveModel::Serializer.config.key_transform = :underscore
ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new('/dev/null'))