Compare commits

..

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

62 changed files with 1808 additions and 2624 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}}

19
.gitignore vendored
View File

@ -16,6 +16,9 @@ doc
# For MacOS:
.DS_Store
# juwelier generated
pkg
# For MacOS:
.DS_Store
@ -23,7 +26,8 @@ doc
#*.tmproj
#tmtags
*.swp
# For vim:
#*.swp
# For redcar:
#.redcar
@ -33,16 +37,3 @@ doc
# For the gem
test.db
# For those using rbenv
.ruby-version
# 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'

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

24
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,24 @@
# 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 :)
For minor fixes just open a pull request to the [dev]( https://github.com/Netflix/fast_jsonapi/tree/dev ) branch on Github.

158
Gemfile.lock Normal file
View File

@ -0,0 +1,158 @@
PATH
remote: .
specs:
fast_jsonapi (1.0.16)
activerecord (~> 5.0)
activesupport (~> 5.0)
multi_json (~> 1.12)
oj (~> 3.3)
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)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
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)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
diff-lcs (1.3)
docile (1.1.5)
erubi (1.7.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
git (1.3.0)
github_api (0.18.2)
addressable (~> 2.4)
descendants_tracker (~> 0.0.4)
faraday (~> 0.8)
hashie (~> 3.5, >= 3.5.2)
oauth2 (~> 1.0)
hashie (3.5.6)
highline (1.7.10)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
json (1.8.6)
jsonapi-renderer (0.2.0)
juwelier (2.1.3)
builder
bundler (>= 1.13)
git (>= 1.2.5)
github_api
highline (>= 1.6.15)
nokogiri (>= 1.5.10)
rake
rdoc
semver
jwt (1.5.6)
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mini_portile2 (2.3.0)
minitest (5.10.3)
multi_json (1.12.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.3.9)
public_suffix (3.0.1)
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)
rake (12.3.0)
rdoc (3.12.2)
json (~> 1.4)
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)
semver (1.0.1)
simplecov (0.15.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
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.4)
bundler (~> 1.0)
byebug
fast_jsonapi!
juwelier (~> 2.1.0)
rdoc (~> 3.12)
rspec (~> 3.5.0)
rspec-benchmark (~> 0.3.0)
simplecov
skylight (~> 1.3)
sqlite3 (~> 1.3)
BUNDLED WITH
1.16.0

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,

705
README.md
View File

@ -1,52 +1,32 @@
# JSON:API Serialization Library
# Fast JSON API
## :warning: :construction: v2 (the `master` branch) is in maintenance mode! :construction: :warning:
[![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/osstracker.svg)]()
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.
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
* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [Rails Generator](#rails-generator)
* [Model Definition](#model-definition)
* [Serializer Definition](#serializer-definition)
* [Object Serialization](#object-serialization)
* [Compound Document](#compound-document)
* [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 +43,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:
@ -74,19 +54,11 @@ $ bundle install
## Usage
### Rails Generator
You can use the bundled generator if you are using the library inside of
a Rails project:
rails g serializer Movie name year
This will create a new serializer in `app/serializers/movie_serializer.rb`
### Model Definition
```ruby
class Movie
attr_accessor :id, :name, :year, :actor_ids, :owner_id, :movie_type_id
attr_accessor :id, :name, :year, :actor_ids, :owner_id
end
```
@ -94,10 +66,8 @@ end
```ruby
class MovieSerializer
include JSONAPI::Serializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
set_id :owner_id # optional
attributes :name, :year
has_many :actors
belongs_to :owner, record_type: :user
@ -115,17 +85,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 +96,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
@ -145,7 +104,7 @@ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```json
{
"data": {
"id": "3",
"id": "232",
"type": "movie",
"attributes": {
"name": "test movie",
@ -175,592 +134,108 @@ 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
# Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel
end
```
Here are examples of how these options transform the keys
```ruby
set_key_transform :camel # "some_key" => "SomeKey"
set_key_transform :camel_lower # "some_key" => "someKey"
set_key_transform :dash # "some_key" => "some-key"
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.
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
attribute :name
end
```
Custom attributes that must be serialized but do not exist on the model can be declared using Ruby block syntax:
```ruby
class MovieSerializer
include JSONAPI::Serializer
attributes :name, :year
attribute :name_with_year do |object|
"#{object.name} (#{object.year})"
end
end
```
The block syntax can also be used to override the property on the object:
```ruby
class MovieSerializer
include JSONAPI::Serializer
attribute :name do |object|
"#{object.name} Part 2"
end
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 ```
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
Performance instrumentation is available by using the
`active_support/notifications`.
To enable it, include the module in your serializer class:
```ruby
require 'jsonapi/serializer'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
```
[Skylight](https://www.skylight.io/) integration is also available and
supported by us, follow the Skylight documentation to enable it.
### Running Tests
The project has and requires 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:
### [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'
```
### Replace all constant references
```diff
class MovieSerializer
- include FastJsonapi::ObjectSerializer
+ include JSONAPI::Serializer
end
```
### Replace removed methods
```diff
- json_string = MovieSerializer.new(movie).serialized_json
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
### 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 gem is built using a gem building gem called [juwelier](https://github.com/flajann2/juwelier).
Beyond just editing source code, youll be interacting with the gem using rake a lot. To see all the tasks available with a brief description, you can run:
```bash
rake -T
```
### Updating Project information
You can update the project information of the gem by updating the [Rakefile](Rakefile). Then you need to generate a new gemspec:
```bash
rake gemspec
```
### Running Tests
We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional tests and performance tests. To run tests use the following rake task:
```bash
rake spec
```
### Installation
```bash
$ rake install
```
The install rake task builds the gem and then installs it. You're all
set if you're using [RVM](http://rvm.beginrescueend.com/), but you may
need to run it with sudo if you have a system-installed Ruby:
### Bumping Version
It feels good to release code. Do it, do it often. But before that, bump
the version. Then release it. There's a few ways to update the version:
```bash
# version:write like before
$ rake version:write MAJOR=0 MINOR=3 PATCH=0
# bump just major, ie 0.1.0 -> 1.0.0
$ rake version:bump:major
# bump just minor, ie 0.1.0 -> 0.2.0
$ rake version:bump:minor
# bump just patch, ie 0.1.0 -> 0.1.1
$ rake version:bump:patch
```
---
### We're Hiring!
Join the Netflix Studio Engineering team and help us build gems like this!
* [Senior Ruby Engineer](https://jobs.netflix.com/jobs/864893)
* [Senior Platfrom Engineer](https://jobs.netflix.com/jobs/865783)
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.

231
README.rdoc Normal file
View File

@ -0,0 +1,231 @@
# Fast JSON API
[![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/osstracker.svg)]()
A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.
# Performance Comparision
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.
## 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
* [Features](#features)
* [Installation](#installation)
* [Usage](#usage)
* [Model Definition](#model-definition)
* [Serializer Definition](#serializer-definition)
* [Object Serialization](#object-serialization)
* [Compound Document](#compound-document)
* [Collection Serialization](#collection-serialization)
* [Caching](#caching)
* [Contributing](#contributing)
## Features
* Declaration syntax similar to Active Model serializer
* Support for belongs_to, has_many and has_one
* Support for compound documents (included)
* Optimized serialization of compound documents
* Caching
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'fast_jsonapi'
```
Execute:
```ruby
$ bundle install
```
## Usage
### Model Definition
```ruby
class Movie
attr_accessor :id, :name, :year, :actor_ids, :owner_id
end
```
### Serializer Definition
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
attributes :name, :year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
```
### Sample Object
```ruby
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
```
### Object Serialization
#### Return a hash
```ruby
hash = MovieSerializer.new(movie).serializable_hash
```
#### Return Serialized JSON
```ruby
json_string = MovieSerializer.new(movie).serialized_json
```
#### Serialized Output
```json
{
"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"
}
}
}
}
}
```
### Compound Document
Support for top-level included member through ` options[:include] `.
```ruby
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
MovieSerializer.new([movie, movie], options).serialized_json
```
### Collection Serialization
```ruby
options[:meta] = { total: 2 }
hash = MovieSerializer.new([movie, movie], options).serializable_hash
json_string = MovieSerializer.new([movie, movie], options).serialized_json
```
### Caching
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
cache_options enabled: true cache_length: 12.hours
attributes :name, :year
end
```
### Customizable Options
Option | Purpose | Example
------------ | ------------- | -------------
set_type | Type name of Object | ```set_type :movie ```
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```
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true cache_length: 12.hours ```
## Contributing
This gem is built using a gem building gem called [juwelier](https://github.com/flajann2/juwelier).
Beyond just editing source code, youll be interacting with the gem using rake a lot. To see all the tasks available with a brief description, you can run:
```bash
rake -T
```
### Updating Project information
You can update the project information of the gem by updating the [Rakefile](Rakefile). Then you need to generate a new gemspec
```bash
rake gemspec
```
### Running Tests
We use rspec for testing. We have unit tests, functional tests and performance tests. To run tests use the following rake task
```bash
rake spec
```
### Installation
```bash
$ rake install
```
The install rake task builds the gem installs it. You're all
set if you're using [RVM](http://rvm.beginrescueend.com/), but you may
need to run it with sudo if you have a system-installed ruby:
### Bumping Version
It feels good to release code. Do it, do it often. But before that, bump
the version. Then release it. There's a few ways to update the version:
```ruby
# version:write like before
$ rake version:write MAJOR=0 MINOR=3 PATCH=0
# bump just major, ie 0.1.0 -> 1.0.0
$ rake version:bump:major
# bump just minor, ie 0.1.0 -> 0.2.0
$ rake version:bump:minor
# bump just patch, ie 0.1.0 -> 0.1.1
$ rake version:bump:patch
```

View File

@ -1,15 +1,55 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'
# encoding: utf-8
desc('Codestyle check and linter')
RuboCop::RakeTask.new('rubocop') do |task|
task.fail_on_error = true
task.patterns = [
'lib/**/*.rb',
'spec/**/*.rb'
]
require 'rubygems'
require 'bundler'
begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end
require 'rake'
require 'juwelier'
Juwelier::Tasks.new do |gem|
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
gem.name = "fast_jsonapi"
gem.homepage = "http://github.com/Netflix/fast_jsonapi"
gem.license = "Apache-2.0"
gem.summary = %Q{fast JSON API(jsonapi.org) serializer}
gem.description = %Q{JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects}
gem.email = ""
gem.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"]
if gem.respond_to?(:metadata)
gem.metadata['allowed_push_host'] = 'https://rubygems.org'
else
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
end
# dependencies defined in Gemfile
end
Juwelier::RubygemsDotOrgTasks.new
require 'rspec/core'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = FileList['spec/**/*_spec.rb']
end
RSpec::Core::RakeTask.new(:spec)
task(default: [:rubocop, :spec])
desc "Code coverage detail"
task :simplecov do
ENV['COVERAGE'] = "true"
Rake::Task['spec'].execute
end
task :default => :spec
require 'rdoc/task'
Rake::RDocTask.new do |rdoc|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "fast_jsonapi #{version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end

1
VERSION Normal file
View File

@ -0,0 +1 @@
1.0.17

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. :)

108
fast_jsonapi.gemspec Normal file
View File

@ -0,0 +1,108 @@
# Generated by juwelier
# DO NOT EDIT THIS FILE DIRECTLY
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
# -*- encoding: utf-8 -*-
# stub: fast_jsonapi 1.0.16 ruby lib
Gem::Specification.new do |s|
s.name = "fast_jsonapi"
s.version = "1.0.17"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.metadata = { "allowed_push_host" => "https://rubygems.org" } if s.respond_to? :metadata=
s.require_paths = ["lib"]
s.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"]
s.date = "2018-02-01"
s.description = "JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects"
s.email = ""
s.extra_rdoc_files = [
"LICENSE.txt",
"README.md",
"README.rdoc"
]
s.files = [
".document",
".rspec",
"Gemfile",
"Gemfile.lock",
"LICENSE.txt",
"README.md",
"README.rdoc",
"Rakefile",
"VERSION",
"docs/collection_serializer_output.json",
"docs/object_serializer.json",
"fast_jsonapi.gemspec",
"lib/extensions/has_one.rb",
"lib/fast_jsonapi.rb",
"lib/fast_jsonapi/object_serializer.rb",
"lib/fast_jsonapi/serialization_core.rb",
"spec/lib/extensions/active_record_spec.rb",
"spec/lib/object_serializer_caching_spec.rb",
"spec/lib/object_serializer_class_methods_spec.rb",
"spec/lib/object_serializer_hyphen_spec.rb",
"spec/lib/object_serializer_performance_spec.rb",
"spec/lib/object_serializer_spec.rb",
"spec/lib/object_serializer_struct_spec.rb",
"spec/lib/serialization_core_spec.rb",
"spec/shared/contexts/ams_context.rb",
"spec/shared/contexts/movie_context.rb",
"spec/spec_helper.rb"
]
s.homepage = "http://github.com/Netflix/fast_jsonapi"
s.licenses = ["Apache-2.0"]
s.rubygems_version = "2.5.1"
s.summary = "fast JSON API(jsonapi.org) serializer"
if s.respond_to? :specification_version then
s.specification_version = 4
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<activesupport>, ["~> 5.0"])
s.add_runtime_dependency(%q<multi_json>, ["~> 1.12"])
s.add_runtime_dependency(%q<oj>, ["~> 3.3"])
s.add_runtime_dependency(%q<activerecord>, ["~> 5.0"])
s.add_development_dependency(%q<skylight>, ["~> 1.3"])
s.add_development_dependency(%q<rspec>, ["~> 3.5.0"])
s.add_development_dependency(%q<rspec-benchmark>, ["~> 0.3.0"])
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
s.add_development_dependency(%q<juwelier>, ["~> 2.1.0"])
s.add_development_dependency(%q<simplecov>, [">= 0"])
s.add_development_dependency(%q<byebug>, [">= 0"])
s.add_development_dependency(%q<active_model_serializers>, ["~> 0.10.4"])
s.add_development_dependency(%q<sqlite3>, ["~> 1.3"])
else
s.add_dependency(%q<activesupport>, ["~> 5.0"])
s.add_dependency(%q<multi_json>, ["~> 1.12"])
s.add_dependency(%q<oj>, ["~> 3.3"])
s.add_dependency(%q<skylight>, ["~> 1.3"])
s.add_dependency(%q<activerecord>, ["~> 5.0"])
s.add_dependency(%q<rspec>, ["~> 3.5.0"])
s.add_dependency(%q<rspec-benchmark>, ["~> 0.3.0"])
s.add_dependency(%q<rdoc>, ["~> 3.12"])
s.add_dependency(%q<bundler>, ["~> 1.0"])
s.add_dependency(%q<juwelier>, ["~> 2.1.0"])
s.add_dependency(%q<simplecov>, [">= 0"])
s.add_dependency(%q<byebug>, [">= 0"])
s.add_dependency(%q<active_model_serializers>, ["~> 0.10.4"])
s.add_dependency(%q<sqlite3>, ["~> 1.3"])
end
else
s.add_dependency(%q<activesupport>, ["~> 5.0"])
s.add_dependency(%q<multi_json>, ["~> 1.12"])
s.add_dependency(%q<oj>, ["~> 3.3"])
s.add_dependency(%q<skylight>, ["~> 1.3"])
s.add_dependency(%q<activerecord>, ["~> 5.0"])
s.add_dependency(%q<rspec>, ["~> 3.5.0"])
s.add_dependency(%q<rspec-benchmark>, ["~> 0.3.0"])
s.add_dependency(%q<rdoc>, ["~> 3.12"])
s.add_dependency(%q<bundler>, ["~> 1.0"])
s.add_dependency(%q<juwelier>, ["~> 2.1.0"])
s.add_dependency(%q<simplecov>, [">= 0"])
s.add_dependency(%q<byebug>, [">= 0"])
s.add_dependency(%q<active_model_serializers>, ["~> 0.10.4"])
s.add_dependency(%q<sqlite3>, ["~> 1.3"])
end
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,4 +1,4 @@
# frozen_string_literal: true
require 'active_record'
::ActiveRecord::Associations::Builder::HasOne.class_eval do
# Based on
@ -8,10 +8,8 @@
super
name = reflection.name
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_id
# if an attribute is already defined with this methods name we should just use it
return read_attribute(__method__) if has_attribute?(__method__)
association(:#{name}).reader.try(:id)
def #{name.to_s}_id
association(:#{name}).reader.id
end
CODE
end

View File

@ -1,12 +1,4 @@
# 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 +0,0 @@
require 'jsonapi/serializer/instrumentation'
warn(
'DEPRECATION: Performance instrumentation is no longer automatic. See: ' \
'https://github.com/jsonapi-serializer/jsonapi-serializer' \
'#performance-instrumentation'
)

View File

@ -1,3 +0,0 @@
require 'skylight'
warn('DEPRECATION: Skylight support was moved into the `skylight` gem.')

View File

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

View File

@ -1,350 +1,206 @@
# 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 'oj'
require 'multi_json'
require 'fast_jsonapi/serialization_core'
begin
require 'skylight'
SKYLIGHT_ENABLED = true
rescue LoadError
SKYLIGHT_ENABLED = false
end
module FastJsonapi
module ObjectSerializer
extend ActiveSupport::Concern
include SerializationCore
TRANSFORMS_MAPPING = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}.freeze
included do
# Skylight integration
# To remove Skylight
# Remove the included do block
# Remove the Gemfile entry
if SKYLIGHT_ENABLED
include Skylight::Helpers
instrument_method :serializable_hash
instrument_method :to_json
end
# Set record_type based on the name of the serializer class
set_type(reflected_record_type) if reflected_record_type
set_type default_record_type if default_record_type
end
def initialize(resource, options = {})
process_options(options)
@resource = resource
if options.present?
@meta_tags = options[:meta]
@includes = options[:include].delete_if(&:blank?) if options[:include].present?
self.class.has_permitted_includes(@includes) if @includes.present?
@known_included_objects = {} # keep track of inc objects that have already been serialized
end
# @records if enumerables like Array, ActiveRecord::Relation but if Struct just make it a @record
if resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
@records = resource
else
@record = resource
end
end
def serializable_hash
if self.class.is_collection?(@resource, @is_collection)
return hash_for_collection
end
hash_for_one_record
end
alias 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[:meta] = @meta_tags if @meta_tags.present?
return hash_for_one_record(serializable_hash) if @record
return hash_for_multiple_records(serializable_hash) if @records
serializable_hash
end
def hash_for_collection
serializable_hash = {}
def hash_for_one_record(serializable_hash)
serializable_hash[:data] = self.class.record_hash(@record)
serializable_hash[:included] = self.class.get_included_records(@record, @includes, @known_included_objects) if @includes.present?
serializable_hash
end
def hash_for_multiple_records(serializable_hash)
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?
@records.each do |record|
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
private
def process_options(options)
@fieldsets = deep_symbolize(options[:fields].presence || {})
@params = {}
return if options.blank?
@known_included_objects = Set.new
@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)
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)
end
elsif collection.is_a? Array
collection.map { |i| deep_symbolize(i) }
else
collection.to_sym
end
def serialized_json
self.class.to_json(serializable_hash)
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'))
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
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 use_hyphen
warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
set_key_transform :dash
@hyphenated = true
end
def set_type(type_name)
self.record_type = run_key_transform(type_name)
self.record_type = type_name
if @hyphenated
self.record_type = type_name.to_s.dasherize.to_sym
end
end
def set_id(id_name = nil, &block)
self.record_id = block || id_name
def default_record_type
if self.name.end_with?('Serializer')
class_name = self.name.demodulize
range_end = class_name.rindex('Serializer')
class_name[0...range_end].underscore.to_sym
end
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
self.cached = cache_options[:enabled] || false
self.cache_length = cache_options[:cache_length] || 5.minutes
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
}
end
def attributes(*attributes_list, &block)
def attributes(*attributes_list)
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
)
key = method_name
if @hyphenated
key = attr_name.to_s.dasherize.to_sym
end
attributes_to_serialize[key] = 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)
def has_many(relationship_name, options = {})
singular_name = relationship_name.to_s.singularize
record_type = options[:record_type] || singular_name.to_sym
name = relationship_name.to_sym
key = options[:key] || name
if @hyphenated
key = options[:key] || relationship_name.to_s.dasherize.to_sym
record_type = options[:record_type] || singular_name.to_s.dasherize.to_sym
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),
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],
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]
)
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
end
def serializer_for(name)
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
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(
serializer_key = options[:serializer] || record_type
relationship = {
key: key,
method: block || link_method_name,
options: options
)
name: name,
id_method_name: options[:id_method_name] || (singular_name + '_ids').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_many,
cached: options[:cached] || false
}
add_relationship(name, relationship)
end
def validate_includes!(includes)
return if includes.blank?
def belongs_to(relationship_name, options = {})
name = relationship_name.to_sym
key = options[:key] || name
record_type = options[:record_type] || name
serializer_key = options[:serializer] || record_type
if @hyphenated
key = options[:key] || relationship_name.to_s.dasherize.to_sym
record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym
end
relationship = {
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
}
add_relationship(name, relationship)
end
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
def has_one(relationship_name, options = {})
name = relationship_name.to_sym
key = options[:key] || name
record_type = options[:record_type] || name
serializer_key = options[:serializer] || record_type
if @hyphenated
key = options[:key] || relationship_name.to_s.dasherize.to_sym
record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym
end
relationship = {
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
}
add_relationship(name, relationship)
end
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
end
def compute_serializer_name(serializer_key)
namespace = self.name.gsub(/()?\w+Serializer$/, '')
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
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,6 @@
# frozen_string_literal: true
require 'active_support'
require 'active_support/concern'
require 'digest/sha1'
module FastJsonapi
MandatoryField = Class.new(StandardError)
module SerializationCore
extend ActiveSupport::Concern
@ -16,185 +10,99 @@ 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
end
def id_hash(id, record_type)
return { id: id.to_s, type: record_type } if id.present?
end
def links_hash(record, params = {})
data_links.each_with_object({}) do |(_k, link), hash|
link.serialize(record, params, hash)
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 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)
def attributes_hash(record)
attributes_hash = {}
attributes_to_serialize.each do |key, method_name|
attributes_hash[key] = record.send(method_name)
end
attributes_hash
end
def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
def relationships_hash(record, relationships = nil)
relationships_hash = {}
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 do |_k, relationship|
name = relationship[:key]
id_method_name = relationship[:id_method_name]
record_type = relationship[:record_type]
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
relationships_hash[name] = {
data: ids_hash(record.send(id_method_name), record_type) || empty_case
}
end
relationships_hash
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?
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?
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?
def record_hash(record)
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do
record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type }
record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
record_hash[:relationships] = {}
record_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present?
record_hash
end
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(record.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
def to_json(payload)
MultiJson.dump(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)
included_records = []
includes_list.each do |item|
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?
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
included_records
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)
end
def has_permitted_includes(requested_includes)
# requested includes should be within relationships defined on serializer
allowed_includes = @relationships_to_serialize.keys
intersection = allowed_includes & requested_includes
if intersection.sort == requested_includes.sort
true
else
raise ArgumentError, "One of keys from #{requested_includes} is not specified as a relationship on the serializer"
end
end
end

View File

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

View File

@ -1,8 +0,0 @@
Description:
Generates a serializer for the given model.
Example:
rails generate serializer Movie
This will create:
app/serializers/movie_serializer.rb

View File

@ -1,19 +0,0 @@
# frozen_string_literal: true
require 'rails/generators/base'
class SerializerGenerator < Rails::Generators::NamedBase
source_root File.expand_path('templates', __dir__)
argument :attributes, type: :array, default: [], banner: 'field field'
def create_serializer_file
template 'serializer.rb.tt', File.join('app', 'serializers', class_path, "#{file_name}_serializer.rb")
end
private
def attributes_names
attributes.map { |a| a.name.to_sym.inspect }
end
end

View File

@ -1,6 +0,0 @@
<% module_namespacing do -%>
class <%= class_name %>Serializer
include JSONAPI::Serializer
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

@ -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,67 @@
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
db.execute_batch <<-SQL
insert into suppliers values ('Supplier1', #{@supplier_id});
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
end
# Clean up DB
after(:all) do
File.delete(@db_file) if File.exist?(@db_file)
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,69 @@
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
end
end

View File

@ -0,0 +1,40 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
include_context 'ams movie class'
context 'when using hyphens for word separation in the JSON API members' do
it 'returns correct hash when serializable_hash is called' do
serializable_hash = HyphenMovieSerializer.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 = HyphenMovieSerializer.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 same thing as ams' do
ams_movie = build_ams_movies(1).first
movie = build_movies(1).first
our_json = HyphenMovieSerializer.new([movie]).serialized_json
ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: :dash).to_json
expect(our_json.length).to eq (ams_json.length)
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 = HyphenMovieTypeSerializer.new(movie_type).serializable_hash
expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym
end
end
end

View File

@ -0,0 +1,87 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
include_context 'ams movie class'
context 'when testing performance of serialization' do
it 'should create a hash of 1000 records in less than 25 ms' do
movies = 1000.times.map { |_i| movie }
expect { MovieSerializer.new(movies).serializable_hash }.to perform_under(25).ms
end
it 'should serialize 1000 records to jsonapi in less than 30 ms' do
movies = 1000.times.map { |_i| movie }
expect { MovieSerializer.new(movies).serialized_json }.to perform_under(30).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(count, ams_time, our_time)
format = '%-15s %-10s %s'
puts ''
puts format(format, 'Serializer', 'Records', 'Time')
puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms')
puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms')
end
context 'when comparing with AMS 0.10.x' do
[1, 25, 250, 1000].each do |movie_count|
speed_factor = 25
it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do
ams_movies = build_ams_movies(movie_count)
movies = build_movies(movie_count)
our_json = nil
ams_json = nil
our_serializer = MovieSerializer.new(movies)
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies)
our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
print_stats(movie_count, ams_time, our_time)
expect(our_json.length).to eq ams_json.length
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
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|
speed_factor = 25
it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do
ams_movies = build_ams_movies(movie_count)
movies = build_movies(movie_count)
our_json = nil
ams_json = nil
options = {}
options[:meta] = { total: movie_count }
options[:include] = [:actors, :movie_type]
our_serializer = MovieSerializer.new(movies, options)
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta])
our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
print_stats(movie_count, ams_time, our_time)
expect(our_json.length).to eq ams_json.length
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
end
end
end
end

View File

@ -0,0 +1,126 @@
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 serializing []' do
json = MovieSerializer.new([]).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']).to eq []
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 '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 'shouldnt set default_type for a serializer that doesnt follow convention' 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,84 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context "movie 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 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,83 @@
RSpec.shared_context 'ams movie class' do
before(:context) do
# models
class AMSMovie < ActiveModelSerializers::Model
attr_accessor :id, :name, :release_year, :actors, :owner, :movie_type
end
class AMSActor < ActiveModelSerializers::Model
attr_accessor :id, :name, :email
end
class AMSUser < ActiveModelSerializers::Model
attr_accessor :id, :name
end
class AMSMovieType < ActiveModelSerializers::Model
attr_accessor :id, :name
end
# serializers
class AMSMovieSerializer < ActiveModel::Serializer
type 'movie'
attributes :name, :release_year
has_many :actors
has_one :owner
belongs_to :movie_type
end
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
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,192 @@
RSpec.shared_context 'movie class' do
# Movie, Actor Classes and serializers
before(:context) do
# models
class Movie
attr_accessor :id, :name, :release_year, :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
# serializers
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
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
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
# Hyphenated keys for the serializer
before(:context) do
class HyphenMovieSerializer
include FastJsonapi::ObjectSerializer
use_hyphen
set_type :movie
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
class HyphenMovieTypeSerializer
include FastJsonapi::ObjectSerializer
use_hyphen
set_type :movie_type
attributes :name
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
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|
a = ActorStruct.new
a[:id] = id
a[:name] = id.to_s
actors << a
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
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,15 @@
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 'multi_json'
require 'byebug'
require 'securerandom'
require 'active_model_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
end
config.include RSpec::Benchmark::Matchers
end
ActiveModel::Serializer.config.adapter = :json_api
ActiveModel::Serializer.config.key_transform = :underscore
ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new('/dev/null'))