Compare commits

...

80 Commits

Author SHA1 Message Date
Clemens Kofler
7db80f673d Clarify README wording related to contributions 2022-04-04 18:52:58 +01:00
Peter Goldstein
bcee2b597b Add Ruby 3.1 to CI
Also fixes the unquoted 3.0, which needs to be quoted to avoid truncation
Addresses a few Rubocop issues and an active_support require issue on Rails 7 w/Zeitwerk
2022-02-02 21:40:34 +00:00
Igor Victor
44cf8495ea Update ci.yml 2021-04-12 11:50:08 +01:00
Stas SUȘCOV
37235057df
Version bump. 2021-03-11 18:35:11 +00:00
Stas SUȘCOV
c21f9def8f Updated the changelog. 2021-03-11 18:33:35 +00:00
Stas SUȘCOV
8b74954478 Added ruby v3 to CI matrix. 2021-03-11 18:33:35 +00:00
Stas SUȘCOV
ef93f7f358 Make rubocop happy. 2021-03-11 18:33:35 +00:00
HubertVonHerbert
c5eb1ce27c
Fix Ruby3 compatibility issue with Procs (#160)
* Fix Ruby3 compatibility issue with Procs

* Fix rubocop complaints

* Remove ruby 3 from CI actions

* Simplify check for &:foo procs
2021-03-11 18:29:59 +00:00
Ryan Romanchuk
a25d415b4d Fix most likely copy/paste error
this lambda yielding a bool for `belongs_to` might confuse readers or conflate the :if block

which would probably look something like this 

```ruby
belongs_to :primary_agent, if: proc { |movie, params| params[:current_user].present? } do |movie, params|
    # in here, params is a hash containing the `:current_user` key
    params[:current_user]
  end
```
2021-02-14 22:17:08 +00:00
Stas SUȘCOV
c3376037e7 Let everyone know there's a WIP branch for v3. 2021-01-11 11:11:24 +00:00
Yaroslav Kasperovych
963cd77900 Fix require clause in fastjson migration guide
Seems like the require clause should be different as it produces an error if used as it is right now.
2020-11-06 10:15:26 +00:00
Tony Dehnke
98dd59884c Fix namespace name per comment in #139 2020-10-26 12:01:52 +00:00
Guillaume Briday
b7e8a30833 Fix typo in readme 2020-10-25 18:13:08 +00:00
Guillaume Briday
f4ed4f0440 Adding migration section 2020-10-25 17:23:20 +00:00
Jorge Garcia
88061bcfac Add deserialization options on Readme 2020-10-08 10:21:20 +01:00
nattfodd
f8255771dc Raise FastJsonapi scoped error in case of unsupported include 2020-09-18 13:12:41 +03:00
Donatas Povilaitis
1bcf8d2cb5
Do not add empty relationships key (#116) 2020-09-01 20:44:08 +01:00
Stas SUȘCOV
0b819e09b9
Version bump. 2020-08-30 16:21:07 +01:00
Stas SUȘCOV
72f3ae64dd Remove skylight and old instrumentation support. 2020-08-30 15:57:07 +01:00
Stas SUȘCOV
eae66a9610 Added new instrumentation support. 2020-08-30 15:57:07 +01:00
Stas SUȘCOV
5538555537 Detect enumerables strictly. Closes #112 2020-08-27 11:51:38 +01:00
Stas SUȘCOV
4d1a764abc Updated cache options helper.
Allow extra arguments to improve the API interface.
2020-08-27 11:51:15 +01:00
Kapil Sachdev
c32358ecf5 fix: Rename FastJsonapi::ObjectSerializer to JSONAPI::Serializer
As the project has been renamed, its better to reflect it in the source 
code as well.
JSONAPI::Serializer is evaluated from FastJsonapi::ObjectSerializer so 
this change  probably will go unnoticed in gem usage.
2020-08-27 10:57:15 +01:00
Kapil Sachdev
f56a354860
Update links to use https (#114) 2020-08-25 18:50:24 +01:00
Matthew Newell
8401d16c2e Fix cache keys to prevent fieldset caching errors
This change alters the cache namespace prior to retrieving cached record
data to ensure that different fieldsets are given different cache keys.

Previously, all cache keys for the same record would be specified
identically, leading to a situation where the fieldset would be ignored
if record caching is enabled.

Fixes #90.
2020-08-11 12:54:57 +01:00
Stas SUȘCOV
1ce4677a22
Docs for how the new parsing of includes works. 2020-08-07 12:45:13 +01:00
Nathaniel Bibler
2a946c7723 Use Ruby's safe navigation for Rubocop 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
51b319def5 Remove unused variable 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
593e8ea4e6 Reduce relationship lookups 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
e2ac01f98a Use a Set for managing uniqueness behavior 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
a4bd5a1edc Remove exponential increase in nested includes 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
f62a5bf162 Remove a looped conditional and #blank? use 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
430a5ca2ac Remove unnecessary sort 2020-08-07 12:36:29 +01:00
Nathaniel Bibler
84cd54bd0e Relocate highly-repetitive relationship check 2020-08-07 12:36:29 +01:00
David Angulo
0e051fcad2 Add example for custom attribute with condtion 2020-08-07 12:35:11 +01:00
Semyon Pupkov
eab3dbdb27 Use jsonapi-rspec version 2020-07-10 09:14:11 +01:00
mperice
dd7f5ba415
Add optional meta field to relationships (#99) (#100)
* Add optional meta field to relationships (#99)

* Fix Rubocop's warnings.

* Minor syntax corrections.
2020-07-07 15:22:25 +01:00
Stas SUȘCOV
6db86e0f4c Switched CI to Rubygems. 2020-06-22 14:47:59 +01:00
Stas SUȘCOV
95ce09d526 Updated the readme. 2020-06-22 14:47:59 +01:00
Stas SUȘCOV
77d622e4a8 Renamed. Added an alias for now... 2020-06-22 14:47:59 +01:00
Kevin Pheasey
3ff69ccce1
Remove ObjectSerializer#serialized_json (#91)
* chore(ObjectSerializer): remove deprecated serialized_json
* update CHANGELOG.md
2020-06-09 14:03:15 +01:00
sun
1819629408
Add set_type documentation (#88) 2020-05-25 12:53:35 +01:00
Kevin Pheasey
2de80d4889
Added tests for polymorphic belongs_to
* chore(tests): add missing test for relationships belongs_to polymorphic type definitions

* chore(tests): add missing test for relationships belongs_to polymorphic type definitions

* linting
2020-05-19 10:18:57 +01:00
Kevin Pheasey
9d08d2abed chore(gitignore): ignore gem builds 2020-05-18 11:24:00 -04:00
Kevin Pheasey
7e2289006c chore(version): release 1.7.2 2020-05-18 11:21:44 -04:00
Kevin Pheasey
1d2eab2510
fix(Relationship): do not set static_record_type for polymorphic relationships (#82) 2020-05-18 10:09:58 -04:00
Stas SUȘCOV
ac34aa22f7
Revert "Updated the installation steps."
This reverts commit ab9db291d4d89ed93029f0844461498364d3be3b.
2020-05-01 16:30:22 +01:00
Kevin Pheasey
b64a0ceeed
fix(ObjectSerializer): pass to_json arguments (#80)
* fix(ObjectSerializer): pass to_json arguments

to_json takes arguments that serialized_json did not define.  This breaks a lot of things.

* Update CHANGELOG.md with latest fix and release
2020-05-01 09:02:03 -04:00
Stas SUȘCOV
c533634293 Rewrite tests. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
73814d1be5 Use rake to run tests. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
713e0ef58d Remove old tests. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
a529d2808a Added Rubocop. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
12e2987420 Rubocop cleanups. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
c32aacd8a6 Updated the readme with performance tests. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
529f51b04e Remove dead code. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
100711f678 Updated the gemspec. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
2a791bd90c Remove performance and skipped tests. 2020-04-29 15:30:44 +01:00
Stas SUȘCOV
ab9db291d4
Updated the installation steps. 2020-04-29 15:15:30 +01:00
Stas SUȘCOV
c31bcdc379
Fixed the org name for GPR. 2020-04-29 15:07:52 +01:00
Stas SUȘCOV
7c31594740
Cleanup the gemspec. 2020-04-29 14:53:03 +01:00
Stas SUȘCOV
07d46a9aac
Try a release gracefully.
If a release exists, it will fail, which is ok.
2020-04-29 14:44:00 +01:00
Stas SUȘCOV
297551c551 Version bump. Updated the changelog. 2020-04-29 14:35:11 +01:00
Stas SUȘCOV
268dbdab17 Publish tagged releases on GPR. 2020-04-29 14:35:11 +01:00
Mark Siemers
0edebd99e6 Check #each instead of #size for is_collection?
See if a resource responds to 'each' instead of 'size' as
things other than collections can respond to 'size' (e.g. File instance)
2020-04-11 12:48:00 +01:00
Markus
990c0acd18
Fix attribute API with lambda (#67)
* add failing spec when attribute is called with lambda
* allow to pass a lambda to `attribute`
2020-02-27 17:49:02 +00:00
Markus
843d943e07
Refactor caching support (#52)
- Very explicit where caching is stored
- No `Rails.cache` automagic with possible colliding keys
- Rails 5.2+ cache versioning support
- Implicit namespace support (by cache instance, e.g.
  ActiveSupport::Cache::MemoryStore.new(namespace: 'jast_jsonapi')
- All cache instance options are supported
2020-02-25 16:49:02 +00:00
Attila Horvath
3faca2d1e0 Fix conditional procedures with lambdas 2020-02-21 14:36:50 +00:00
Attila Horvath
08a20d0ebb Fix relationships with &:proc shorthands 2020-02-20 13:44:18 +00:00
James Reichley
9daa5a64f1 Fix syntax error in json_serialization.md 2020-02-19 09:51:03 +00:00
hotatekaoru
e79406d455 Fix README typo
Fix typo in the readme sample command
from 
1b3d73cbf3
2020-02-18 13:49:06 +00:00
Stas SUȘCOV
1b3d73cbf3
Remove multi-to-json. Add a deprecation warning. 2020-02-15 15:05:06 +00:00
Christopher Sansone
6d01bec146
Improved relationship serializer options (#32)
* allow the relationship's serializer key to be a Proc

* fixes

* specifically test the relationship name and the resource type

* support object blocks without a serializer defined

* stop validation gracefully if the relationship is polymorphic

* improve performance by using instance variables instead of accessor methods

* force initialization up front to avoid the iterative calls

* serializer procs should act like polymorphic when determining the id_method_name

* specs for serializer procs

* updated with more details and examples for relationship serializer options

* adjust specs to define the serializers

* avoid extra method calls for performance

* name change

* one less function call for better performance

* do not require lazy loaded relationships to resolve the serializer

* give polymorphic precedence for backwards compatibility

* move serializer inference into ObjectSerializer to allow for overriding

* move method for better diff

* fix race condition in multi-threaded environments
2020-02-15 14:57:44 +00:00
Pedro Caseiro
79a49fb3bd Run CI against ruby 2.7 2020-01-17 16:07:21 +00:00
Stas SUȘCOV
ca5c776d71 Added templates for contributing. Minor cleanups. 2020-01-17 13:46:34 +00:00
Stas SUȘCOV
eb8460fd74 Switch to Github CI. 2020-01-07 18:51:20 +00:00
Stas SUȘCOV
2172e40892 Remove unused files. 2020-01-07 18:51:20 +00:00
Attila Horváth
c305b699a9 Clean up some whitespace in the README (#35) 2019-12-18 13:19:21 +00:00
Aubrey Holland
1d7c18f5da Support for polymorphic id_method_name (#17)
* fix id method bugs, add specs
* Use SecureRandom.uuid
2019-12-03 18:04:04 +00:00
Nick Rattermann
2d92ab4cf9 Update README.md (#20)
* Update README.md

Update README.md to include proper way to install this forked gem.

* Update README.md
2019-11-20 12:05:04 +00:00
Kevin Pheasey
6a08165347
Merge pull request #18 from fast-jsonapi/dev
1.6.0
2019-11-04 17:38:13 -05:00
80 changed files with 1798 additions and 4354 deletions

View File

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

40
.github/workflows/ci.yml vendored Normal file
View File

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

7
.gitignore vendored
View File

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

102
.rubocop.yml Normal file
View File

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

View File

@ -1,15 +0,0 @@
language: ruby
rvm:
- 2.2.9
- 2.3.6
- 2.4.3
- 2.5.0
- 2.6
before_install:
- "travis_retry gem update --system 2.7.9"
- "travis_retry gem install bundler -v '1.17.3'"
install: BUNDLER_VERSION=1.17.3 bundle install --path=vendor/bundle --retry=3 --jobs=3
script:
- bundle exec rspec

View File

@ -4,6 +4,66 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- ...
## [2.2.0] - 2021-03-11
### Added
- Proper error is raised on unsupported includes (#125)
### Changed
- Documentation updates (#137 #139 #143 #146)
### Fixed
- Empty relationships are no longer added to serialized doc (#116)
- Ruby v3 compatibility (#160)
## [2.1.0] - 2020-08-30
### Added
- Optional meta field to relationships (#99 #100)
- Support for `params` on cache keys (#117)
### Changed
- Performance instrumentation (#110 #39)
- Improved collection detection (#112)
### Fixed
- Ensure caching correctly incorporates fieldset information into the cache key to prevent incorrect fieldset caching (#90)
- Performance optimizations for nested includes (#103)
## [2.0.0] - 2020-06-22
The project was renamed to `jsonapi-serializer`! (#94)
### Changed
- Remove `ObjectSerializer#serialized_json` (#91)
## [1.7.2] - 2020-05-18
### Fixed
- Relationship#record_type_for does not assign static record type for polymorphic relationships (#83)
## [1.7.1] - 2020-05-01
### Fixed
- ObjectSerializer#serialized_json accepts arguments for to_json (#80)
## [1.7.0] - 2020-04-29
### Added
- Serializer option support for procs (#32)
- JSON serialization API method is now implementable (#44)
### Changed
- Support for polymorphic `id_method_name` (#17)
- Relationships support for `&:proc` syntax (#58)
- Conditional support for procs (#59)
- Attribute support for procs (#67)
- Refactor caching support (#52)
- `is_collection?` is safer for objects (#18)
### Removed
- `serialized_json` is now deprecated (#44)
## [1.6.0] - 2019-11-04
### Added
- Allow relationship links to be delcared as a method ([#2](https://github.com/fast-jsonapi/fast_jsonapi/pull/2))
@ -17,6 +77,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Define ObjectSerializer.set_key_transform mapping as a constant ([#7](https://github.com/fast-jsonapi/fast_jsonapi/pull/7))
- Optimize SerializtionCore.remaining_items by taking from original array ([#9](https://github.com/fast-jsonapi/fast_jsonapi/pull/9))
- Optimize ObjectSerializer.deep_symbolize by using each_with_object instead of Hash[map] ([#6](https://github.com/fast-jsonapi/fast_jsonapi/pull/6))
[Unreleased]: https://github.com/fast-jsonapi/fast_jsonapi/compare/dev...HEAD
[1.6.0]: https://github.com/fast-jsonapi/fast_jsonapi/compare/1.5...1.6.0

View File

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

View File

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

View File

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

338
README.md
View File

@ -1,24 +1,29 @@
# Fast JSON API
# JSON:API Serialization Library
[![Build Status](https://travis-ci.org/Netflix/fast_jsonapi.svg?branch=master)](https://travis-ci.org/Netflix/fast_jsonapi)
## :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).
Note: this gem deals only with implementing the JSON:API spec. If your API
responses are not formatted according to the JSON:API spec, this library will
not work for you.
---
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
We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the [performance document](https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md) for any questions related to methodology.
We compare serialization times with `ActiveModelSerializer` and alternative
implementations as part of performance tests available at
[jsonapi-serializer/comparisons](https://github.com/jsonapi-serializer/comparisons).
## Benchmark times for 250 records
```bash
$ rspec
Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms
```
We want to ensure that with every
change on this library, serialization time stays significantly faster than
the performance provided by the alternatives. Please read the performance
article in the `docs` folder for any questions related to methodology.
# Table of Contents
@ -36,8 +41,12 @@ Fast JSON API serialized 250 records in 3.01 ms
* [Params](#params)
* [Conditional Attributes](#conditional-attributes)
* [Conditional Relationships](#conditional-relationships)
* [Specifying a Relationship Serializer](#specifying-a-relationship-serializer)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods)
* [Performance Instrumentation](#performance-instrumentation)
* [Deserialization](#deserialization)
* [Migrating from Netflix/fast_jsonapi](#migrating-from-netflixfast_jsonapi)
* [Contributing](#contributing)
@ -54,7 +63,7 @@ Fast JSON API serialized 250 records in 3.01 ms
Add this line to your application's Gemfile:
```ruby
gem 'fast_jsonapi'
gem 'jsonapi-serializer'
```
Execute:
@ -85,7 +94,8 @@ end
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_type :movie # optional
set_id :owner_id # optional
attributes :name, :year
@ -127,7 +137,7 @@ hash = MovieSerializer.new(movie).serializable_hash
#### Return Serialized JSON
```ruby
json_string = MovieSerializer.new(movie).serialized_json
json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
#### Serialized Output
@ -166,12 +176,16 @@ json_string = MovieSerializer.new(movie).serialized_json
```
#### The Optionality of `set_type`
By default fast_jsonapi will try to figure the type based on the name of the serializer class. For example `class MovieSerializer` will automatically have a type of `:movie`. If your serializer class name does not follow this format, you have to manually state the `set_type` at the serializer.
### Key Transforms
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel
end
@ -186,13 +200,13 @@ set_key_transform :underscore # "some_key" => "some_key"
```
### Attributes
Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
Attributes are defined using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attribute :name
end
@ -202,7 +216,7 @@ Custom attributes that must be serialized but do not exist on the model can be d
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name, :year
@ -216,7 +230,7 @@ The block syntax can also be used to override the property on the object:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attribute :name do |object|
"#{object.name} Part 2"
@ -228,7 +242,7 @@ Attributes can also use a different name by passing the original method or acces
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name
@ -237,7 +251,7 @@ end
```
### Links Per Object
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.
Links are defined using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.
You can configure the method to use on the object for example a link with key `self` will get set to the value returned by a method called `url` on the movie object.
@ -245,29 +259,29 @@ You can also use a block to define a url as shown in `custom_url`. You can acces
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
link :public_url
link :self, :url
link :custom_url do |object|
"http://movies.com/#{object.name}-(#{object.year})"
"https://movies.com/#{object.name}-(#{object.year})"
end
link :personalized_url do |object, params|
"http://movies.com/#{object.name}-#{params[:user].reference_code}"
"https://movies.com/#{object.name}-#{params[:user].reference_code}"
end
end
```
#### Links on a Relationship
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))
You can specify [relationship links](https://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](https://jsonapi.org/format/#document-resource-object-related-resource-links))
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
has_many :actors, links: {
self: :url,
@ -302,7 +316,7 @@ For every resource in the collection, you can include a meta object containing n
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
meta do |movie|
{
@ -312,9 +326,26 @@ class MovieSerializer
end
```
#### Meta on a Relationship
You can specify [relationship meta](https://jsonapi.org/format/#document-resource-object-relationships) by using the `meta:` option on the serializer. Relationship meta in JSON API is useful if you wish to provide non-standard meta-information about the relationship.
Meta can be defined either by passing a static hash or by using Proc to the `meta` key. In the latter case, the record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, meta: Proc.new do |movie_record, params|
{ count: movie_record.actors.length }
end
end
```
### Compound Document
Support for top-level and nested included associations through ` options[:include] `.
Support for top-level and nested included associations through `options[:include]`.
```ruby
options = {}
@ -325,7 +356,7 @@ options[:links] = {
prev: '...'
}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new(movies, options).serialized_json
MovieSerializer.new(movies, options).serializable_hash.to_json
```
### Collection Serialization
@ -338,7 +369,7 @@ options[:links] = {
prev: '...'
}
hash = MovieSerializer.new(movies, options).serializable_hash
json_string = MovieSerializer.new(movies, options).serialized_json
json_string = MovieSerializer.new(movies, options).serializable_hash.to_json
```
#### Control Over Collection Serialization
@ -362,17 +393,50 @@ was introduced to be able to have precise control this behavior
- `false` will always treat input resource as *single object*
### Caching
Requires a `cache_key` method be defined on model:
To enable caching, use `cache_options store: <cache_store>`:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
cache_options enabled: true, cache_length: 12.hours
attributes :name, :year
include JSONAPI::Serializer
# use rails cache with a separate namespace and fixed expiry
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
```
`store` is required can be anything that implements a
`#fetch(record, **options, &block)` method:
- `record` is the record that is currently serialized
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports
- `&block` should be executed to fetch new data if cache is empty
So for the example above it will call the cache instance like this:
```ruby
Rails.cache.fetch(record, namespace: 'jsonapi-serializer', expires_in: 1.hour) { ... }
```
#### Caching and Sparse Fieldsets
If caching is enabled and fields are provided to the serializer, the fieldset will be appended to the cache key's namespace.
For example, given the following serializer definition and instance:
```ruby
class ActorSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
serializer = ActorSerializer.new(actor, { fields: { actor: [:first_name] } })
```
The following cache namespace will be generated: `'jsonapi-serializer-fieldset:first_name'`.
### Params
In some cases, attribute values might require more information than what is
@ -387,7 +451,7 @@ parameter.
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_id do |movie, params|
# in here, params is a hash containing the `:admin` key
@ -402,7 +466,7 @@ class MovieSerializer
belongs_to :primary_agent do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user].is_employee? ? true : false
params[:current_user]
end
end
@ -421,7 +485,7 @@ Conditional attributes can be defined by passing a Proc to the `if` key on the `
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name, :year
attribute :release_year, if: Proc.new { |record|
@ -433,6 +497,13 @@ class MovieSerializer
# The director will be serialized only if the :admin key of params is true
params && params[:admin] == true
}
# Custom attribute `name_year` will only be serialized if both `name` and `year` fields are present
attribute :name_year, if: Proc.new { |record|
record.name.present? && record.year.present?
} do |object|
"#{object.name} - #{object.year}"
end
end
# ...
@ -447,7 +518,7 @@ Conditional relationships can be defined by passing a Proc to the `if` key. Retu
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# Actors will only be serialized if the record has any associated actors
has_many :actors, if: Proc.new { |record| record.actors.any? }
@ -462,13 +533,59 @@ serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }
serializer.serializable_hash
```
### Specifying a Relationship Serializer
In many cases, the relationship can automatically detect the serializer to use.
```ruby
class MovieSerializer
include JSONAPI::Serializer
# resolves to StudioSerializer
belongs_to :studio
# resolves to ActorSerializer
has_many :actors
end
```
At other times, such as when a property name differs from the class name, you may need to explicitly state the serializer to use. You can do so by specifying a different symbol or the serializer class itself (which is the recommended usage):
```ruby
class MovieSerializer
include JSONAPI::Serializer
# resolves to MovieStudioSerializer
belongs_to :studio, serializer: :movie_studio
# resolves to PerformerSerializer
has_many :actors, serializer: PerformerSerializer
end
```
For more advanced cases, such as polymorphic relationships and Single Table Inheritance, you may need even greater control to select the serializer based on the specific object or some specified serialization parameters. You can do by defining the serializer as a `Proc`:
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, serializer: Proc.new do |record, params|
if record.comedian?
ComedianSerializer
elsif params[:use_drama_serializer]
DramaSerializer
else
ActorSerializer
end
end
end
```
### Sparse Fieldsets
Attributes and relationships can be selectively returned per record type by using the `fields` option.
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name, :year
end
@ -499,7 +616,7 @@ module AvatarHelper
end
class UserSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
include AvatarHelper # mixes in your helper method as class method
@ -524,7 +641,7 @@ module AvatarHelper
end
class UserSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
extend AvatarHelper # mixes in your helper method as class method
@ -543,66 +660,107 @@ end
Option | Purpose | Example
------------ | ------------- | -------------
set_type | Type name of Object | ```set_type :movie ```
key | Key of Object | ```belongs_to :owner, key: :user ```
set_id | ID of Object | ```set_id :owner_id ``` or ```set_id { \|record, params\| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" }```
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds```
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | ```has_many :locations, id_method_name: :place_ids ```
object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ```
record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user```
serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor``` or ```has_many :actors, serializer: MyApp::Api::V1::ActorSerializer```
polymorphic | Allows different record types for a polymorphic association | ```has_many :targets, polymorphic: true```
polymorphic | Sets custom record types for each object class in a polymorphic association | ```has_many :targets, polymorphic: { Person => :person, Group => :group }```
set_type | Type name of Object | `set_type :movie`
key | Key of Object | `belongs_to :owner, key: :user`
set_id | ID of Object | `set_id :owner_id` or `set_id { \|record, params\| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" }`
cache_options | Hash with store to enable caching and optional further cache options | `cache_options store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes`
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | `has_many :locations, id_method_name: :place_ids`
object_method_name | Set custom method name to get related objects | `has_many :locations, object_method_name: :places`
record_type | Set custom Object Type for a relationship | `belongs_to :owner, record_type: :user`
serializer | Set custom Serializer for a relationship | `has_many :actors, serializer: :custom_actor`, `has_many :actors, serializer: MyApp::Api::V1::ActorSerializer`, or `has_many :actors, serializer -> (object, params) { (return a serializer class) }`
polymorphic | Allows different record types for a polymorphic association | `has_many :targets, polymorphic: true`
polymorphic | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
### Instrumentation
### Performance Instrumentation
`fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer:
Performance instrumentation is available by using the
`active_support/notifications`.
To enable it, include the module in your serializer class:
```ruby
require 'fast_jsonapi/instrumentation/skylight'
require 'jsonapi/serializer'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
```
Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:
```ruby
require 'fast_jsonapi/instrumentation'
```
The two instrumented 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'
```
## Contributing
Please see [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md) for more details on contributing
[Skylight](https://www.skylight.io/) integration is also available and
supported by us, follow the Skylight documentation to enable it.
### Running Tests
We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional tests and performance tests. To run tests use the following command:
The project has and requires unit tests, functional tests and performance
tests. To run tests use the following command:
```bash
rspec
```
To run tests without the performance tests (for quicker test runs):
## Deserialization
We currently do not support deserialization, but we recommend to use any of the next gems:
```bash
rspec spec --tag ~performance:true
### [JSONAPI.rb](https://github.com/stas/jsonapi.rb)
This gem provides the next features alongside deserialization:
- Collection meta
- Error handling
- Includes and sparse fields
- Filtering and sorting
- Pagination
## Migrating from Netflix/fast_jsonapi
If you come from [Netflix/fast_jsonapi](https://github.com/Netflix/fast_jsonapi), here is the instructions to switch.
### Modify your Gemfile
```diff
- gem 'fast_jsonapi'
+ gem 'jsonapi-serializer'
```
To run tests only performance tests:
### Replace all constant references
```bash
rspec spec --tag performance:true
```diff
class MovieSerializer
- include FastJsonapi::ObjectSerializer
+ include JSONAPI::Serializer
end
```
### Replace removed methods
```diff
- json_string = MovieSerializer.new(movie).serialized_json
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
### Replace require references
```diff
- require 'fast_jsonapi'
+ require 'jsonapi/serializer'
```
### Update your cache options
See [docs](https://github.com/jsonapi-serializer/jsonapi-serializer#caching).
```diff
- cache_options enabled: true, cache_length: 12.hours
+ cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
```
## Contributing
Please follow the instructions we provide as part of the issue and
pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and
contributors are expected to adhere to the
[Contributor Covenant](https://contributor-covenant.org) code of conduct.

15
Rakefile Normal file
View File

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

@ -1 +0,0 @@
1.0.18

16
docs/ISSUE_TEMPLATE.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,37 @@
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,5 +1,7 @@
# frozen_string_literal: true
require 'jsonapi/serializer/errors'
module FastJsonapi
require 'fast_jsonapi/object_serializer'
if defined?(::Rails)

View File

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

View File

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

View File

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

@ -1,15 +0,0 @@
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,2 +1,3 @@
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
require 'skylight'
warn('DEPRECATION: Skylight support was moved into the `skylight` gem.')

View File

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

View File

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

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

View File

@ -1,10 +1,11 @@
# frozen_string_literal: true
require 'active_support'
require 'active_support/time'
require 'active_support/json'
require 'active_support/concern'
require 'active_support/inflector'
require 'active_support/core_ext/numeric/time'
require 'fast_jsonapi/helpers'
require 'fast_jsonapi/attribute'
require 'fast_jsonapi/relationship'
require 'fast_jsonapi/link'
@ -15,8 +16,6 @@ module FastJsonapi
extend ActiveSupport::Concern
include SerializationCore
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
TRANSFORMS_MAPPING = {
camel: :camelize,
camel_lower: [:camelize, :lower],
@ -36,11 +35,13 @@ module FastJsonapi
end
def serializable_hash
return hash_for_collection if is_collection?(@resource, @is_collection)
if self.class.is_collection?(@resource, @is_collection)
return hash_for_collection
end
hash_for_one_record
end
alias_method :to_hash, :serializable_hash
alias to_hash serializable_hash
def hash_for_one_record
serializable_hash = { data: nil }
@ -72,10 +73,6 @@ module FastJsonapi
serializable_hash
end
def serialized_json
ActiveSupport::JSON.encode(serializable_hash)
end
private
def process_options(options)
@ -84,12 +81,12 @@ module FastJsonapi
return if options.blank?
@known_included_objects = {}
@known_included_objects = Set.new
@meta = options[:meta]
@links = options[:links]
@is_collection = options[:is_collection]
@params = options[:params] || {}
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash)
if options[:include].present?
@includes = options[:include].reject(&:blank?).map(&:to_sym)
@ -109,13 +106,15 @@ module FastJsonapi
end
end
def is_collection?(resource, force_is_collection = nil)
return force_is_collection unless force_is_collection.nil?
resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
end
class_methods do
# Detects a collection/enumerable
#
# @return [TrueClass] on a successful detection
def is_collection?(resource, force_is_collection = nil)
return force_is_collection unless force_is_collection.nil?
resource.is_a?(Enumerable) && !resource.respond_to?(:each_pair)
end
def inherited(subclass)
super(subclass)
@ -124,22 +123,18 @@ module FastJsonapi
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
subclass.transform_method = transform_method
subclass.cache_length = cache_length
subclass.race_condition_ttl = race_condition_ttl
subclass.data_links = data_links.dup if data_links.present?
subclass.cached = cached
subclass.cache_store_instance = cache_store_instance
subclass.cache_store_options = cache_store_options
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
subclass.meta_to_serialize = meta_to_serialize
subclass.record_id = record_id
end
def reflected_record_type
return @reflected_record_type if defined?(@reflected_record_type)
@reflected_record_type ||= begin
if self.name && self.name.end_with?('Serializer')
self.name.split('::').last.chomp('Serializer').underscore.to_sym
end
end
@reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer'))
end
def set_key_transform(transform_name)
@ -148,13 +143,14 @@ module FastJsonapi
# ensure that the record type is correctly transformed
if record_type
set_type(record_type)
# TODO: Remove dead code
elsif reflected_record_type
set_type(reflected_record_type)
end
end
def run_key_transform(input)
if self.transform_method.present?
if transform_method.present?
input.to_s.send(*@transform_method).to_sym
else
input.to_sym
@ -175,15 +171,41 @@ module FastJsonapi
end
def cache_options(cache_options)
self.cached = cache_options[:enabled] || false
self.cache_length = cache_options[:cache_length] || 5.minutes
self.race_condition_ttl = cache_options[:race_condition_ttl] || 5.seconds
# FIXME: remove this if block once deprecated cache_options are not supported anymore
unless cache_options.key?(:store)
# fall back to old, deprecated behaviour because no store was passed.
# we assume the user explicitly wants new behaviour if he passed a
# store because this is the new syntax.
deprecated_cache_options(cache_options)
return
end
self.cache_store_instance = cache_options[:store]
self.cache_store_options = cache_options.except(:store)
end
# FIXME: remove this method once deprecated cache_options are not supported anymore
def deprecated_cache_options(cache_options)
warn('DEPRECATION WARNING: `store:` is a required cache option, we will default to `Rails.cache` for now. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.')
%i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
warn("DEPRECATION WARNING: `#{key}` is a deprecated cache option and will have no effect soon. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.")
end
self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
self.cache_store_options = {
expires_in: cache_options[:cache_length] || 5.minutes,
race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
}
end
def attributes(*attributes_list, &block)
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
self.attributes_to_serialize = {} if attributes_to_serialize.nil?
# to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
block = attributes_list.pop if attributes_list.last.is_a?(Proc)
attributes_list.each do |attr_name|
method_name = attr_name
@ -203,12 +225,14 @@ module FastJsonapi
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
if !relationship.cached
self.uncachable_relationships_to_serialize[relationship.name] = relationship
# TODO: Remove this undocumented option.
# Delegate the caching to the serializer exclusively.
if relationship.cached
cachable_relationships_to_serialize[relationship.name] = relationship
else
self.cachable_relationships_to_serialize[relationship.name] = relationship
uncachable_relationships_to_serialize[relationship.name] = relationship
end
self.relationships_to_serialize[relationship.name] = relationship
relationships_to_serialize[relationship.name] = relationship
end
def has_many(relationship_name, options = {}, &block)
@ -226,75 +250,86 @@ module FastJsonapi
add_relationship(relationship)
end
def meta(&block)
self.meta_to_serialize = block
def meta(meta_name = nil, &block)
self.meta_to_serialize = block || meta_name
end
def create_relationship(base_key, relationship_type, options, block)
name = base_key.to_sym
if relationship_type == :has_many
base_serialization_key = base_key.to_s.singularize
base_key_sym = base_serialization_key.to_sym
id_postfix = '_ids'
else
base_serialization_key = base_key
base_key_sym = name
id_postfix = '_id'
end
polymorphic = fetch_polymorphic_option(options)
Relationship.new(
owner: self,
key: options[:key] || run_key_transform(base_key),
name: name,
id_method_name: compute_id_method_name(
options[:id_method_name],
"#{base_serialization_key}#{id_postfix}".to_sym,
polymorphic,
options[:serializer],
block
),
record_type: options[:record_type] || run_key_transform(base_key_sym),
record_type: options[:record_type],
object_method_name: options[:object_method_name] || name,
object_block: block,
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
serializer: options[:serializer],
relationship_type: relationship_type,
cached: options[:cached],
polymorphic: fetch_polymorphic_option(options),
polymorphic: polymorphic,
conditional_proc: options[:if],
transform_method: @transform_method,
meta: options[:meta],
links: options[:links],
lazy_load_data: options[:lazy_load_data]
)
end
def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, block)
if block.present?
def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
if block.present? || serializer.is_a?(Proc) || polymorphic
custom_id_method_name || :id
else
custom_id_method_name || id_method_name_from_relationship
end
end
def compute_serializer_name(serializer_key)
return serializer_key unless serializer_key.is_a? Symbol
def serializer_for(name)
namespace = self.name.gsub(/()?\w+Serializer$/, '')
serializer_name = serializer_key.to_s.classify + 'Serializer'
(namespace + serializer_name).to_sym
serializer_name = "#{name.to_s.demodulize.classify}Serializer"
serializer_class_name = namespace + serializer_name
begin
serializer_class_name.constantize
rescue NameError
raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \
"Attempted to find '#{serializer_class_name}'. " \
'Consider specifying the serializer directly through options[:serializer].'
end
end
def fetch_polymorphic_option(options)
option = options[:polymorphic]
return false unless option.present?
return option if option.respond_to? :keys
{}
end
# def link(link_name, link_method_name = nil, &block)
def link(*params, &block)
self.data_links = {} if self.data_links.nil?
self.data_links = {} if data_links.nil?
options = params.last.is_a?(Hash) ? params.pop : {}
link_name = params.first
link_method_name = params[-1]
key = run_key_transform(link_name)
self.data_links[key] = Link.new(
data_links[key] = Link.new(
key: key,
method: block || link_method_name,
options: options
@ -304,14 +339,11 @@ module FastJsonapi
def validate_includes!(includes)
return if includes.blank?
includes.each do |include_item|
klass = self
parse_include_item(include_item).each do |parsed_include|
relationships_to_serialize = klass.relationships_to_serialize || {}
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
klass = relationship_to_include.serializer.to_s.constantize unless relationship_to_include.polymorphic.is_a?(Hash)
end
parse_includes_list(includes).each_key do |include_item|
relationship_to_include = relationships_to_serialize[include_item]
raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_include
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
end
end
end

View File

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

View File

@ -10,17 +10,17 @@ module FastJsonapi
def serialize(record, serialization_params, output_hash)
if conditionally_allowed?(record, serialization_params)
output_hash[key] = if method.is_a?(Proc)
method.arity.abs == 1 ? method.call(record) : method.call(record, serialization_params)
if method.is_a?(Proc)
output_hash[key] = FastJsonapi.call_proc(method, record, serialization_params)
else
record.public_send(method)
output_hash[key] = record.public_send(method)
end
end
end
def conditionally_allowed?(record, serialization_params)
if conditional_proc.present?
conditional_proc.call(record, serialization_params)
FastJsonapi.call_proc(conditional_proc, record, serialization_params)
else
true
end

View File

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

View File

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

View File

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

View File

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

12
lib/jsonapi/serializer.rb Normal file
View File

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

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

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

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

View File

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

40
spec/fixtures/_user.rb vendored Normal file
View File

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

80
spec/fixtures/actor.rb vendored Normal file
View File

@ -0,0 +1,80 @@
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 Normal file
View File

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

View File

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

@ -1,73 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when caching has_many' do
before(:each) do
rails = OpenStruct.new
rails.cache = ActiveSupport::Cache::MemoryStore.new
stub_const('Rails', rails)
end
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[: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
end

View File

@ -1,516 +0,0 @@
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
let(:serializer) { AppName::V1::MovieSerializer }
let(:children) { [:roles] }
context 'with overrides' do
let(:children) { [:roles, id_method_name: :roles_only_ids, record_type: :super_role] }
it_behaves_like 'returning correct relationship hash', :'AppName::V1::RoleSerializer', :roles_only_ids, :super_role
end
context 'without overrides' do
let(:children) { [:roles] }
it_behaves_like 'returning correct relationship hash', :'AppName::V1::RoleSerializer', :role_ids, :role
end
end
context 'without namespace' do
let(:serializer) { MovieSerializer }
context 'with overrides' do
let(:children) { [:roles, id_method_name: :roles_only_ids, record_type: :super_role] }
it_behaves_like 'returning correct relationship hash', :'RoleSerializer', :roles_only_ids, :super_role
end
context 'without overrides' do
let(:children) { [:roles] }
it_behaves_like 'returning correct relationship hash', :'RoleSerializer', :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 '#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
let(:parent) { [:area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area] }
it_behaves_like 'returning correct relationship hash', :'MyAreaSerializer', :blah_id, :awesome_area
end
context 'without overrides' do
let(:parent) { [:area] }
it_behaves_like 'returning correct relationship hash', :'AreaSerializer', :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 '#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
let(:partner) { [:area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area] }
it_behaves_like 'returning correct relationship hash', :'MyAreaSerializer', :blah_id, :awesome_area
end
context 'without overrides' do
let(:partner) { [:area] }
it_behaves_like 'returning correct relationship hash', :'AreaSerializer', :area_id, :area
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
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)
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 }
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
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
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

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

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

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

@ -1,51 +0,0 @@
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 ListSerializer
include FastJsonapi::ObjectSerializer
set_type :list
attributes :name
set_key_transform :dash
has_many :items, polymorphic: true
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
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
end
end

View File

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,490 +0,0 @@
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 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 enabled: true
end
class CachingMovieWithHasManySerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
has_many :actors, cached: true
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
end
class ActorSerializer
include FastJsonapi::ObjectSerializer
set_type :actor
attributes :name, :email
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 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 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
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

@ -1,18 +0,0 @@
RSpec.shared_examples 'returning correct relationship hash' do |serializer, 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.serializer).to be serializer
expect(relationship.id_method_name).to be id_method_name
expect(relationship.record_type).to be record_type
end
end
RSpec.shared_examples 'returning key transformed hash' do |movie_type, serializer_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(movie_type)
expect(hash[:data][0][:relationships][movie_type][:data][:type]).to eq(movie_type)
expect(hash[:included][0][:type]).to eq(serializer_type)
end
end

View File

@ -1,23 +1,30 @@
require 'active_record'
require 'fast_jsonapi'
require 'rspec-benchmark'
require 'byebug'
require 'active_model_serializers'
require 'oj'
require 'jsonapi/serializable'
require 'jsonapi-serializers'
require 'simplecov'
Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file }
Dir[File.dirname(__FILE__) + '/shared/examples/*.rb'].each {|file| require file }
SimpleCov.start do
add_group 'Lib', 'lib'
add_group 'Tests', 'spec'
end
SimpleCov.minimum_coverage 90
require 'active_support'
require 'active_support/core_ext/object/json'
require 'jsonapi/serializer'
require 'ffaker'
require 'rspec'
require 'jsonapi/rspec'
require 'byebug'
require 'securerandom'
Dir[File.expand_path('spec/fixtures/*.rb')].sort.each { |f| require f }
RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
if ENV['TRAVIS'] == 'true' || ENV['TRAVIS'] == true
config.filter_run_excluding performance: true
config.include JSONAPI::RSpec
config.mock_with :rspec
config.filter_run_when_matching :focus
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
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'))