Compare commits

..

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

70 changed files with 4570 additions and 1368 deletions

View File

@ -1,19 +1,19 @@
name: CI name: CI
on: [push, pull_request] on: [push]
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-18.04
strategy: strategy:
matrix: matrix:
ruby: [2.4, 2.7, '3.0', 3.1, truffleruby-head] ruby: [2.4, 2.5, 2.6, 2.7]
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Sets up the Ruby version - name: Sets up the Ruby version
uses: ruby/setup-ruby@v1 uses: actions/setup-ruby@v1
with: with:
ruby-version: ${{ matrix.ruby }} ruby-version: ${{ matrix.ruby }}
@ -24,17 +24,17 @@ jobs:
bundle install bundle install
- name: Runs code QA and tests - name: Runs code QA and tests
run: bundle exec rake run: rspec
- name: Publish to Rubygems - name: Publish to GPR
continue-on-error: true if: ${{ startsWith(github.ref, 'refs/tags/') }}
if: ${{ github.ref == 'refs/heads/master' }}
run: | run: |
mkdir -p $HOME/.gem mkdir -p $HOME/.gem
touch $HOME/.gem/credentials touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec gem build
gem push *.gem gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
env: env:
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}} GEM_HOST_API_KEY: ${{secrets.GPR_AUTH_TOKEN}}
OWNER: fast_jsonapi

5
.gitignore vendored
View File

@ -41,8 +41,5 @@ test.db
/vendor /vendor
# Don't checkin Gemfile.lock # Don't checkin Gemfile.lock
# See: https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/ # See: http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
Gemfile.lock 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

@ -4,50 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- ...
## [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 ## [1.7.0] - 2020-04-29
### Added ### Added
- Serializer option support for procs (#32) - Serializer option support for procs (#32)

View File

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

243
README.md
View File

@ -1,29 +1,23 @@
# JSON:API Serialization Library # Fast JSON API
## :warning: :construction: v2 (the `master` branch) is in maintenance mode! :construction: :warning: A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.
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!
# Performance Comparison # Performance Comparison
We compare serialization times with `ActiveModelSerializer` and alternative We compare serialization times with Active Model Serializer as part of RSpec
implementations as part of performance tests available at performance tests included on this library. We want to ensure that with every
[jsonapi-serializer/comparisons](https://github.com/jsonapi-serializer/comparisons). change on this library, serialization time is about _25 times_ faster than
the ActiveModelSerializers on up to a current benchmark of 1000 records. Please
read the performance article in the `docs` folder for any questions related to
methodology.
We want to ensure that with every ## Benchmark times for 250 records
change on this library, serialization time stays significantly faster than
the performance provided by the alternatives. Please read the performance ```bash
article in the `docs` folder for any questions related to methodology. $ rspec
Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms
```
# Table of Contents # Table of Contents
@ -44,9 +38,6 @@ article in the `docs` folder for any questions related to methodology.
* [Specifying a Relationship Serializer](#specifying-a-relationship-serializer) * [Specifying a Relationship Serializer](#specifying-a-relationship-serializer)
* [Sparse Fieldsets](#sparse-fieldsets) * [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods) * [Using helper methods](#using-helper-methods)
* [Performance Instrumentation](#performance-instrumentation)
* [Deserialization](#deserialization)
* [Migrating from Netflix/fast_jsonapi](#migrating-from-netflixfast_jsonapi)
* [Contributing](#contributing) * [Contributing](#contributing)
@ -63,7 +54,7 @@ article in the `docs` folder for any questions related to methodology.
Add this line to your application's Gemfile: Add this line to your application's Gemfile:
```ruby ```ruby
gem 'jsonapi-serializer' gem 'fast_jsonapi', '~> 1.6.0', git: 'https://github.com/fast-jsonapi/fast_jsonapi'
``` ```
Execute: Execute:
@ -94,8 +85,7 @@ end
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
set_type :movie # optional set_type :movie # optional
set_id :owner_id # optional set_id :owner_id # optional
attributes :name, :year attributes :name, :year
@ -176,16 +166,12 @@ json_string = MovieSerializer.new(movie).serializable_hash.to_json
``` ```
#### The Optionality of `set_type`
By default fast_jsonapi will try to figure the type based on the name of the serializer class. For example `class MovieSerializer` will automatically have a type of `:movie`. If your serializer class name does not follow this format, you have to manually state the `set_type` at the serializer.
### Key Transforms ### 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 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 ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
# Available options :camel, :camel_lower, :dash, :underscore(default) # Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel set_key_transform :camel
end end
@ -200,13 +186,13 @@ set_key_transform :underscore # "some_key" => "some_key"
``` ```
### Attributes ### Attributes
Attributes are defined using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute. Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized: 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 ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attribute :name attribute :name
end end
@ -216,7 +202,7 @@ Custom attributes that must be serialized but do not exist on the model can be d
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attributes :name, :year attributes :name, :year
@ -230,7 +216,7 @@ The block syntax can also be used to override the property on the object:
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attribute :name do |object| attribute :name do |object|
"#{object.name} Part 2" "#{object.name} Part 2"
@ -242,7 +228,7 @@ Attributes can also use a different name by passing the original method or acces
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attributes :name attributes :name
@ -251,7 +237,7 @@ end
``` ```
### Links Per Object ### 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. Links are defined in FastJsonapi 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 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.
@ -259,29 +245,29 @@ You can also use a block to define a url as shown in `custom_url`. You can acces
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
link :public_url link :public_url
link :self, :url link :self, :url
link :custom_url do |object| link :custom_url do |object|
"https://movies.com/#{object.name}-(#{object.year})" "http://movies.com/#{object.name}-(#{object.year})"
end end
link :personalized_url do |object, params| link :personalized_url do |object, params|
"https://movies.com/#{object.name}-#{params[:user].reference_code}" "http://movies.com/#{object.name}-#{params[:user].reference_code}"
end end
end end
``` ```
#### Links on a Relationship #### 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)) You can specify [relationship links](http://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](http://jsonapi.org/format/#document-resource-object-related-resource-links))
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
has_many :actors, links: { has_many :actors, links: {
self: :url, self: :url,
@ -316,7 +302,7 @@ For every resource in the collection, you can include a meta object containing n
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
meta do |movie| meta do |movie|
{ {
@ -326,23 +312,6 @@ class MovieSerializer
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 ### Compound Document
Support for top-level and nested included associations through `options[:include]`. Support for top-level and nested included associations through `options[:include]`.
@ -398,10 +367,10 @@ To enable caching, use `cache_options store: <cache_store>`:
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
# use rails cache with a separate namespace and fixed expiry # use rails cache with a separate namespace and fixed expiry
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour cache_options store: Rails.cache, namespace: 'fast-jsonapi', expires_in: 1.hour
end end
``` ```
@ -412,31 +381,12 @@ end
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports - `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 - `&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: So for the example above, FastJsonapi will call the cache instance like this:
```ruby ```ruby
Rails.cache.fetch(record, namespace: 'jsonapi-serializer', expires_in: 1.hour) { ... } Rails.cache.fetch(record, namespace: 'fast-jsonapi, 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 ### Params
In some cases, attribute values might require more information than what is In some cases, attribute values might require more information than what is
@ -451,7 +401,7 @@ parameter.
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
set_id do |movie, params| set_id do |movie, params|
# in here, params is a hash containing the `:admin` key # in here, params is a hash containing the `:admin` key
@ -466,7 +416,7 @@ class MovieSerializer
belongs_to :primary_agent do |movie, params| belongs_to :primary_agent do |movie, params|
# in here, params is a hash containing the `:current_user` key # in here, params is a hash containing the `:current_user` key
params[:current_user] params[:current_user].is_employee? ? true : false
end end
end end
@ -485,7 +435,7 @@ Conditional attributes can be defined by passing a Proc to the `if` key on the `
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attributes :name, :year attributes :name, :year
attribute :release_year, if: Proc.new { |record| attribute :release_year, if: Proc.new { |record|
@ -497,13 +447,6 @@ class MovieSerializer
# The director will be serialized only if the :admin key of params is true # The director will be serialized only if the :admin key of params is true
params && params[:admin] == 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 end
# ... # ...
@ -518,7 +461,7 @@ Conditional relationships can be defined by passing a Proc to the `if` key. Retu
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
# Actors will only be serialized if the record has any associated actors # Actors will only be serialized if the record has any associated actors
has_many :actors, if: Proc.new { |record| record.actors.any? } has_many :actors, if: Proc.new { |record| record.actors.any? }
@ -539,7 +482,7 @@ In many cases, the relationship can automatically detect the serializer to use.
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
# resolves to StudioSerializer # resolves to StudioSerializer
belongs_to :studio belongs_to :studio
@ -552,7 +495,7 @@ At other times, such as when a property name differs from the class name, you ma
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
# resolves to MovieStudioSerializer # resolves to MovieStudioSerializer
belongs_to :studio, serializer: :movie_studio belongs_to :studio, serializer: :movie_studio
@ -565,7 +508,7 @@ For more advanced cases, such as polymorphic relationships and Single Table Inhe
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
has_many :actors, serializer: Proc.new do |record, params| has_many :actors, serializer: Proc.new do |record, params|
if record.comedian? if record.comedian?
@ -585,7 +528,7 @@ Attributes and relationships can be selectively returned per record type by usin
```ruby ```ruby
class MovieSerializer class MovieSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attributes :name, :year attributes :name, :year
end end
@ -616,7 +559,7 @@ module AvatarHelper
end end
class UserSerializer class UserSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
include AvatarHelper # mixes in your helper method as class method include AvatarHelper # mixes in your helper method as class method
@ -641,7 +584,7 @@ module AvatarHelper
end end
class UserSerializer class UserSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
extend AvatarHelper # mixes in your helper method as class method extend AvatarHelper # mixes in your helper method as class method
@ -671,27 +614,36 @@ serializer | Set custom Serializer for a relationship | `has_many :actors, seria
polymorphic | Allows different record types for a polymorphic association | `has_many :targets, polymorphic: true` 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 }` polymorphic | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
### Performance Instrumentation ### Instrumentation
Performance instrumentation is available by using the `fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer:
`active_support/notifications`.
To enable it, include the module in your serializer class:
```ruby ```ruby
require 'jsonapi/serializer' require 'fast_jsonapi/instrumentation/skylight'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
``` ```
[Skylight](https://www.skylight.io/) integration is also available and Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:
supported by us, follow the Skylight documentation to enable it.
```ruby
require 'fast_jsonapi/instrumentation'
```
The two instrumented notifications are supplied by these two constants:
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`
It is also possible to instrument one method without the other by using one of the following require statements:
```ruby
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'
```
Same goes for the Skylight integration:
```ruby
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
```
### Running Tests ### Running Tests
The project has and requires unit tests, functional tests and performance The project has and requires unit tests, functional tests and performance
@ -701,59 +653,16 @@ tests. To run tests use the following command:
rspec rspec
``` ```
## Deserialization To run tests without the performance tests (for quicker test runs):
We currently do not support deserialization, but we recommend to use any of the next gems:
### [JSONAPI.rb](https://github.com/stas/jsonapi.rb) ```bash
rspec spec --tag ~performance:true
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 To run tests only performance tests:
```diff ```bash
class MovieSerializer rspec spec --tag performance:true
- 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 ## Contributing
@ -763,4 +672,4 @@ pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and This project is intended to be a safe, welcoming space for collaboration, and
contributors are expected to adhere to the contributors are expected to adhere to the
[Contributor Covenant](https://contributor-covenant.org) code of conduct. [Contributor Covenant](http://contributor-covenant.org) code of conduct.

View File

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

View File

@ -13,7 +13,7 @@ require 'oj'
require 'fast_jsonapi' require 'fast_jsonapi'
class BaseSerializer class BaseSerializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
def to_json def to_json
Oj.dump(serializable_hash) Oj.dump(serializable_hash)

View File

@ -23,7 +23,7 @@ cases.
We came up with patterns that we can rely upon such as: We came up with patterns that we can rely upon such as:
* We always use [JSON:API](https://jsonapi.org/) for our APIs * We always use [JSON:API](http://jsonapi.org/) for our APIs
* We almost always serialize a homogenous list of objects (Example: An array of * We almost always serialize a homogenous list of objects (Example: An array of
movies) movies)

39
fast_jsonapi.gemspec Normal file
View File

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

View File

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

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'jsonapi/serializer/errors'
module FastJsonapi module FastJsonapi
require 'fast_jsonapi/object_serializer' require 'fast_jsonapi/object_serializer'
if defined?(::Rails) if defined?(::Rails)

View File

@ -6,16 +6,7 @@ module FastJsonapi
# @param [Array<Object>] *params any number of parameters to be passed to the Proc # @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 # @return [Object] the result of the Proc call with the supplied parameters
def call_proc(proc, *params) def call_proc(proc, *params)
# The parameters array for a lambda created from a symbol (&:foo) differs proc.call(*params.take(proc.parameters.length))
# 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 end
end end

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
require 'skylight'
SKYLIGHT_NORMALIZER_BASE_CLASS = begin
::Skylight::Core::Normalizers::Normalizer
rescue NameError
::Skylight::Normalizers::Normalizer
end

View File

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

View File

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

View File

@ -1,6 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'active_support'
require 'active_support/time' require 'active_support/time'
require 'active_support/concern' require 'active_support/concern'
require 'active_support/inflector' require 'active_support/inflector'
@ -16,6 +15,8 @@ module FastJsonapi
extend ActiveSupport::Concern extend ActiveSupport::Concern
include SerializationCore include SerializationCore
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
TRANSFORMS_MAPPING = { TRANSFORMS_MAPPING = {
camel: :camelize, camel: :camelize,
camel_lower: [:camelize, :lower], camel_lower: [:camelize, :lower],
@ -35,13 +36,11 @@ module FastJsonapi
end end
def serializable_hash def serializable_hash
if self.class.is_collection?(@resource, @is_collection) return hash_for_collection if is_collection?(@resource, @is_collection)
return hash_for_collection
end
hash_for_one_record hash_for_one_record
end end
alias to_hash serializable_hash alias_method :to_hash, :serializable_hash
def hash_for_one_record def hash_for_one_record
serializable_hash = { data: nil } serializable_hash = { data: nil }
@ -73,6 +72,15 @@ module FastJsonapi
serializable_hash serializable_hash
end end
def serialized_json
warn(
'DEPRECATION: `#serialized_json` will be removed in the next release. '\
'More details: https://github.com/fast-jsonapi/fast_jsonapi/pull/44'
)
serializable_hash.to_json
end
alias_method :to_json, :serialized_json
private private
def process_options(options) def process_options(options)
@ -81,12 +89,12 @@ module FastJsonapi
return if options.blank? return if options.blank?
@known_included_objects = Set.new @known_included_objects = {}
@meta = options[:meta] @meta = options[:meta]
@links = options[:links] @links = options[:links]
@is_collection = options[:is_collection] @is_collection = options[:is_collection]
@params = options[:params] || {} @params = options[:params] || {}
raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash) raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
if options[:include].present? if options[:include].present?
@includes = options[:include].reject(&:blank?).map(&:to_sym) @includes = options[:include].reject(&:blank?).map(&:to_sym)
@ -106,15 +114,13 @@ module FastJsonapi
end end
end end
class_methods do def is_collection?(resource, force_is_collection = nil)
# Detects a collection/enumerable return force_is_collection unless force_is_collection.nil?
#
# @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) resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
end end
class_methods do
def inherited(subclass) def inherited(subclass)
super(subclass) super(subclass)
@ -134,7 +140,11 @@ module FastJsonapi
def reflected_record_type def reflected_record_type
return @reflected_record_type if defined?(@reflected_record_type) return @reflected_record_type if defined?(@reflected_record_type)
@reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer')) @reflected_record_type ||= begin
if self.name && self.name.end_with?('Serializer')
self.name.split('::').last.chomp('Serializer').underscore.to_sym
end
end
end end
def set_key_transform(transform_name) def set_key_transform(transform_name)
@ -143,14 +153,13 @@ module FastJsonapi
# ensure that the record type is correctly transformed # ensure that the record type is correctly transformed
if record_type if record_type
set_type(record_type) set_type(record_type)
# TODO: Remove dead code
elsif reflected_record_type elsif reflected_record_type
set_type(reflected_record_type) set_type(reflected_record_type)
end end
end end
def run_key_transform(input) def run_key_transform(input)
if transform_method.present? if self.transform_method.present?
input.to_s.send(*@transform_method).to_sym input.to_s.send(*@transform_method).to_sym
else else
input.to_sym input.to_sym
@ -172,7 +181,7 @@ module FastJsonapi
def cache_options(cache_options) def cache_options(cache_options)
# FIXME: remove this if block once deprecated cache_options are not supported anymore # FIXME: remove this if block once deprecated cache_options are not supported anymore
unless cache_options.key?(:store) if !cache_options.key?(:store)
# fall back to old, deprecated behaviour because no store was passed. # fall back to old, deprecated behaviour because no store was passed.
# we assume the user explicitly wants new behaviour if he passed a # we assume the user explicitly wants new behaviour if he passed a
# store because this is the new syntax. # store because this is the new syntax.
@ -202,7 +211,7 @@ module FastJsonapi
def attributes(*attributes_list, &block) def attributes(*attributes_list, &block)
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array) attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {} options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
self.attributes_to_serialize = {} if attributes_to_serialize.nil? self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
# to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }` # to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
block = attributes_list.pop if attributes_list.last.is_a?(Proc) block = attributes_list.pop if attributes_list.last.is_a?(Proc)
@ -225,14 +234,12 @@ module FastJsonapi
self.cachable_relationships_to_serialize = {} if cachable_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? self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
# TODO: Remove this undocumented option. if !relationship.cached
# Delegate the caching to the serializer exclusively. self.uncachable_relationships_to_serialize[relationship.name] = relationship
if relationship.cached
cachable_relationships_to_serialize[relationship.name] = relationship
else else
uncachable_relationships_to_serialize[relationship.name] = relationship self.cachable_relationships_to_serialize[relationship.name] = relationship
end end
relationships_to_serialize[relationship.name] = relationship self.relationships_to_serialize[relationship.name] = relationship
end end
def has_many(relationship_name, options = {}, &block) def has_many(relationship_name, options = {}, &block)
@ -258,9 +265,11 @@ module FastJsonapi
name = base_key.to_sym name = base_key.to_sym
if relationship_type == :has_many if relationship_type == :has_many
base_serialization_key = base_key.to_s.singularize base_serialization_key = base_key.to_s.singularize
base_key_sym = base_serialization_key.to_sym
id_postfix = '_ids' id_postfix = '_ids'
else else
base_serialization_key = base_key base_serialization_key = base_key
base_key_sym = name
id_postfix = '_id' id_postfix = '_id'
end end
polymorphic = fetch_polymorphic_option(options) polymorphic = fetch_polymorphic_option(options)
@ -285,7 +294,6 @@ module FastJsonapi
polymorphic: polymorphic, polymorphic: polymorphic,
conditional_proc: options[:if], conditional_proc: options[:if],
transform_method: @transform_method, transform_method: @transform_method,
meta: options[:meta],
links: options[:links], links: options[:links],
lazy_load_data: options[:lazy_load_data] lazy_load_data: options[:lazy_load_data]
) )
@ -301,14 +309,14 @@ module FastJsonapi
def serializer_for(name) def serializer_for(name)
namespace = self.name.gsub(/()?\w+Serializer$/, '') namespace = self.name.gsub(/()?\w+Serializer$/, '')
serializer_name = "#{name.to_s.demodulize.classify}Serializer" serializer_name = name.to_s.demodulize.classify + 'Serializer'
serializer_class_name = namespace + serializer_name serializer_class_name = namespace + serializer_name
begin begin
serializer_class_name.constantize return serializer_class_name.constantize
rescue NameError rescue NameError
raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \ raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " +
"Attempted to find '#{serializer_class_name}'. " \ "Attempted to find '#{serializer_class_name}'. " +
'Consider specifying the serializer directly through options[:serializer].' "Consider specifying the serializer directly through options[:serializer]."
end end
end end
@ -316,20 +324,19 @@ module FastJsonapi
option = options[:polymorphic] option = options[:polymorphic]
return false unless option.present? return false unless option.present?
return option if option.respond_to? :keys return option if option.respond_to? :keys
{} {}
end end
# def link(link_name, link_method_name = nil, &block) # def link(link_name, link_method_name = nil, &block)
def link(*params, &block) def link(*params, &block)
self.data_links = {} if data_links.nil? self.data_links = {} if self.data_links.nil?
options = params.last.is_a?(Hash) ? params.pop : {} options = params.last.is_a?(Hash) ? params.pop : {}
link_name = params.first link_name = params.first
link_method_name = params[-1] link_method_name = params[-1]
key = run_key_transform(link_name) key = run_key_transform(link_name)
data_links[key] = Link.new( self.data_links[key] = Link.new(
key: key, key: key,
method: block || link_method_name, method: block || link_method_name,
options: options options: options
@ -339,11 +346,20 @@ module FastJsonapi
def validate_includes!(includes) def validate_includes!(includes)
return if includes.blank? return if includes.blank?
parse_includes_list(includes).each_key do |include_item| includes.each do |include_item|
relationship_to_include = relationships_to_serialize[include_item] klass = self
raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_include parse_include_item(include_item).each do |parsed_include|
relationships_to_serialize = klass.relationships_to_serialize || {}
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class. relationship_to_include = relationships_to_serialize[parsed_include]
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
if relationship_to_include.static_serializer
klass = relationship_to_include.static_serializer
else
# the serializer may change based on the object (e.g. polymorphic relationships),
# so inner relationships cannot be validated
break
end
end
end end
end end
end end

View File

@ -1,6 +1,6 @@
module FastJsonapi module FastJsonapi
class Relationship 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 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, :lazy_load_data
def initialize( def initialize(
owner:, owner:,
@ -12,12 +12,11 @@ module FastJsonapi
object_block:, object_block:,
serializer:, serializer:,
relationship_type:, relationship_type:,
cached: false,
polymorphic:, polymorphic:,
conditional_proc:, conditional_proc:,
transform_method:, transform_method:,
links:, links:,
meta:,
cached: false,
lazy_load_data: false lazy_load_data: false
) )
@owner = owner @owner = owner
@ -34,7 +33,6 @@ module FastJsonapi
@conditional_proc = conditional_proc @conditional_proc = conditional_proc
@transform_method = transform_method @transform_method = transform_method
@links = links || {} @links = links || {}
@meta = meta || {}
@lazy_load_data = lazy_load_data @lazy_load_data = lazy_load_data
@record_types_for = {} @record_types_for = {}
@serializers_for_name = {} @serializers_for_name = {}
@ -45,16 +43,15 @@ module FastJsonapi
empty_case = relationship_type == :has_many ? [] : nil empty_case = relationship_type == :has_many ? [] : nil
output_hash[key] = {} output_hash[key] = {}
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case unless lazy_load_data && !included unless (lazy_load_data && !included)
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
add_meta_hash(record, serialization_params, output_hash) if meta.present? end
add_links_hash(record, serialization_params, output_hash) if links.present? add_links_hash(record, serialization_params, output_hash) if links.present?
end end
end end
def fetch_associated_object(record, params) def fetch_associated_object(record, params)
return FastJsonapi.call_proc(object_block, record, params) unless object_block.nil? return FastJsonapi.call_proc(object_block, record, params) unless object_block.nil?
record.send(object_method_name) record.send(object_method_name)
end end
@ -67,9 +64,8 @@ module FastJsonapi
end end
def serializer_for(record, serialization_params) def serializer_for(record, serialization_params)
# TODO: Remove this, dead code...
if @static_serializer if @static_serializer
@static_serializer return @static_serializer
elsif polymorphic elsif polymorphic
name = polymorphic[record.class] if polymorphic.is_a?(Hash) name = polymorphic[record.class] if polymorphic.is_a?(Hash)
@ -83,7 +79,6 @@ module FastJsonapi
serializer_for_name(record.class.name) serializer_for_name(record.class.name)
else else
# TODO: Remove this, dead code...
raise "Unknown serializer for object #{record.inspect}" raise "Unknown serializer for object #{record.inspect}"
end end
end end
@ -107,11 +102,9 @@ module FastJsonapi
return unless associated_object = fetch_associated_object(record, params) return unless associated_object = fetch_associated_object(record, params)
if associated_object.respond_to? :map return associated_object.map do |object|
return associated_object.map do |object| id_hash_from_record object, params
id_hash_from_record object, params end if associated_object.respond_to? :map
end
end
id_hash_from_record associated_object, params id_hash_from_record associated_object, params
end end
@ -123,11 +116,10 @@ module FastJsonapi
def ids_hash(ids, record_type) def ids_hash(ids, record_type)
return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map 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 id_hash(ids, record_type) # ids variable is just a single id here
end end
def id_hash(id, record_type, default_return = false) def id_hash(id, record_type, default_return=false)
if id.present? if id.present?
{ id: id.to_s, type: record_type } { id: id.to_s, type: record_type }
else else
@ -139,33 +131,24 @@ module FastJsonapi
if object_block.present? if object_block.present?
object = FastJsonapi.call_proc(object_block, record, params) 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.map { |item| item.public_send(id_method_name) } if object.respond_to? :map
return object.try(id_method_name) return object.try(id_method_name)
end end
record.public_send(id_method_name) record.public_send(id_method_name)
end end
def add_links_hash(record, params, output_hash) def add_links_hash(record, params, output_hash)
output_hash[key][:links] = if links.is_a?(Symbol) if links.is_a?(Symbol)
record.public_send(links) output_hash[key][:links] = record.public_send(links)
else else
links.each_with_object({}) do |(key, method), hash| output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash) Link.new(key: key, method: method).serialize(record, params, hash)\
end end
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 end
def run_key_transform(input) def run_key_transform(input)
if transform_method.present? if self.transform_method.present?
input.to_s.send(*transform_method).to_sym input.to_s.send(*self.transform_method).to_sym
else else
input.to_sym input.to_sym
end end
@ -173,7 +156,6 @@ module FastJsonapi
def initialize_static_serializer def initialize_static_serializer
return if @initialized_static_serializer return if @initialized_static_serializer
@static_serializer = compute_static_serializer @static_serializer = compute_static_serializer
@static_record_type = compute_static_record_type @static_record_type = compute_static_record_type
@initialized_static_serializer = true @initialized_static_serializer = true
@ -217,20 +199,14 @@ module FastJsonapi
def record_type_for(record, serialization_params) def record_type_for(record, serialization_params)
# if the record type is static, return it # if the record type is static, return it
return @static_record_type if @static_record_type return @static_record_type if @static_record_type
# if not, use the record type of the serializer, and memoize the transformed version # if not, use the record type of the serializer, and memoize the transformed version
serializer = serializer_for(record, serialization_params) serializer = serializer_for(record, serialization_params)
@record_types_for[serializer] ||= run_key_transform(serializer.record_type) @record_types_for[serializer] ||= run_key_transform(serializer.record_type)
end end
def compute_static_record_type def compute_static_record_type
if polymorphic return run_key_transform(record_type) if record_type
nil return run_key_transform(@static_serializer.record_type) if @static_serializer
elsif record_type
run_key_transform(record_type)
elsif @static_serializer
run_key_transform(@static_serializer.record_type)
end
end end
end end
end end

View File

@ -10,10 +10,10 @@ module FastJsonapi
def serialize(record, serialization_params, output_hash) def serialize(record, serialization_params, output_hash)
if conditionally_allowed?(record, serialization_params) if conditionally_allowed?(record, serialization_params)
if method.is_a?(Proc) output_hash[key] = if method.is_a?(Proc)
output_hash[key] = FastJsonapi.call_proc(method, record, serialization_params) FastJsonapi.call_proc(method, record, serialization_params)
else else
output_hash[key] = record.public_send(method) record.public_send(method)
end end
end end
end end

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'active_support'
require 'active_support/concern' require 'active_support/concern'
require 'digest/sha1'
module FastJsonapi module FastJsonapi
MandatoryField = Class.new(StandardError) MandatoryField = Class.new(StandardError)
@ -27,7 +25,7 @@ module FastJsonapi
end end
class_methods do class_methods do
def id_hash(id, record_type, default_return = false) def id_hash(id, record_type, default_return=false)
if id.present? if id.present?
{ id: id.to_s, type: record_type } { id: id.to_s, type: record_type }
else else
@ -68,132 +66,83 @@ module FastJsonapi
def record_hash(record, fieldset, includes_list, params = {}) def record_hash(record, fieldset, includes_list, params = {})
if cache_store_instance if cache_store_instance
cache_opts = record_cache_options(cache_store_options, fieldset, includes_list, params) record_hash = cache_store_instance.fetch(record, **cache_store_options) do
record_hash = cache_store_instance.fetch(record, **cache_opts) do
temp_hash = id_hash(id_from_record(record, params), record_type, true) temp_hash = id_hash(id_from_record(record, params), record_type, true)
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present? temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
temp_hash[:relationships] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present? temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
temp_hash[:links] = links_hash(record, params) if data_links.present? temp_hash[:links] = links_hash(record, params) if data_links.present?
temp_hash temp_hash
end end
record_hash[:relationships] = (record_hash[:relationships] || {}).merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present? record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
else else
record_hash = id_hash(id_from_record(record, params), record_type, true) record_hash = id_hash(id_from_record(record, params), record_type, true)
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present? record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, fieldset, includes_list, params) if relationships_to_serialize.present? record_hash[:relationships] = relationships_hash(record, nil, fieldset, includes_list, params) if relationships_to_serialize.present?
record_hash[:links] = links_hash(record, params) if data_links.present? record_hash[:links] = links_hash(record, params) if data_links.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
end end
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
end 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) def id_from_record(record, params)
return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc) return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc)
return record.send(record_id) if record_id return record.send(record_id) if record_id
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id) raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
record.id record.id
end end
# It chops out the root association (first part) from each include. def parse_include_item(include_item)
# return [include_item.to_sym] unless include_item.to_s.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. include_item.to_s.split('.').map!(&:to_sym)
# end
# This method will turn that include array into a Hash that looks like:
# def remaining_items(items)
# { return unless items.size > 1
# authors: Set.new([
# 'books', [items[1..-1].join('.').to_sym]
# '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
end end
# includes handler # includes handler
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {}) def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
return unless includes_list.present? return unless includes_list.present?
return [] unless relationships_to_serialize
includes_list = parse_includes_list(includes_list) includes_list.sort.each_with_object([]) do |include_item, included_records|
items = parse_include_item(include_item)
remaining_items = remaining_items(items)
includes_list.each_with_object([]) do |include_item, included_records| items.each do |item|
relationship_item = relationships_to_serialize[include_item.first] next unless relationships_to_serialize && relationships_to_serialize[item]
relationship_item = relationships_to_serialize[item]
next unless relationship_item.include_relationship?(record, params)
relationship_type = relationship_item.relationship_type
next unless relationship_item&.include_relationship?(record, params) included_objects = relationship_item.fetch_associated_object(record, params)
next if included_objects.blank?
included_objects = [included_objects] unless relationship_type == :has_many
included_objects = Array(relationship_item.fetch_associated_object(record, params)) static_serializer = relationship_item.static_serializer
next if included_objects.empty? static_record_type = relationship_item.static_record_type
static_serializer = relationship_item.static_serializer included_objects.each do |inc_obj|
static_record_type = relationship_item.static_record_type serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type
included_objects.each do |inc_obj| if remaining_items.present?
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params) serializer_records = serializer.get_included_records(inc_obj, remaining_items, known_included_objects, fieldsets, params)
record_type = static_record_type || serializer.record_type included_records.concat(serializer_records) unless serializer_records.empty?
end
if include_item.last.any? code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params) next if known_included_objects.key?(code)
included_records.concat(serializer_records) unless serializer_records.empty?
known_included_objects[code] = inc_obj
included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params)
end 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 end
end end
end end

View File

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

View File

@ -13,7 +13,7 @@ class SerializerGenerator < Rails::Generators::NamedBase
private private
def attributes_names def attributes_names
attributes.map { |a| a.name.to_sym.inspect } attributes.map { |a| a.name.to_sym.inspect }
end end
end end

View File

@ -1,6 +1,6 @@
<% module_namespacing do -%> <% module_namespacing do -%>
class <%= class_name %>Serializer class <%= class_name %>Serializer
include JSONAPI::Serializer include FastJsonapi::ObjectSerializer
attributes <%= attributes_names.join(", ") %> attributes <%= attributes_names.join(", ") %>
end end
<% 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,155 @@
require 'spec_helper'
require 'active_record'
require 'sqlite3'
describe 'active record' do
# Setup DB
before(:all) do
@db_file = "test.db"
# Open a database
db = SQLite3::Database.new @db_file
# Create tables
db.execute_batch <<-SQL
create table suppliers (
name varchar(30),
id int primary key
);
create table accounts (
name varchar(30),
id int primary key,
supplier_id int,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
);
SQL
# Insert records
@account_id = 2
@supplier_id = 1
@supplier_id_without_account = 3
db.execute_batch <<-SQL
insert into suppliers values ('Supplier1', #{@supplier_id}),
('SupplierWithoutAccount', #{@supplier_id_without_account});
insert into accounts values ('Dollar Account', #{@account_id}, #{@supplier_id});
SQL
end
# Setup Active Record
before(:all) do
class Supplier < ActiveRecord::Base
has_one :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
end
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => @db_file
)
end
context 'has one patch' do
it 'has account_id method for a supplier' do
expect(Supplier.first.respond_to?(:account_id)).to be true
expect(Supplier.first.account_id).to eq @account_id
end
it 'has account_id method return nil if account not present' do
expect(Supplier.find(@supplier_id_without_account).account_id).to eq nil
end
end
# Clean up DB
after(:all) do
File.delete(@db_file) if File.exist?(@db_file)
end
end
describe 'active record has_one through' do
# Setup DB
before(:all) do
@db_file = "test_two.db"
# Open a database
db = SQLite3::Database.new @db_file
# Create tables
db.execute_batch <<-SQL
create table forests (
id int primary key,
name varchar(30)
);
create table trees (
id int primary key,
forest_id int,
name varchar(30),
FOREIGN KEY (forest_id) REFERENCES forests(id)
);
create table fruits (
id int primary key,
tree_id int,
name varchar(30),
FOREIGN KEY (tree_id) REFERENCES trees(id)
);
SQL
# Insert records
db.execute_batch <<-SQL
insert into forests values (1, 'sherwood');
insert into trees values (2, 1,'pine');
insert into fruits values (3, 2, 'pine nut');
insert into fruits(id,name) values (4,'apple');
SQL
end
# Setup Active Record
before(:all) do
class Forest < ActiveRecord::Base
has_many :trees
end
class Tree < ActiveRecord::Base
belongs_to :forest
end
class Fruit < ActiveRecord::Base
belongs_to :tree
has_one :forest, through: :tree
end
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => @db_file
)
end
context 'revenue' do
it 'has an forest_id' do
expect(Fruit.find(3).respond_to?(:forest_id)).to be true
expect(Fruit.find(3).forest_id).to eq 1
expect(Fruit.find(3).forest.name).to eq "sherwood"
end
it 'has nil if tree id not available' do
expect(Fruit.find(4).respond_to?(:tree_id)).to be true
expect(Fruit.find(4).forest_id).to eq nil
end
end
# Clean up DB
after(:all) do
File.delete(@db_file) if File.exist?(@db_file)
end
end

73
spec/lib/helpers_spec.rb Normal file
View File

@ -0,0 +1,73 @@
require 'spec_helper'
describe FastJsonapi do
describe '.call_proc' do
context 'with a Proc' do
context 'with no parameters' do
let(:function) { proc { 42 } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(42)
end
end
context 'with a single parameter' do
let(:function) { proc { |a| 42 + a } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(43)
end
end
context 'with multiple parameters' do
let(:function) { proc { |a, b| 42 + a + b } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(45)
end
end
context 'with default parameters' do
let(:function) { proc { |a = 0, b = 0| 42 + a + b } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(45)
end
end
end
context 'with a lambda' do
context 'with no parameters' do
let(:function) { -> { 42 } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(42)
end
end
context 'with a single parameter' do
let(:function) { ->(a) { 42 + a } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(43)
end
end
context 'with multiple parameters' do
let(:function) { ->(a, b) { 42 + a + b } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(45)
end
end
context 'with default parameters' do
let(:function) { ->(a = 0, b = 0) { 42 + a + b } }
it 'calls the proc' do
expect(FastJsonapi.call_proc(function, 1, 2)).to eq(45)
end
end
end
end
end

View File

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

View File

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

View File

@ -0,0 +1,14 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
context 'instrument' do
context 'skylight' do
# skip for normal runs because this could alter some
# other test by insterting the instrumentation
xit 'make sure requiring skylight normalizers works' do
require 'fast_jsonapi/instrumentation/skylight'
end
end
end
end

View File

@ -0,0 +1,118 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context "params option" do
let(:hash) { serializer.serializable_hash }
before(:context) do
class Movie
def viewed?(user)
user.viewed.include?(id)
end
end
class MovieSerializer
attribute :viewed do |movie, params|
params[:user] ? movie.viewed?(params[:user]) : false
end
attribute :no_param_attribute do |movie|
"no-param-attribute"
end
end
User = Struct.new(:viewed)
end
after(:context) do
Object.send(:remove_const, User) if Object.constants.include?(User)
end
context "enforces a hash only params" do
let(:params) { User.new([]) }
it "fails when creating a serializer with an object as params" do
expect(-> { MovieSerializer.new(movie, {params: User.new([])}) }).to raise_error(ArgumentError)
end
it "succeeds creating a serializer with a hash" do
expect(-> { MovieSerializer.new(movie, {params: {current_user: User.new([])}}) }).not_to raise_error
end
end
context "passing params to the serializer" do
let(:params) { {user: User.new([movie.id])} }
let(:options_with_params) { {params: params} }
context "with a single record" do
let(:serializer) { MovieSerializer.new(movie, options_with_params) }
it "handles attributes that use params" do
expect(hash[:data][:attributes][:viewed]).to eq(true)
end
it "handles attributes that don't use params" do
expect(hash[:data][:attributes][:no_param_attribute]).to eq("no-param-attribute")
end
end
context "with a list of records" do
let(:movies) { build_movies(3) }
let(:user) { User.new(movies.map { |m| [true, false].sample ? m.id : nil }.compact) }
let(:params) { {user: user} }
let(:serializer) { MovieSerializer.new(movies, options_with_params) }
it "has 3 items" do
hash[:data].length == 3
end
it "handles passing params to a list of resources" do
param_attribute_values = hash[:data].map { |data| [data[:id], data[:attributes][:viewed]] }
expected_values = movies.map { |m| [m.id.to_s, user.viewed.include?(m.id)] }
expect(param_attribute_values).to eq(expected_values)
end
it "handles attributes without params" do
no_param_attribute_values = hash[:data].map { |data| data[:attributes][:no_param_attribute] }
expected_values = (1..3).map { "no-param-attribute" }
expect(no_param_attribute_values).to eq(expected_values)
end
end
end
context "without passing params to the serializer" do
context "with a single movie" do
let(:serializer) { MovieSerializer.new(movie) }
it "handles param attributes" do
expect(hash[:data][:attributes][:viewed]).to eq(false)
end
it "handles attributes that don't use params" do
expect(hash[:data][:attributes][:no_param_attribute]).to eq("no-param-attribute")
end
end
context "with multiple movies" do
let(:serializer) { MovieSerializer.new(build_movies(3)) }
it "handles attributes with params" do
param_attribute_values = hash[:data].map { |data| data[:attributes][:viewed] }
expect(param_attribute_values).to eq([false, false, false])
end
it "handles attributes that don't use params" do
no_param_attribute_values = hash[:data].map { |data| data[:attributes][:no_param_attribute] }
expected_attribute_values = (1..3).map { "no-param-attribute" }
expect(no_param_attribute_values).to eq(expected_attribute_values)
end
end
end
end
end

View File

@ -0,0 +1,107 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when caching has_many' do
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:links] = { self: 'self' }
options[:include] = [:actors]
movies = build_movies(2)
serializable_hash = CachingMovieSerializer.new(movies, options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:links]).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[:links]).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
# FIXME: remove this if block once deprecated cache_options are not supported anymore
context 'when using deprecated cache options' do
let(:deprecated_caching_movie_serializer_class) do
rails = OpenStruct.new
rails.cache = ActiveSupport::Cache::MemoryStore.new
stub_const('Rails', rails)
Class.new do
def self.name
'DeprecatedCachingMovieSerializer'
end
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
end
it 'uses cached values for the record' do
previous_name = movie.name
previous_actors = movie.actors
deprecated_caching_movie_serializer_class.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 = deprecated_caching_movie_serializer_class.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
end
end

View File

@ -0,0 +1,690 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
describe '#has_many' do
subject(:relationship) { serializer.relationships_to_serialize[:roles] }
before do
serializer.has_many *children
end
after do
serializer.relationships_to_serialize = {}
end
context 'with namespace' do
before do
class AppName::V1::RoleSerializer
include FastJsonapi::ObjectSerializer
end
end
let(:serializer) { AppName::V1::MovieSerializer }
let(:children) { [:roles] }
let(:relationship_serializer) { AppName::V1::RoleSerializer }
context 'with overrides' do
let(:children) { [:roles, id_method_name: :roles_only_ids, record_type: :super_role] }
it_behaves_like 'returning correct relationship hash', :roles_only_ids, :super_role
end
context 'without overrides' do
let(:children) { [:roles] }
it_behaves_like 'returning correct relationship hash', :role_ids, :role
end
end
context 'without namespace' do
before do
class RoleSerializer
include FastJsonapi::ObjectSerializer
end
end
let(:serializer) { MovieSerializer }
let(:relationship_serializer) { RoleSerializer }
context 'with overrides' do
let(:children) { [:roles, id_method_name: :roles_only_ids, record_type: :super_role] }
it_behaves_like 'returning correct relationship hash', :roles_only_ids, :super_role
end
context 'without overrides' do
let(:children) { [:roles] }
it_behaves_like 'returning correct relationship hash', :role_ids, :role
end
end
end
describe '#has_many with block' do
before do
MovieSerializer.has_many :awards do |movie|
movie.actors.map(&:awards).flatten
end
end
after do
MovieSerializer.relationships_to_serialize.delete(:awards)
end
context 'awards is not included' do
subject(:hash) { MovieSerializer.new(movie).serializable_hash }
it 'returns correct hash' do
expect(hash[:data][:relationships][:awards][:data].length).to eq(6)
expect(hash[:data][:relationships][:awards][:data][0]).to eq({ id: '9', type: :award })
expect(hash[:data][:relationships][:awards][:data][-1]).to eq({ id: '28', type: :award })
end
end
context 'state is included' do
subject(:hash) { MovieSerializer.new(movie, include: [:awards]).serializable_hash }
it 'returns correct hash' do
expect(hash[:included].length).to eq 6
expect(hash[:included][0][:id]).to eq '9'
expect(hash[:included][0][:type]).to eq :award
expect(hash[:included][0][:attributes]).to eq({ id: 9, title: 'Test Award 9' })
expect(hash[:included][0][:relationships]).to eq({ actor: { data: { id: '1', type: :actor } } })
expect(hash[:included][-1][:id]).to eq '28'
expect(hash[:included][-1][:type]).to eq :award
expect(hash[:included][-1][:attributes]).to eq({ id: 28, title: 'Test Award 28' })
expect(hash[:included][-1][:relationships]).to eq({ actor: { data: { id: '3', type: :actor } } })
end
end
end
describe '#has_many with block and id_method_name' do
before do
MovieSerializer.has_many(:awards, id_method_name: :imdb_award_id) do |movie|
movie.actors.map(&:awards).flatten
end
end
after do
MovieSerializer.relationships_to_serialize.delete(:awards)
end
context 'awards is not included' do
subject(:hash) { MovieSerializer.new(movie).serializable_hash }
it 'returns correct hash where id is obtained from the method specified via `id_method_name`' do
expected_award_data = movie.actors.map(&:awards).flatten.map do |actor|
{ id: actor.imdb_award_id.to_s, type: actor.class.name.downcase.to_sym }
end
serialized_award_data = hash[:data][:relationships][:awards][:data]
expect(serialized_award_data).to eq(expected_award_data)
end
end
end
describe '#has_many with &:proc' do
before do
MovieSerializer.has_many :stars, &:actors
end
after do
MovieSerializer.relationships_to_serialize.delete(:stars)
end
subject(:hash) { MovieSerializer.new(movie).serializable_hash }
it 'returns correct hash' do
expect(hash[:data][:relationships][:stars][:data].length).to eq(3)
expect(hash[:data][:relationships][:stars][:data][0]).to eq({ id: '1', type: :actor })
expect(hash[:data][:relationships][:stars][:data][1]).to eq({ id: '2', type: :actor })
expect(hash[:data][:relationships][:stars][:data][2]).to eq({ id: '3', type: :actor })
end
end
describe '#belongs_to' do
subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] }
before do
MovieSerializer.belongs_to *parent
end
after do
MovieSerializer.relationships_to_serialize = {}
end
context 'with overrides' do
before do
class MyAreaSerializer
include FastJsonapi::ObjectSerializer
end
end
let(:parent) { [:area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area] }
let(:relationship_serializer) { MyAreaSerializer }
it_behaves_like 'returning correct relationship hash', :blah_id, :awesome_area
end
context 'without overrides' do
before do
class AreaSerializer
include FastJsonapi::ObjectSerializer
end
end
let(:parent) { [:area] }
let(:relationship_serializer) { AreaSerializer }
it_behaves_like 'returning correct relationship hash', :area_id, :area
end
end
describe '#belongs_to with block' do
before do
ActorSerializer.belongs_to :state do |actor|
actor.agency.state
end
end
after do
ActorSerializer.relationships_to_serialize.delete(:actorc)
end
context 'state is not included' do
subject(:hash) { ActorSerializer.new(actor).serializable_hash }
it 'returns correct hash' do
expect(hash[:data][:relationships][:state][:data]).to eq({ id: '1', type: :state })
end
end
context 'state is included' do
subject(:hash) { ActorSerializer.new(actor, include: [:state]).serializable_hash }
it 'returns correct hash' do
expect(hash[:included].length).to eq 1
expect(hash[:included][0][:id]).to eq '1'
expect(hash[:included][0][:type]).to eq :state
expect(hash[:included][0][:attributes]).to eq({ id: 1, name: 'Test State 1' })
expect(hash[:included][0][:relationships]).to eq({ agency: { data: [{ id: '432', type: :agency }] } })
end
end
end
describe '#belongs_to with &:proc' do
before do
MovieSerializer.belongs_to :user, &:owner
end
after do
MovieSerializer.relationships_to_serialize.delete(:user)
end
subject(:hash) { MovieSerializer.new(movie).serializable_hash }
it 'returns correct hash' do
expect(hash[:data][:relationships][:user][:data]).to eq({ id: '3', type: :owner })
end
end
describe '#has_one' do
subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] }
before do
MovieSerializer.has_one *partner
end
after do
MovieSerializer.relationships_to_serialize = {}
end
context 'with overrides' do
before do
class MyAreaSerializer
include FastJsonapi::ObjectSerializer
end
end
let(:partner) { [:area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area] }
let(:relationship_serializer) { MyAreaSerializer }
it_behaves_like 'returning correct relationship hash', :blah_id, :awesome_area
end
context 'without overrides' do
before do
class AreaSerializer
include FastJsonapi::ObjectSerializer
end
end
let(:partner) { [:area] }
let(:relationship_serializer) { AreaSerializer }
it_behaves_like 'returning correct relationship hash', :area_id, :area
end
end
describe '#has_one with &:proc' do
before do
MovieSerializer.has_one :user, &:owner
end
after do
MovieSerializer.relationships_to_serialize.delete(:user)
end
subject(:hash) { MovieSerializer.new(movie).serializable_hash }
it 'returns correct hash' do
expect(hash[:data][:relationships][:user][:data]).to eq({ id: '3', type: :owner })
end
end
describe '#set_id' do
let(:params) { {} }
subject(:serializable_hash) do
MovieSerializer.new(resource, { params: params }).serializable_hash
end
context 'method name' do
before do
MovieSerializer.set_id :owner_id
end
after do
MovieSerializer.set_id nil
end
context 'when one record is given' do
let(:resource) { movie }
it 'returns correct hash which id equals owner_id' do
expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id
end
end
context 'when an array of records is given' do
let(:resource) { build_movies(2) }
it 'returns correct hash which id equals owner_id' do
expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id
expect(serializable_hash[:data][1][:id].to_i).to eq movie.owner_id
end
end
end
context 'with block' do
let(:params) { { prefix: 'movie' } }
before do
MovieSerializer.set_id do |record, params|
"#{params[:prefix]}-#{record.owner_id}"
end
end
after do
MovieSerializer.set_id nil
end
context 'when one record is given' do
let(:resource) { movie }
it 'returns correct hash which id equals movie-id' do
expect(serializable_hash[:data][:id]).to eq "movie-#{movie.owner_id}"
end
end
context 'when an array of records is given' do
let(:resource) { build_movies(2) }
it 'returns correct hash which id equals movie-id' do
expect(serializable_hash[:data][0][:id]).to eq "movie-#{movie.owner_id}"
expect(serializable_hash[:data][1][:id]).to eq "movie-#{movie.owner_id}"
end
end
end
context 'with a lambda' do
let(:params) { { prefix: 'movie' } }
before do
MovieSerializer.set_id ->(record) { "#{params[:prefix]}-#{record.owner_id}" }
end
after do
MovieSerializer.set_id nil
end
let(:resource) { movie }
it 'returns correct hash which id equals movie-id' do
expect(serializable_hash[:data][:id]).to eq "movie-#{movie.owner_id}"
end
end
end
describe '#use_hyphen' do
subject { MovieSerializer.use_hyphen }
after do
MovieSerializer.transform_method = nil
end
it 'sets the correct transform_method when use_hyphen is used' do
warning_message = "DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead\n"
expect { subject }.to output(warning_message).to_stderr
expect(MovieSerializer.instance_variable_get(:@transform_method)).to eq :dasherize
end
end
describe '#attribute' do
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }
context 'with block' do
before do
movie.release_year = 2008
MovieSerializer.attribute :title_with_year do |record|
"#{record.name} (#{record.release_year})"
end
end
after do
MovieSerializer.attributes_to_serialize.delete(:title_with_year)
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:attributes][:name]).to eq movie.name
expect(serializable_hash[:data][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})"
end
end
context 'with &:proc' do
before do
movie.release_year = 2008
MovieSerializer.attribute :released_in_year, &:release_year
MovieSerializer.attribute :name, &:local_name
end
after do
MovieSerializer.attributes_to_serialize.delete(:released_in_year)
MovieSerializer.attributes_to_serialize.delete(:name)
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:attributes][:name]).to eq "english #{movie.name}"
expect(serializable_hash[:data][:attributes][:released_in_year]).to eq movie.release_year
end
end
context 'with lambda' do
before do
movie.release_year = 2008
MovieSerializer.attribute :released_in_year, &:release_year
MovieSerializer.attribute :name, ->(object) { object.local_name }
end
after do
MovieSerializer.attributes_to_serialize.delete(:released_in_year)
MovieSerializer.attributes_to_serialize.delete(:name)
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:attributes][:name]).to eq "english #{movie.name}"
expect(serializable_hash[:data][:attributes][:released_in_year]).to eq movie.release_year
end
end
end
describe '#meta' do
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }
context 'with block' do
before do
movie.release_year = 2008
MovieSerializer.meta do |movie|
{
years_since_release: year_since_release_calculator(movie.release_year)
}
end
end
after do
movie.release_year = nil
MovieSerializer.meta_to_serialize = nil
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:meta]).to eq ({ years_since_release: year_since_release_calculator(movie.release_year) })
end
end
context 'with lambda' do
before do
movie.release_year = 2008
MovieSerializer.meta ->(movie) { { years_since_release: year_since_release_calculator(movie.release_year) } }
end
after do
movie.release_year = nil
MovieSerializer.meta_to_serialize = nil
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:meta]).to eq ({ years_since_release: year_since_release_calculator(movie.release_year) })
end
end
private
def year_since_release_calculator(release_year)
Date.current.year - release_year
end
end
describe '#link' do
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }
after do
MovieSerializer.data_links = {}
ActorSerializer.data_links = {}
end
context 'with block calling instance method on serializer' do
before do
MovieSerializer.link(:self) do |movie_object|
movie_object.url
end
end
let(:url) { "http://movies.com/#{movie.id}" }
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:links][:self]).to eq url
end
end
context 'with block and param' do
before do
MovieSerializer.link(:public_url) do |movie_object|
"http://movies.com/#{movie_object.id}"
end
end
let(:url) { "http://movies.com/#{movie.id}" }
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:links][:public_url]).to eq url
end
end
context 'with method' do
before do
MovieSerializer.link(:object_id, :id)
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:links][:object_id]).to eq movie.id
end
end
context 'with method and convention' do
before do
MovieSerializer.link(:url)
end
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:data][:links][:url]).to eq movie.url
end
end
context 'when inheriting from a parent serializer' do
before do
MovieSerializer.link(:url) do |movie_object|
"http://movies.com/#{movie_object.id}"
end
end
subject(:action_serializable_hash) { ActionMovieSerializer.new(movie).serializable_hash }
subject(:horror_serializable_hash) { HorrorMovieSerializer.new(movie).serializable_hash }
let(:url) { "http://movies.com/#{movie.id}" }
it 'returns the link for the correct sub-class' do
expect(action_serializable_hash[:data][:links][:url]).to eq "/action-movie/#{movie.id}"
end
end
describe 'optional links' do
subject(:downloadable_serializable_hash) { OptionalDownloadableMovieSerializer.new(movie, params).serializable_hash }
context 'when the link is provided' do
let(:params) { { params: { signed_url: signed_url } } }
let(:signed_url) { 'http://example.com/download_link?signature=abcdef' }
it 'includes the link' do
expect(downloadable_serializable_hash[:data][:links][:download]).to eq signed_url
end
end
context 'when the link is not provided' do
let(:params) { { params: {} } }
it 'does not include the link' do
expect(downloadable_serializable_hash[:data][:links]).to_not have_key(:download)
end
end
end
describe 'optional links with a lambda' do
subject(:downloadable_serializable_hash) { OptionalDownloadableMovieWithLambdaSerializer.new(movie).serializable_hash }
context 'when the link should be provided' do
before { movie.release_year = 2001 }
it 'includes the link' do
expect(downloadable_serializable_hash[:data][:links][:download]).to eq '/download/232'
end
end
context 'when the link should not be provided' do
before { movie.release_year = 1970 }
it 'does not include the link' do
expect(downloadable_serializable_hash[:data][:links]).to_not have_key(:download)
end
end
end
end
describe '#key_transform' do
subject(:hash) { movie_serializer_class.new(build_movies(2), include: [:movie_type]).serializable_hash }
let(:movie_serializer_class) { "#{key_transform}_movie_serializer".classify.constantize }
before(:context) do
[:dash, :camel, :camel_lower, :underscore].each do |key_transform|
movie_serializer_name = "#{key_transform}_movie_serializer".classify
movie_type_serializer_name = "#{key_transform}_movie_type_serializer".classify
# https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
movie_serializer_class = Object.const_set(movie_serializer_name, Class.new)
# https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/67-instance-eval
movie_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_type :movie
set_key_transform key_transform
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type, serializer: "#{key_transform}_movie_type".to_sym
end
movie_type_serializer_class = Object.const_set(movie_type_serializer_name, Class.new)
movie_type_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_key_transform key_transform
attributes :name
end
end
end
context 'when key_transform is dash' do
let(:key_transform) { :dash }
it_behaves_like 'returning key transformed hash', :'movie-type', :'dash-movie-type', :'release-year'
end
context 'when key_transform is camel' do
let(:key_transform) { :camel }
it_behaves_like 'returning key transformed hash', :MovieType, :CamelMovieType, :ReleaseYear
end
context 'when key_transform is camel_lower' do
let(:key_transform) { :camel_lower }
it_behaves_like 'returning key transformed hash', :movieType, :camelLowerMovieType, :releaseYear
end
context 'when key_transform is underscore' do
let(:key_transform) { :underscore }
it_behaves_like 'returning key transformed hash', :movie_type, :underscore_movie_type, :release_year
end
end
describe '#set_key_transform after #set_type' do
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }
before do
MovieSerializer.set_type type_name
MovieSerializer.set_key_transform :camel
end
after do
MovieSerializer.transform_method = nil
MovieSerializer.set_type :movie
end
context 'when sets singular type name' do
let(:type_name) { :film }
it 'returns correct hash which type equals transformed set_type value' do
expect(serializable_hash[:data][:type]).to eq :Film
end
end
context 'when sets plural type name' do
let(:type_name) { :films }
it 'returns correct hash which type equals transformed set_type value' do
expect(serializable_hash[:data][:type]).to eq :Films
end
end
end
end

View File

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

View File

@ -0,0 +1,189 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
after(:all) do
classes_to_remove = %i[
User
UserSerializer
Country
CountrySerializer
Employee
EmployeeSerializer
Photo
PhotoSerializer
EmployeeAccount
]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
class User
attr_accessor :id, :first_name, :last_name, :uuid
attr_accessor :address_ids, :country_id
def photo
p = Photo.new
p.id = 1
p.user_id = id
p
end
def photo_id
1
end
end
class UserSerializer
include FastJsonapi::ObjectSerializer
set_type :user
set_id :uuid
attributes :first_name, :last_name
attribute :full_name do |user, params|
"#{user.first_name} #{user.last_name}"
end
has_many :addresses, cached: true
belongs_to :country
has_one :photo
end
class Address
attr_accessor :street, :city, :state, :postal_code
end
class AddressSerializer
include FastJsonapi::ObjectSerializer
attributes :street, :city, :state, :postal_code
end
class Photo
attr_accessor :id, :user_id
end
class PhotoSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
end
class Country
attr_accessor :id, :name
end
class CountrySerializer
include FastJsonapi::ObjectSerializer
attributes :name
end
class EmployeeAccount
attr_accessor :id, :employee_id
end
class EmployeeAccountSerializer
include FastJsonapi::ObjectSerializer
belongs_to :employee
end
class Employee < User
attr_accessor :id, :location, :compensation
def account
a = EmployeeAccount.new
a.id = 1
a.employee_id = id
a
end
def account_id
1
end
end
class EmployeeSerializer < UserSerializer
include FastJsonapi::ObjectSerializer
attributes :location
attributes :compensation
has_one :account, serializer: EmployeeAccountSerializer
end
it 'sets the correct record type' do
expect(EmployeeSerializer.reflected_record_type).to eq :employee
expect(EmployeeSerializer.record_type).to eq :employee
end
context 'when testing inheritance of attributes' do
it 'includes parent attributes' do
subclass_attributes = EmployeeSerializer.attributes_to_serialize
superclass_attributes = UserSerializer.attributes_to_serialize
expect(subclass_attributes).to include(superclass_attributes)
end
it 'returns inherited attribute with a block correctly' do
e = Employee.new
e.id = 1
e.first_name = 'S'
e.last_name = 'K'
attributes_hash = EmployeeSerializer.new(e).serializable_hash[:data][:attributes]
expect(attributes_hash).to include(full_name: 'S K')
end
it 'includes child attributes' do
expect(EmployeeSerializer.attributes_to_serialize[:location].method).to eq(:location)
end
it 'doesnt change parent class attributes' do
EmployeeSerializer
expect(UserSerializer.attributes_to_serialize).not_to have_key(:location)
end
it 'inherits the id source' do
e = Employee.new
e.id = 2
e.uuid = SecureRandom.uuid
id = EmployeeSerializer.new(e).serializable_hash[:data][:id]
expect(id).to eq(e.uuid)
end
end
context 'when testing inheritance of relationship' do
it 'includes parent relationships' do
subclass_relationships = EmployeeSerializer.relationships_to_serialize
superclass_relationships = UserSerializer.relationships_to_serialize
expect(subclass_relationships).to include(superclass_relationships)
end
it 'returns inherited relationship correctly' do
e = Employee.new
e.country_id = 1
relationships_hash = EmployeeSerializer.new(e).serializable_hash[:data][:relationships][:country]
expect(relationships_hash).to include(data: { id: "1", type: :country })
end
it 'includes child relationships' do
expect(EmployeeSerializer.relationships_to_serialize.keys).to include(:account)
end
it 'doesnt change parent class attributes' do
EmployeeSerializer
expect(UserSerializer.relationships_to_serialize.keys).not_to include(:account)
end
it 'includes parent cached relationships' do
subclass_relationships = EmployeeSerializer.cachable_relationships_to_serialize
superclass_relationships = UserSerializer.cachable_relationships_to_serialize
expect(subclass_relationships).to include(superclass_relationships)
end
end
context 'when test inheritence of other attributes' do
it 'inherits the tranform method' do
EmployeeSerializer
expect(UserSerializer.transform_method).to eq EmployeeSerializer.transform_method
end
end
end

View File

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

View File

@ -0,0 +1,99 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
class List
attr_accessor :id, :name, :items
end
class ChecklistItem
attr_accessor :id, :name
end
class Car
attr_accessor :id, :model, :year
end
class Animal
attr_accessor :id, :uuid, :species
end
class ChecklistItemSerializer
include FastJsonapi::ObjectSerializer
set_type :checklist_item
attributes :name
set_key_transform :dash
end
class CarSerializer
include FastJsonapi::ObjectSerializer
set_type :car
attributes :model, :year
set_key_transform :dash
end
class AnimalSerializer
include FastJsonapi::ObjectSerializer
set_type :checklist_item
attributes :uuid, :species
end
class ListSerializer
include FastJsonapi::ObjectSerializer
set_type :list
attributes :name
set_key_transform :dash
has_many :items, polymorphic: true
end
class ZooSerializer
include FastJsonapi::ObjectSerializer
set_type :list
attributes :name
has_many :items, polymorphic: true, id_method_name: :uuid
end
let(:car) do
car = Car.new
car.id = 1
car.model = 'Toyota Corolla'
car.year = 1987
car
end
let(:checklist_item) do
checklist_item = ChecklistItem.new
checklist_item.id = 2
checklist_item.name = 'Do this action!'
checklist_item
end
let(:animal) do
animal = Animal.new
animal.id = 1
animal.species = 'Mellivora capensis'
animal.uuid = SecureRandom.uuid
animal
end
context 'when serializing id and type of polymorphic relationships' do
it 'should return correct type when transform_method is specified' do
list = List.new
list.id = 1
list.items = [checklist_item, car]
list_hash = ListSerializer.new(list).to_hash
record_type = list_hash[:data][:relationships][:items][:data][0][:type]
expect(record_type).to eq 'checklist-item'.to_sym
record_type = list_hash[:data][:relationships][:items][:data][1][:type]
expect(record_type).to eq 'car'.to_sym
end
it 'should use the correct id method on associated objects' do
list = List.new
list.id = 1
list.items = [animal]
list_hash = ZooSerializer.new(list).to_hash
id = list_hash[:data][:relationships][:items][:data][0][:id]
expect(id).to eq animal.uuid
end
end
end

View File

@ -0,0 +1,112 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context "params option" do
let(:hash) { serializer.serializable_hash }
context "generating links for a serializer relationship" do
let(:params) { { } }
let(:options_with_params) { { params: params } }
let(:relationship_url) { "http://movies.com/#{movie.id}/relationships/actors" }
let(:related_url) { "http://movies.com/movies/#{movie.name.parameterize}/actors/" }
before(:context) do
class MovieSerializer
has_many :actors, lazy_load_data: false, links: {
self: :actors_relationship_url,
related: -> (object, params = {}) {
"#{params.has_key?(:secure) ? "https" : "http"}://movies.com/movies/#{object.name.parameterize}/actors/"
}
}
end
end
context "with a single record" do
let(:serializer) { MovieSerializer.new(movie, options_with_params) }
let(:links) { hash[:data][:relationships][:actors][:links] }
it "handles relationship links that call a method" do
expect(links).to be_present
expect(links[:self]).to eq(relationship_url)
end
it "handles relationship links that call a proc" do
expect(links).to be_present
expect(links[:related]).to eq(related_url)
end
context "with serializer params" do
let(:params) { { secure: true } }
let(:secure_related_url) { related_url.gsub("http", "https") }
it "passes the params to the link serializer correctly" do
expect(links).to be_present
expect(links[:related]).to eq(secure_related_url)
end
end
end
end
context "lazy loading relationship data" do
before(:context) do
class LazyLoadingMovieSerializer < MovieSerializer
has_many :actors, lazy_load_data: true, links: {
related: :actors_relationship_url
}
end
end
let(:serializer) { LazyLoadingMovieSerializer.new(movie) }
let(:actor_hash) { hash[:data][:relationships][:actors] }
it "does not include the :data key" do
expect(actor_hash).to be_present
expect(actor_hash).not_to have_key(:data)
end
end
context "including lazy loaded relationships" do
before(:context) do
class LazyLoadingMovieSerializer < MovieSerializer
has_many :actors, lazy_load_data: true, links: {
related: :actors_relationship_url
}
end
end
let(:serializer) { LazyLoadingMovieSerializer.new(movie, include: [:actors]) }
let(:actor_hash) { hash[:data][:relationships][:actors] }
it "includes the :data key" do
expect(actor_hash).to be_present
expect(actor_hash).to have_key(:data)
end
end
context "relationship links defined by a method on the object" do
before(:context) do
class Movie
def relationship_links
{ self: "http://movies.com/#{id}/relationships/actors" }
end
end
class LinksPassingMovieSerializer < MovieSerializer
has_many :actors, links: :relationship_links
end
end
let(:serializer) { LinksPassingMovieSerializer.new(movie) }
let(:links) { hash[:data][:relationships][:actors][:links] }
let(:relationship_url) { "http://movies.com/#{movie.id}/relationships/actors" }
it "generates relationship links in the object" do
expect(links).to be_present
expect(links[:self]).to eq(relationship_url)
end
end
end
end

View File

@ -0,0 +1,63 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context "params option" do
let(:hash) { serializer.serializable_hash }
before(:context) do
class MovieSerializer
has_many :agencies do |movie, params|
movie.actors.map(&:agency) if params[:authorized]
end
belongs_to :primary_agency do |movie, params|
movie.actors.map(&:agency)[0] if params[:authorized]
end
belongs_to :secondary_agency do |movie|
movie.actors.map(&:agency)[1]
end
end
end
context "passing params to the serializer" do
let(:params) { {authorized: true} }
let(:options_with_params) { {params: params} }
context "with a single record" do
let(:serializer) { MovieSerializer.new(movie, options_with_params) }
it "handles relationships that use params" do
ids = hash[:data][:relationships][:agencies][:data].map{|a| a[:id]}
ids.map!(&:to_i)
expect(ids).to eq [0,1,2]
end
it "handles relationships that don't use params" do
expect(hash[:data][:relationships][:secondary_agency][:data]).to include({id: 1.to_s})
end
end
context "with a list of records" do
let(:movies) { build_movies(3) }
let(:params) { {authorized: true} }
let(:serializer) { MovieSerializer.new(movies, options_with_params) }
it "handles relationship params when passing params to a list of resources" do
relationships_hashes = hash[:data].map{|a| a[:relationships][:agencies][:data]}.uniq.flatten
expect(relationships_hashes.map{|a| a[:id].to_i}).to contain_exactly 0,1,2
uniq_count = hash[:data].map{|a| a[:relationships][:primary_agency] }.uniq.count
expect(uniq_count).to eq 1
end
it "handles relationships without params" do
uniq_count = hash[:data].map{|a| a[:relationships][:secondary_agency] }.uniq.count
expect(uniq_count).to eq 1
end
end
end
end
end

View File

@ -0,0 +1,107 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
class Person
attr_accessor :id, :name, :assets
end
class House
attr_accessor :id, :address
end
class Car
attr_accessor :id, :model, :year
end
class PersonSerializer
include FastJsonapi::ObjectSerializer
set_type :person
attributes :name
set_key_transform :dash
has_many :assets, serializer: -> (object) do
if object.is_a?(House)
HouseSerializer
elsif object.is_a?(Car)
CarSerializer
end
end
end
class HouseSerializer
include FastJsonapi::ObjectSerializer
set_type :house
attributes :address
set_key_transform :dash
end
class CarSerializer
include FastJsonapi::ObjectSerializer
set_type :car
attributes :model, :year
set_key_transform :dash
end
let(:house) do
house = House.new
house.id = 123
house.address = '1600 Pennsylvania Avenue'
house
end
let(:car) do
car = Car.new
car.id = 456
car.model = 'Toyota Corolla'
car.year = 1987
car
end
context 'when serializing a relationship with a serializer block' do
it 'should output the correct JSON based on the proper serializer' do
person = Person.new
person.id = 1
person.name = 'Bob'
person.assets = [house, car]
person_hash = PersonSerializer.new(person).to_hash
relationships = person_hash[:data][:relationships]
house_relationship = relationships[:assets][:data][0]
expect(house_relationship[:type].to_s).to eq 'house'
expect(house_relationship[:id].to_s).to eq house.id.to_s
car_relationship = relationships[:assets][:data][1]
expect(car_relationship[:type].to_s).to eq 'car'
expect(car_relationship[:id].to_s).to eq car.id.to_s
expect(person_hash[:data]).to_not have_key :included
end
it 'should output the correct included records' do
person = Person.new
person.id = 1
person.name = 'Bob'
person.assets = [house, car]
person_hash = PersonSerializer.new(person, { include: [ :assets ] }).to_hash
relationships = person_hash[:data][:relationships]
house_relationship = relationships[:assets][:data][0]
expect(house_relationship[:type].to_s).to eq 'house'
expect(house_relationship[:id].to_s).to eq house.id.to_s
car_relationship = relationships[:assets][:data][1]
expect(car_relationship[:type].to_s).to eq 'car'
expect(car_relationship[:id].to_s).to eq car.id.to_s
included = person_hash[:included]
house_included = included[0]
expect(house_included[:type].to_s).to eq 'house'
expect(house_included[:id].to_s).to eq house.id.to_s
expect(house_included[:attributes][:address]).to eq house.address
car_included = included[1]
expect(car_included[:type].to_s).to eq 'car'
expect(car_included[:id].to_s).to eq car.id.to_s
expect(car_included[:attributes][:model]).to eq car.model
end
end
end

View File

@ -0,0 +1,593 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
include_context 'group class'
let(:movies) { build_movies(2) }
context 'when testing instance methods of object serializer' do
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:links] = { self: 'self' }
options[:include] = [:actors]
serializable_hash = MovieSerializer.new(movies, options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 4
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:links]).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[:links]).to be nil
expect(serializable_hash[:included]).to be nil
end
it 'returns correct nested includes when serializable_hash is called' do
# 3 actors, 3 agencies
include_object_total = 6
options = {}
options[:include] = [:actors, :'actors.agency']
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included].length).to eq include_object_total
(0..include_object_total-1).each do |include|
expect(serializable_hash[:included][include]).to be_instance_of(Hash)
end
options[:include] = [:'actors.agency']
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included].length).to eq include_object_total
(0..include_object_total-1).each do |include|
expect(serializable_hash[:included][include]).to be_instance_of(Hash)
end
end
it 'returns correct number of records when serialized_json is called for an array' do
options = {}
options[:meta] = { total: 2 }
json = MovieSerializer.new(movies, options).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data'].length).to eq 2
expect(serializable_hash['meta']).to be_instance_of(Hash)
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 belongs_to returns nil and there is a block for the relationship' do
movie.owner_id = nil
json = MovieSerializer.new(movie, {include: [:owner]}).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['owner']['data']).to be nil
end
it 'returns correct json when has_one returns nil' do
supplier.account_id = nil
json = SupplierSerializer.new(supplier).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['account']['data']).to be nil
end
it 'returns correct json when serializing []' do
json = MovieSerializer.new([]).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']).to eq []
end
describe '#as_json' do
it 'returns a json hash' do
json_hash = MovieSerializer.new(movie).as_json
expect(json_hash['data']['id']).to eq movie.id.to_s
end
it 'returns multiple records' do
json_hash = MovieSerializer.new(movies).as_json
expect(json_hash['data'].length).to eq 2
end
it 'removes non-relevant attributes' do
movie.director = 'steven spielberg'
json_hash = MovieSerializer.new(movie).as_json
expect(json_hash['data']['director']).to eq(nil)
end
end
it 'returns errors when serializing with non-existent includes key' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:blah_blah]
expect { MovieSerializer.new(movies, options).serializable_hash }.to raise_error(ArgumentError)
end
it 'returns errors when serializing with non-existent and existent includes keys' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors, :blah_blah]
expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError)
end
it 'does not throw an error with non-empty string array includes key' do
options = {}
options[:include] = ['actors']
expect { MovieSerializer.new(movie, options) }.not_to raise_error
end
it 'does not throw an error with non-empty string array includes keys' do
options = {}
options[:include] = ['actors', 'owner']
expect { MovieSerializer.new(movie, options) }.not_to raise_error
end
it 'returns keys when serializing with empty string/nil array includes key' do
options = {}
options[:meta] = { total: 2 }
options[:include] = ['']
expect(MovieSerializer.new(movies, options).serializable_hash.keys).to eq [:data, :meta]
options[:include] = [nil]
expect(MovieSerializer.new(movies, options).serializable_hash.keys).to eq [:data, :meta]
end
end
context 'id attribute is the same for actors and not a primary key' do
before do
ActorSerializer.set_id :email
movie.actor_ids = [0, 0, 0]
class << movie
def actors
super.each_with_index { |actor, i| actor.email = "actor#{i}@email.com" }
end
end
end
after { ActorSerializer.set_id nil }
let(:options) { { include: ['actors'] } }
subject { MovieSerializer.new(movie, options).serializable_hash }
it 'returns all actors in includes' do
expect(
subject[:included].select { |i| i[:type] == :actor }.map { |i| i[:id] }
).to eq(
movie.actors.map(&:email)
)
end
end
context 'nested includes' do
it 'has_many to belongs_to: returns correct nested includes when serializable_hash is called' do
# 3 actors, 3 agencies
include_object_total = 6
options = {}
options[:include] = [:actors, :'actors.agency']
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included].length).to eq include_object_total
(0..include_object_total-1).each do |include|
expect(serializable_hash[:included][include]).to be_instance_of(Hash)
end
options[:include] = [:'actors.agency']
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included].length).to eq include_object_total
(0..include_object_total-1).each do |include|
expect(serializable_hash[:included][include]).to be_instance_of(Hash)
end
end
it '`has_many` to `belongs_to` to `belongs_to` - returns correct nested includes when serializable_hash is called' do
# 3 actors, 3 agencies, 1 state
include_object_total = 7
options = {}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
serializable_hash = MovieSerializer.new([movie], options).serializable_hash
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included].length).to eq include_object_total
actors_serialized = serializable_hash[:included].find_all { |included| included[:type] == :actor }.map { |included| included[:id].to_i }
agencies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :agency }.map { |included| included[:id].to_i }
states_serialized = serializable_hash[:included].find_all { |included| included[:type] == :state }.map { |included| included[:id].to_i }
movie.actors.each do |actor|
expect(actors_serialized).to include(actor.id)
end
agencies = movie.actors.map(&:agency).uniq
agencies.each do |agency|
expect(agencies_serialized).to include(agency.id)
end
states = agencies.map(&:state).uniq
states.each do |state|
expect(states_serialized).to include(state.id)
end
end
it 'has_many => has_one returns correct nested includes when serializable_hash is called' do
options = {}
options[:include] = [:movies, :'movies.advertising_campaign']
serializable_hash = MovieTypeSerializer.new([movie_type], options).serializable_hash
movies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :movie }.map { |included| included[:id].to_i }
advertising_campaigns_serialized = serializable_hash[:included].find_all { |included| included[:type] == :advertising_campaign }.map { |included| included[:id].to_i }
movies = movie_type.movies
movies.each do |movie|
expect(movies_serialized).to include(movie.id)
end
advertising_campaigns = movies.map(&:advertising_campaign)
advertising_campaigns.each do |advertising_campaign|
expect(advertising_campaigns_serialized).to include(advertising_campaign.id)
end
end
it 'belongs_to: returns correct nested includes when nested attributes are nil when serializable_hash is called' do
class Movie
def advertising_campaign
nil
end
end
options = {}
options[:include] = [:movies, :'movies.advertising_campaign']
serializable_hash = MovieTypeSerializer.new([movie_type], options).serializable_hash
movies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :movie }.map { |included| included[:id].to_i }
movies = movie_type.movies
movies.each do |movie|
expect(movies_serialized).to include(movie.id)
end
end
it 'polymorphic has_many: returns correct nested includes when serializable_hash is called' do
options = {}
options[:include] = [:groupees]
serializable_hash = GroupSerializer.new([group], options).serializable_hash
persons_serialized = serializable_hash[:included].find_all { |included| included[:type] == :person }.map { |included| included[:id].to_i }
groups_serialized = serializable_hash[:included].find_all { |included| included[:type] == :group }.map { |included| included[:id].to_i }
persons = group.groupees.find_all { |groupee| groupee.is_a?(Person) }
persons.each do |person|
expect(persons_serialized).to include(person.id)
end
groups = group.groupees.find_all { |groupee| groupee.is_a?(Group) }
groups.each do |group|
expect(groups_serialized).to include(group.id)
end
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 'should set default_type for a namespaced serializer' do
module V1
class BlahSerializer
include FastJsonapi::ObjectSerializer
end
end
expect(V1::BlahSerializer.record_type).to be :blah
end
it 'shouldnt set default_type for a serializer that doesnt follow convention' do
class BlahBlahSerializerBuilder
include FastJsonapi::ObjectSerializer
end
expect(BlahBlahSerializerBuilder.record_type).to be_nil
end
it 'shouldnt set default_type for an anonymous serializer' do
serializer_class = Class.new do
include FastJsonapi::ObjectSerializer
end
expect(serializer_class.record_type).to be_nil
end
end
context 'when serializing included, serialize any links' do
before do
ActorSerializer.link(:self) do |actor_object|
actor_object.url
end
end
subject(:serializable_hash) do
options = {}
options[:include] = [:actors]
MovieSerializer.new(movie, options).serializable_hash
end
let(:actor) { movie.actors.first }
let(:url) { "http://movies.com/actors/#{actor.id}" }
it 'returns correct hash when serializable_hash is called' do
expect(serializable_hash[:included][0][:links][:self]).to eq url
end
end
context 'when serializing included, params should be available in any serializer' do
subject(:serializable_hash) do
options = {}
options[:include] = [:"actors.awards"]
options[:params] = { include_award_year: true }
MovieSerializer.new(movie, options).serializable_hash
end
let(:actor) { movie.actors.first }
let(:award) { actor.awards.first }
let(:year) { award.year }
it 'passes params to deeply nested includes' do
expect(year).to_not be_blank
expect(serializable_hash[:included][0][:attributes][:year]).to eq year
end
end
context 'when is_collection option present' do
subject { MovieSerializer.new(resource, is_collection_options).serializable_hash }
context 'autodetect' do
let(:is_collection_options) { {} }
context 'collection if no option present' do
let(:resource) { [movie] }
it { expect(subject[:data]).to be_a(Array) }
end
context 'single if no option present' do
let(:resource) { movie }
it { expect(subject[:data]).to be_a(Hash) }
end
end
context 'force is_collection to true' do
let(:is_collection_options) { { is_collection: true } }
context 'collection will pass' do
let(:resource) { [movie] }
it { expect(subject[:data]).to be_a(Array) }
end
context 'single will raise error' do
let(:resource) { movie }
it { expect { subject }.to raise_error(NoMethodError, /method(.*)each/) }
end
end
context 'force is_collection to false' do
let(:is_collection_options) { { is_collection: false } }
context 'collection will fail without id' do
let(:resource) { [movie] }
it { expect { subject }.to raise_error(FastJsonapi::MandatoryField, /id is a mandatory field/) }
end
context 'single will pass' do
let(:resource) { movie }
it { expect(subject[:data]).to be_a(Hash) }
end
end
end
context 'when optional attributes are determined by record data' do
it 'returns optional attribute when attribute is included' do
movie.release_year = 2001
json = MovieOptionalRecordDataSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['attributes']['release_year']).to eq movie.release_year
end
it "doesn't return optional attribute when attribute is not included" do
movie.release_year = 1970
json = MovieOptionalRecordDataSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['attributes'].has_key?('release_year')).to be_falsey
end
end
context 'when optional attributes are determined by record data with a lambda' do
it 'returns optional attribute when attribute is included' do
movie.release_year = 2001
json = MovieOptionalRecordDataWithLambdaSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['attributes']['release_year']).to eq movie.release_year
end
it "doesn't return optional attribute when attribute is not included" do
movie.release_year = 1970
json = MovieOptionalRecordDataWithLambdaSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['attributes'].has_key?('release_year')).to be_falsey
end
end
context 'when optional attributes are determined by params data' do
it 'returns optional attribute when attribute is included' do
movie.director = 'steven spielberg'
json = MovieOptionalParamsDataSerializer.new(movie, { params: { admin: true }}).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['attributes']['director']).to eq 'steven spielberg'
end
it "doesn't return optional attribute when attribute is not included" do
movie.director = 'steven spielberg'
json = MovieOptionalParamsDataSerializer.new(movie, { params: { admin: false }}).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['attributes'].has_key?('director')).to be_falsey
end
end
context 'when optional relationships are determined by record data' do
it 'returns optional relationship when relationship is included' do
json = MovieOptionalRelationshipSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_truthy
end
context "when relationship is not included" do
let(:json) {
MovieOptionalRelationshipSerializer.new(movie, options).serialized_json
}
let(:options) {
{}
}
let(:serializable_hash) {
JSON.parse(json)
}
it "doesn't return optional relationship" do
movie.actor_ids = []
expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_falsey
end
it "doesn't include optional relationship" do
movie.actor_ids = []
options[:include] = [:actors]
expect(serializable_hash['included']).to be_blank
end
end
end
context 'when optional relationships are determined by record data with a lambda' do
it 'returns optional relationship when relationship is included' do
json = MovieOptionalRelationshipWithLambdaSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_truthy
end
context "when relationship is not included" do
let(:json) {
MovieOptionalRelationshipWithLambdaSerializer.new(movie, options).serialized_json
}
let(:options) {
{}
}
let(:serializable_hash) {
JSON.parse(json)
}
it "doesn't return optional relationship" do
movie.actor_ids = []
expect(serializable_hash['data']['relationships'].has_key?('actors')).to be_falsey
end
it "doesn't include optional relationship" do
movie.actor_ids = []
options[:include] = [:actors]
expect(serializable_hash['included']).to be_blank
end
end
end
context 'when include has frozen array' do
let(:options) { { include: [:actors].freeze }}
let(:json) { MovieOptionalRelationshipSerializer.new(movie, options).serialized_json }
it 'does not raise and error' do
expect(json['included']).to_not be_blank
end
end
context 'when optional relationships are determined by params data' do
it 'returns optional relationship when relationship is included' do
json = MovieOptionalRelationshipWithParamsSerializer.new(movie, { params: { admin: true }}).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships'].has_key?('owner')).to be_truthy
end
context "when relationship is not included" do
let(:json) {
MovieOptionalRelationshipWithParamsSerializer.new(movie, options).serialized_json
}
let(:options) {
{ params: { admin: false }}
}
let(:serializable_hash) {
JSON.parse(json)
}
it "doesn't return optional relationship" do
expect(serializable_hash['data']['relationships'].has_key?('owner')).to be_falsey
end
it "doesn't include optional relationship" do
options[:include] = [:owner]
expect(serializable_hash['included']).to be_blank
end
end
end
context 'when attribute contents are determined by params data' do
it 'does not throw an error with no params are passed' do
expect { MovieOptionalAttributeContentsWithParamsSerializer.new(movie).serialized_json }.not_to raise_error
end
end
end

View File

@ -0,0 +1,41 @@
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[:links] = { self: 'self' }
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 4
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:links]).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[:links]).to be nil
expect(serializable_hash[:included]).to be nil
expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s
end
context 'struct without id' do
it 'returns correct hash when serializable_hash is called' do
serializer = MovieWithoutIdStructSerializer.new(movie_struct_without_id)
expect { serializer.serializable_hash }.to raise_error(FastJsonapi::MandatoryField)
end
end
end
end

View File

@ -0,0 +1,72 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context "movie class"
include_context 'group class'
context 'when testing class methods of serialization core' do
it 'returns correct hash when id_hash is called' do
inputs = [{id: 23, record_type: :movie}, {id: 'x', record_type: 'person'}]
inputs.each do |hash|
result_hash = MovieSerializer.send(:id_hash, hash[:id], hash[:record_type])
expect(result_hash[:id]).to eq hash[:id].to_s
expect(result_hash[:type]).to eq hash[:record_type]
end
result_hash = MovieSerializer.send(:id_hash, nil, 'movie')
expect(result_hash).to be nil
end
it 'returns 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, attribute|
value = attributes_hash[key]
expect(value).to eq movie.send(attribute.method)
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, nil, nil)
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, {}, nil)
end
expect(included_records.size).to eq 3
end
end
end

View File

@ -0,0 +1,155 @@
RSpec.shared_context 'ams movie class' do
before(:context) do
# models
class AMSModel < ActiveModelSerializers::Model
derive_attributes_from_names_and_fix_accessors
end
class AMSMovieType < AMSModel
attributes :id, :name, :movies
end
class AMSMovie < AMSModel
attributes :id, :name, :release_year, :actors, :owner, :movie_type, :advertising_campaign
def movie_type
mt = AMSMovieType.new
mt.id = 1
mt.name = 'Episode'
mt.movies = [self]
mt
end
end
class AMSAdvertisingCampaign < AMSModel
attributes :id, :name, :movie
end
class AMSAward < AMSModel
attributes :id, :title, :actor
end
class AMSAgency < AMSModel
attributes :id, :name, :actors
end
class AMSActor < AMSModel
attributes :id, :name, :email, :agency, :awards, :agency_id
def agency
AMSAgency.new.tap do |a|
a.id = agency_id
a.name = "Test Agency #{agency_id}"
end
end
def award_ids
[id * 9, id * 9 + 1]
end
def awards
award_ids.map do |i|
AMSAward.new.tap do |a|
a.id = i
a.title = "Test Award #{i}"
end
end
end
end
class AMSUser < AMSModel
attributes :id, :name
end
class AMSMovieType < AMSModel
attributes :id, :name
end
# serializers
class AMSAwardSerializer < ActiveModel::Serializer
type 'award'
attributes :id, :title
belongs_to :actor
end
class AMSAgencySerializer < ActiveModel::Serializer
type 'agency'
attributes :id, :name
belongs_to :state
has_many :actors
end
class AMSActorSerializer < ActiveModel::Serializer
type 'actor'
attributes :name, :email
belongs_to :agency, serializer: ::AMSAgencySerializer
has_many :awards, serializer: ::AMSAwardSerializer
end
class AMSUserSerializer < ActiveModel::Serializer
type 'user'
attributes :name
end
class AMSMovieTypeSerializer < ActiveModel::Serializer
type 'movie_type'
attributes :name
has_many :movies
end
class AMSAdvertisingCampaignSerializer < ActiveModel::Serializer
type 'advertising_campaign'
attributes :name
end
class AMSMovieSerializer < ActiveModel::Serializer
type 'movie'
attributes :name, :release_year
has_many :actors
has_one :owner
belongs_to :movie_type
has_one :advertising_campaign
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.agency_id = i
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
let(:ams_advertising_campaign) do
campaign = AMSAdvertisingCampaign.new
campaign.id = 1
campaign.name = "Movie is incredible!!"
campaign
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.advertising_campaign = ams_advertising_campaign
m
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,511 @@
RSpec.shared_context 'movie class' do
# Movie, Actor Classes and serializers
before(:context) do
# models
class Movie
attr_accessor :id,
:name,
:release_year,
:director,
:actor_ids,
:owner_id,
:movie_type_id
def actors
actor_ids.map.with_index do |id, i|
a = Actor.new
a.id = id
a.name = "Test #{a.id}"
a.email = "test#{a.id}@test.com"
a.agency_id = i
a
end
end
def movie_type
mt = MovieType.new
mt.id = movie_type_id
mt.name = 'Episode'
mt.movie_ids = [id]
mt
end
def advertising_campaign_id
1
end
def advertising_campaign
ac = AdvertisingCampaign.new
ac.id = 1
ac.movie_id = id
ac.name = "Movie #{name} is incredible!!"
ac
end
def owner
return unless owner_id
ow = Owner.new
ow.id = owner_id
ow
end
def cache_key
"#{id}"
end
def local_name(locale = :english)
"#{locale} #{name}"
end
def url
"http://movies.com/#{id}"
end
def actors_relationship_url
"#{url}/relationships/actors"
end
end
class Actor
attr_accessor :id, :name, :email, :agency_id
def agency
Agency.new.tap do |a|
a.id = agency_id
a.name = "Test Agency #{agency_id}"
a.state_id = 1
end
end
def awards
award_ids.map do |i|
Award.new.tap do |a|
a.id = i
a.title = "Test Award #{i}"
a.actor_id = id
a.imdb_award_id = i * 10
a.year = 1990 + i
end
end
end
def award_ids
[id * 9, id * 9 + 1]
end
def url
"http://movies.com/actors/#{id}"
end
end
class AdvertisingCampaign
attr_accessor :id, :name, :movie_id
end
class Agency
attr_accessor :id, :name, :state_id
def state
State.new.tap do |s|
s.id = state_id
s.name = "Test State #{state_id}"
s.agency_ids = [id]
end
end
end
class Award
attr_accessor :id, :title, :actor_id, :year, :imdb_award_id
end
class State
attr_accessor :id, :name, :agency_ids
end
class MovieType
attr_accessor :id, :name, :movie_ids
def movies
movie_ids.map.with_index do |id, i|
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
end
end
class Agency
attr_accessor :id, :name, :actor_ids
end
class Agency
attr_accessor :id, :name, :actor_ids
end
class Supplier
attr_accessor :id, :account_id
def account
if account_id
a = Account.new
a.id = account_id
a
end
end
end
class Account
attr_accessor :id
end
class Owner
attr_accessor :id
end
class OwnerSerializer
include FastJsonapi::ObjectSerializer
end
# serializers
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
# director attr is not mentioned intentionally
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user do |object, params|
object.owner
end
belongs_to :movie_type
has_one :advertising_campaign
end
class GenreMovieSerializer < MovieSerializer
link(:something) { '/something/' }
end
class ActionMovieSerializer < GenreMovieSerializer
link(:url) { |object| "/action-movie/#{object.id}" }
end
class HorrorMovieSerializer < GenreMovieSerializer
link(:url) { |object| "/horror-movie/#{object.id}" }
end
class OptionalDownloadableMovieSerializer < MovieSerializer
link(:download, if: Proc.new { |record, params| params && params[:signed_url] }) do |movie, params|
params[:signed_url]
end
end
class OptionalDownloadableMovieWithLambdaSerializer < MovieSerializer
link(:download, if: ->(record) { record.release_year >= 2000 }) do |movie|
"/download/#{movie.id}"
end
end
class MovieWithoutIdStructSerializer
include FastJsonapi::ObjectSerializer
attributes :name, :release_year
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 store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes
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 store: ActiveSupport::Cache::MemoryStore.new, namespace: 'fast-jsonapi'
end
class ActorSerializer
include FastJsonapi::ObjectSerializer
set_type :actor
attributes :name, :email
belongs_to :agency
has_many :awards
belongs_to :agency
end
class AgencySerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
belongs_to :state
has_many :actors
end
class AwardSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :title
attribute :year, if: Proc.new { |record, params|
params[:include_award_year].present? ?
params[:include_award_year] :
false
}
belongs_to :actor
end
class StateSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
has_many :agency
end
class AdvertisingCampaignSerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
belongs_to :movie
end
class MovieTypeSerializer
include FastJsonapi::ObjectSerializer
set_type :movie_type
attributes :name
has_many :movies
end
class MovieSerializerWithAttributeBlock
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
attribute :title_with_year do |record|
"#{record.name} (#{record.release_year})"
end
end
class MovieSerializerWithAttributeBlock
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
attribute :title_with_year do |record|
"#{record.name} (#{record.release_year})"
end
end
class AgencySerializer
include FastJsonapi::ObjectSerializer
attributes :id, :name
has_many :actors
end
class SupplierSerializer
include FastJsonapi::ObjectSerializer
set_type :supplier
has_one :account
end
class AccountSerializer
include FastJsonapi::ObjectSerializer
set_type :account
belongs_to :supplier
end
class MovieOptionalRecordDataSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
attribute :release_year, if: Proc.new { |record| record.release_year >= 2000 }
end
class MovieOptionalRecordDataWithLambdaSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
attribute :release_year, if: ->(record) { record.release_year >= 2000 }
end
class MovieOptionalParamsDataSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
attribute :director, if: Proc.new { |record, params| params[:admin] == true }
end
class MovieOptionalRelationshipSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
has_many :actors, if: Proc.new { |record| record.actors.any? }
end
class MovieOptionalRelationshipWithLambdaSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
has_many :actors, if: ->(record) { record.actors.any? }
end
class MovieOptionalRelationshipWithParamsSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
belongs_to :owner, record_type: :user, if: Proc.new { |record, params| params[:admin] == true }
end
class MovieOptionalAttributeContentsWithParamsSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name
attribute :director do |record, params|
data = {}
data[:first_name] = 'steven'
data[:last_name] = 'spielberg' if params[:admin]
data
end
end
end
# Namespaced MovieSerializer
before(:context) do
# namespaced model stub
module AppName
module V1
class MovieSerializer
include FastJsonapi::ObjectSerializer
# to test if compute_serializer_name works
end
end
end
end
# Movie and Actor struct
before(:context) do
MovieStruct = Struct.new(
:id,
:name,
:release_year,
:actor_ids,
:actors,
:owner_id,
:owner,
:movie_type_id,
:advertising_campaign_id
)
ActorStruct = Struct.new(:id, :name, :email, :agency_id, :award_ids)
MovieWithoutIdStruct = Struct.new(:name, :release_year)
AgencyStruct = Struct.new(:id, :name, :actor_ids)
end
after(:context) do
classes_to_remove = %i[
ActionMovieSerializer
GenreMovieSerializer
HorrorMovieSerializer
OptionalDownloadableMovieSerializer
OptionalDownloadableMovieWithLambdaSerializer
Movie
MovieSerializer
Actor
ActorSerializer
MovieType
MovieTypeSerializer
AppName::V1::MovieSerializer
MovieStruct
ActorStruct
MovieWithoutIdStruct
HyphenMovieSerializer
MovieWithoutIdStructSerializer
Agency
AgencyStruct
AgencySerializer
AdvertisingCampaign
AdvertisingCampaignSerializer
]
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
agency = AgencyStruct
actors = []
3.times.each do |id|
actors << ActorStruct.new(id, id.to_s, id.to_s, id, [id])
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_struct_without_id) do
MovieWithoutIdStruct.new('struct without id', 2018)
end
let(:movie) do
m = Movie.new
m.id = 232
m.name = 'test movie'
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
let(:actor) do
Actor.new.tap do |a|
a.id = 234
a.name = 'test actor'
a.email = 'test@test.com'
a.agency_id = 432
end
end
let(:movie_type) do
movie
mt = MovieType.new
mt.id = movie.movie_type_id
mt.name = 'Foreign Thriller'
mt.movie_ids = [movie.id]
mt
end
let(:supplier) do
s = Supplier.new
s.id = 1
s.account_id = 1
s
end
def build_movies(count)
count.times.map do |i|
m = Movie.new
m.id = i + 1
m.name = 'test movie'
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
end
end

View File

@ -0,0 +1,18 @@
RSpec.shared_examples 'returning correct relationship hash' do |id_method_name, record_type|
it 'returns correct relationship hash' do
expect(relationship).to be_instance_of(FastJsonapi::Relationship)
# expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship.static_serializer).to be relationship_serializer
expect(relationship.id_method_name).to be id_method_name
expect(relationship.static_record_type).to be record_type
end
end
RSpec.shared_examples 'returning key transformed hash' do |relationship_name, resource_type, release_year|
it 'returns correctly transformed hash' do
expect(hash[:data][0][:attributes]).to have_key(release_year)
expect(hash[:data][0][:relationships]).to have_key(relationship_name)
expect(hash[:data][0][:relationships][relationship_name][:data][:type]).to eq(resource_type)
expect(hash[:included][0][:type]).to eq(resource_type)
end
end

View File

@ -1,30 +1,21 @@
require 'simplecov' require 'active_record'
require 'fast_jsonapi'
SimpleCov.start do require 'rspec-benchmark'
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 'byebug' require 'byebug'
require 'securerandom' require 'active_model_serializers'
require 'oj'
require 'jsonapi/serializable'
require 'jsonapi-serializers'
Dir[File.expand_path('spec/fixtures/*.rb')].sort.each { |f| require f } Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file }
Dir[File.dirname(__FILE__) + '/shared/examples/*.rb'].each {|file| require file }
RSpec.configure do |config| RSpec.configure do |config|
config.include JSONAPI::RSpec config.include RSpec::Benchmark::Matchers
config.filter_run_excluding performance: ENV['BENCHMARK'].blank?
config.mock_with :rspec
config.filter_run_when_matching :focus
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end end
Oj.optimize_rails
ActiveModel::Serializer.config.adapter = :json_api
ActiveModel::Serializer.config.key_transform = :underscore
ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new('/dev/null'))