Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7db80f673d | ||
|
bcee2b597b | ||
|
44cf8495ea | ||
|
37235057df | ||
|
c21f9def8f | ||
|
8b74954478 | ||
|
ef93f7f358 | ||
|
c5eb1ce27c | ||
|
a25d415b4d | ||
|
c3376037e7 | ||
|
963cd77900 | ||
|
98dd59884c | ||
|
b7e8a30833 | ||
|
f4ed4f0440 | ||
|
88061bcfac | ||
|
f8255771dc | ||
|
1bcf8d2cb5 | ||
|
0b819e09b9 | ||
|
72f3ae64dd | ||
|
eae66a9610 | ||
|
5538555537 | ||
|
4d1a764abc | ||
|
c32358ecf5 | ||
|
f56a354860 | ||
|
8401d16c2e | ||
|
1ce4677a22 | ||
|
2a946c7723 | ||
|
51b319def5 | ||
|
593e8ea4e6 | ||
|
e2ac01f98a | ||
|
a4bd5a1edc | ||
|
f62a5bf162 | ||
|
430a5ca2ac | ||
|
84cd54bd0e | ||
|
0e051fcad2 | ||
|
eab3dbdb27 | ||
|
dd7f5ba415 | ||
|
6db86e0f4c | ||
|
95ce09d526 | ||
|
77d622e4a8 | ||
|
3ff69ccce1 | ||
|
1819629408 | ||
|
2de80d4889 | ||
|
9d08d2abed | ||
|
7e2289006c | ||
|
1d2eab2510 | ||
|
ac34aa22f7 |
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@ -4,16 +4,16 @@ on: [push, pull_request]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-18.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ruby: [2.4, 2.5, 2.6, 2.7]
|
ruby: [2.4, 2.7, '3.0', 3.1, truffleruby-head]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Sets up the Ruby version
|
- name: Sets up the Ruby version
|
||||||
uses: actions/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: ${{ matrix.ruby }}
|
ruby-version: ${{ matrix.ruby }}
|
||||||
|
|
||||||
@ -26,16 +26,15 @@ jobs:
|
|||||||
- name: Runs code QA and tests
|
- name: Runs code QA and tests
|
||||||
run: bundle exec rake
|
run: bundle exec rake
|
||||||
|
|
||||||
- name: Publish to GPR
|
- name: Publish to Rubygems
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
if: ${{ github.ref == 'refs/heads/master' }}
|
if: ${{ github.ref == 'refs/heads/master' }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $HOME/.gem
|
mkdir -p $HOME/.gem
|
||||||
touch $HOME/.gem/credentials
|
touch $HOME/.gem/credentials
|
||||||
chmod 0600 $HOME/.gem/credentials
|
chmod 0600 $HOME/.gem/credentials
|
||||||
printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
printf -- "---\n:rubygems_api_key: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||||
gem build *.gemspec
|
gem build *.gemspec
|
||||||
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
gem push *.gem
|
||||||
env:
|
env:
|
||||||
GEM_HOST_API_KEY: ${{secrets.GPR_AUTH_TOKEN}}
|
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
|
||||||
OWNER: fast-jsonapi
|
|
||||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -41,5 +41,8 @@ test.db
|
|||||||
/vendor
|
/vendor
|
||||||
|
|
||||||
# Don't checkin Gemfile.lock
|
# Don't checkin Gemfile.lock
|
||||||
# See: http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
|
# See: https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
|
|
||||||
|
# Gem builds
|
||||||
|
/*.gem
|
||||||
|
24
.rubocop.yml
24
.rubocop.yml
@ -2,6 +2,10 @@ require:
|
|||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
|
|
||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
|
SuggestExtensions: false
|
||||||
|
|
||||||
Style/FrozenStringLiteralComment:
|
Style/FrozenStringLiteralComment:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
@ -38,6 +42,9 @@ Performance/TimesMap:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/**/**.rb'
|
- 'spec/**/**.rb'
|
||||||
|
|
||||||
|
Gemspec/RequiredRubyVersion:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
# TODO: Fix these...
|
# TODO: Fix these...
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@ -76,3 +83,20 @@ Naming/PredicateName:
|
|||||||
Naming/AccessorMethodName:
|
Naming/AccessorMethodName:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/**/**.rb'
|
- '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'
|
||||||
|
40
CHANGELOG.md
40
CHANGELOG.md
@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
- ...
|
||||||
|
|
||||||
|
## [2.2.0] - 2021-03-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Proper error is raised on unsupported includes (#125)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Documentation updates (#137 #139 #143 #146)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Empty relationships are no longer added to serialized doc (#116)
|
||||||
|
- Ruby v3 compatibility (#160)
|
||||||
|
|
||||||
|
## [2.1.0] - 2020-08-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Optional meta field to relationships (#99 #100)
|
||||||
|
- Support for `params` on cache keys (#117)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Performance instrumentation (#110 #39)
|
||||||
|
- Improved collection detection (#112)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure caching correctly incorporates fieldset information into the cache key to prevent incorrect fieldset caching (#90)
|
||||||
|
- Performance optimizations for nested includes (#103)
|
||||||
|
|
||||||
|
## [2.0.0] - 2020-06-22
|
||||||
|
|
||||||
|
The project was renamed to `jsonapi-serializer`! (#94)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove `ObjectSerializer#serialized_json` (#91)
|
||||||
|
|
||||||
|
## [1.7.2] - 2020-05-18
|
||||||
|
### Fixed
|
||||||
|
- Relationship#record_type_for does not assign static record type for polymorphic relationships (#83)
|
||||||
|
|
||||||
## [1.7.1] - 2020-05-01
|
## [1.7.1] - 2020-05-01
|
||||||
### Fixed
|
### Fixed
|
||||||
- ObjectSerializer#serialized_json accepts arguments for to_json (#80)
|
- ObjectSerializer#serialized_json accepts arguments for to_json (#80)
|
||||||
|
3
Gemfile
3
Gemfile
@ -2,6 +2,3 @@ source 'https://rubygems.org'
|
|||||||
|
|
||||||
# Specify your gem's dependencies in fast_jsonapi.gemspec
|
# Specify your gem's dependencies in fast_jsonapi.gemspec
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
# TODO: Remove once the gem is released...
|
|
||||||
gem 'jsonapi-rspec', github: 'jsonapi-rb/jsonapi-rspec'
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
https://www.apache.org/licenses/
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
@ -192,7 +192,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
|||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
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
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
231
README.md
231
README.md
@ -1,12 +1,24 @@
|
|||||||
# Fast JSON API
|
# JSON:API Serialization Library
|
||||||
|
|
||||||
A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.
|
## :warning: :construction: v2 (the `master` branch) is in maintenance mode! :construction: :warning:
|
||||||
|
|
||||||
|
We'll gladly accept bugfixes and security-related fixes for v2 (the `master` branch), but at this stage, contributions for new features/improvements are welcome only for v3. Please feel free to leave comments in the [v3 Pull Request](https://github.com/jsonapi-serializer/jsonapi-serializer/pull/141).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A fast [JSON:API](https://jsonapi.org/) serializer for Ruby Objects.
|
||||||
|
|
||||||
|
Previously this project was called **fast_jsonapi**, we forked the project
|
||||||
|
and renamed it to **jsonapi/serializer** in order to keep it alive.
|
||||||
|
|
||||||
|
We would like to thank the Netflix team for the initial work and to all our
|
||||||
|
contributors and users for the continuous support!
|
||||||
|
|
||||||
# Performance Comparison
|
# Performance Comparison
|
||||||
|
|
||||||
We compare serialization times with `ActiveModelSerializer` and alternative
|
We compare serialization times with `ActiveModelSerializer` and alternative
|
||||||
implementations as part of performance tests available at
|
implementations as part of performance tests available at
|
||||||
[fast-jsonapi/comparisons](https://github.com/fast-jsonapi/comparisons).
|
[jsonapi-serializer/comparisons](https://github.com/jsonapi-serializer/comparisons).
|
||||||
|
|
||||||
We want to ensure that with every
|
We want to ensure that with every
|
||||||
change on this library, serialization time stays significantly faster than
|
change on this library, serialization time stays significantly faster than
|
||||||
@ -32,6 +44,9 @@ article in the `docs` folder for any questions related to methodology.
|
|||||||
* [Specifying a Relationship Serializer](#specifying-a-relationship-serializer)
|
* [Specifying a Relationship Serializer](#specifying-a-relationship-serializer)
|
||||||
* [Sparse Fieldsets](#sparse-fieldsets)
|
* [Sparse Fieldsets](#sparse-fieldsets)
|
||||||
* [Using helper methods](#using-helper-methods)
|
* [Using helper methods](#using-helper-methods)
|
||||||
|
* [Performance Instrumentation](#performance-instrumentation)
|
||||||
|
* [Deserialization](#deserialization)
|
||||||
|
* [Migrating from Netflix/fast_jsonapi](#migrating-from-netflixfast_jsonapi)
|
||||||
* [Contributing](#contributing)
|
* [Contributing](#contributing)
|
||||||
|
|
||||||
|
|
||||||
@ -45,14 +60,10 @@ article in the `docs` folder for any questions related to methodology.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
The `fast_jsonapi` is hosted on Github Package Registry and not on Rubygems.
|
Add this line to your application's Gemfile:
|
||||||
|
|
||||||
Add the GPR source and the gem to your Gemfile:
|
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
source 'https://rubygems.pkg.github.com/fast-jsonapi'
|
gem 'jsonapi-serializer'
|
||||||
|
|
||||||
gem 'fast_jsonapi'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Execute:
|
Execute:
|
||||||
@ -83,7 +94,8 @@ end
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
set_type :movie # optional
|
set_type :movie # optional
|
||||||
set_id :owner_id # optional
|
set_id :owner_id # optional
|
||||||
attributes :name, :year
|
attributes :name, :year
|
||||||
@ -164,12 +176,16 @@ json_string = MovieSerializer.new(movie).serializable_hash.to_json
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### The Optionality of `set_type`
|
||||||
|
By default fast_jsonapi will try to figure the type based on the name of the serializer class. For example `class MovieSerializer` will automatically have a type of `:movie`. If your serializer class name does not follow this format, you have to manually state the `set_type` at the serializer.
|
||||||
|
|
||||||
### Key Transforms
|
### Key Transforms
|
||||||
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
|
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
# Available options :camel, :camel_lower, :dash, :underscore(default)
|
# Available options :camel, :camel_lower, :dash, :underscore(default)
|
||||||
set_key_transform :camel
|
set_key_transform :camel
|
||||||
end
|
end
|
||||||
@ -184,13 +200,13 @@ set_key_transform :underscore # "some_key" => "some_key"
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Attributes
|
### 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:
|
By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
attribute :name
|
attribute :name
|
||||||
end
|
end
|
||||||
@ -200,7 +216,7 @@ Custom attributes that must be serialized but do not exist on the model can be d
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
attributes :name, :year
|
attributes :name, :year
|
||||||
|
|
||||||
@ -214,7 +230,7 @@ The block syntax can also be used to override the property on the object:
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
attribute :name do |object|
|
attribute :name do |object|
|
||||||
"#{object.name} Part 2"
|
"#{object.name} Part 2"
|
||||||
@ -226,7 +242,7 @@ Attributes can also use a different name by passing the original method or acces
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
attributes :name
|
attributes :name
|
||||||
|
|
||||||
@ -235,7 +251,7 @@ end
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Links Per Object
|
### 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.
|
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.
|
||||||
|
|
||||||
@ -243,29 +259,29 @@ You can also use a block to define a url as shown in `custom_url`. You can acces
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
link :public_url
|
link :public_url
|
||||||
|
|
||||||
link :self, :url
|
link :self, :url
|
||||||
|
|
||||||
link :custom_url do |object|
|
link :custom_url do |object|
|
||||||
"http://movies.com/#{object.name}-(#{object.year})"
|
"https://movies.com/#{object.name}-(#{object.year})"
|
||||||
end
|
end
|
||||||
|
|
||||||
link :personalized_url do |object, params|
|
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
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Links on a Relationship
|
#### 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
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
has_many :actors, links: {
|
has_many :actors, links: {
|
||||||
self: :url,
|
self: :url,
|
||||||
@ -300,7 +316,7 @@ For every resource in the collection, you can include a meta object containing n
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
meta do |movie|
|
meta do |movie|
|
||||||
{
|
{
|
||||||
@ -310,6 +326,23 @@ class MovieSerializer
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Meta on a Relationship
|
||||||
|
|
||||||
|
You can specify [relationship meta](https://jsonapi.org/format/#document-resource-object-relationships) by using the `meta:` option on the serializer. Relationship meta in JSON API is useful if you wish to provide non-standard meta-information about the relationship.
|
||||||
|
|
||||||
|
Meta can be defined either by passing a static hash or by using Proc to the `meta` key. In the latter case, the record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
|
||||||
|
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class MovieSerializer
|
||||||
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
|
has_many :actors, meta: Proc.new do |movie_record, params|
|
||||||
|
{ count: movie_record.actors.length }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
### Compound Document
|
### Compound Document
|
||||||
|
|
||||||
Support for top-level and nested included associations through `options[:include]`.
|
Support for top-level and nested included associations through `options[:include]`.
|
||||||
@ -365,10 +398,10 @@ To enable caching, use `cache_options store: <cache_store>`:
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
# use rails cache with a separate namespace and fixed expiry
|
# use rails cache with a separate namespace and fixed expiry
|
||||||
cache_options store: Rails.cache, namespace: 'fast-jsonapi', expires_in: 1.hour
|
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -379,12 +412,31 @@ end
|
|||||||
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports
|
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports
|
||||||
- `&block` should be executed to fetch new data if cache is empty
|
- `&block` should be executed to fetch new data if cache is empty
|
||||||
|
|
||||||
So for the example above, FastJsonapi will call the cache instance like this:
|
So for the example above it will call the cache instance like this:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Rails.cache.fetch(record, namespace: 'fast-jsonapi, expires_in: 1.hour) { ... }
|
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
|
### Params
|
||||||
|
|
||||||
In some cases, attribute values might require more information than what is
|
In some cases, attribute values might require more information than what is
|
||||||
@ -399,7 +451,7 @@ parameter.
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
set_id do |movie, params|
|
set_id do |movie, params|
|
||||||
# in here, params is a hash containing the `:admin` key
|
# in here, params is a hash containing the `:admin` key
|
||||||
@ -414,7 +466,7 @@ class MovieSerializer
|
|||||||
|
|
||||||
belongs_to :primary_agent do |movie, params|
|
belongs_to :primary_agent do |movie, params|
|
||||||
# in here, params is a hash containing the `:current_user` key
|
# in here, params is a hash containing the `:current_user` key
|
||||||
params[:current_user].is_employee? ? true : false
|
params[:current_user]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -433,7 +485,7 @@ Conditional attributes can be defined by passing a Proc to the `if` key on the `
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
attributes :name, :year
|
attributes :name, :year
|
||||||
attribute :release_year, if: Proc.new { |record|
|
attribute :release_year, if: Proc.new { |record|
|
||||||
@ -445,6 +497,13 @@ class MovieSerializer
|
|||||||
# The director will be serialized only if the :admin key of params is true
|
# The director will be serialized only if the :admin key of params is true
|
||||||
params && params[:admin] == true
|
params && params[:admin] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Custom attribute `name_year` will only be serialized if both `name` and `year` fields are present
|
||||||
|
attribute :name_year, if: Proc.new { |record|
|
||||||
|
record.name.present? && record.year.present?
|
||||||
|
} do |object|
|
||||||
|
"#{object.name} - #{object.year}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
@ -459,7 +518,7 @@ Conditional relationships can be defined by passing a Proc to the `if` key. Retu
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
# Actors will only be serialized if the record has any associated actors
|
# Actors will only be serialized if the record has any associated actors
|
||||||
has_many :actors, if: Proc.new { |record| record.actors.any? }
|
has_many :actors, if: Proc.new { |record| record.actors.any? }
|
||||||
@ -480,7 +539,7 @@ In many cases, the relationship can automatically detect the serializer to use.
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
# resolves to StudioSerializer
|
# resolves to StudioSerializer
|
||||||
belongs_to :studio
|
belongs_to :studio
|
||||||
@ -493,7 +552,7 @@ At other times, such as when a property name differs from the class name, you ma
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
# resolves to MovieStudioSerializer
|
# resolves to MovieStudioSerializer
|
||||||
belongs_to :studio, serializer: :movie_studio
|
belongs_to :studio, serializer: :movie_studio
|
||||||
@ -506,7 +565,7 @@ For more advanced cases, such as polymorphic relationships and Single Table Inhe
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
has_many :actors, serializer: Proc.new do |record, params|
|
has_many :actors, serializer: Proc.new do |record, params|
|
||||||
if record.comedian?
|
if record.comedian?
|
||||||
@ -526,7 +585,7 @@ Attributes and relationships can be selectively returned per record type by usin
|
|||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
attributes :name, :year
|
attributes :name, :year
|
||||||
end
|
end
|
||||||
@ -557,7 +616,7 @@ module AvatarHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
class UserSerializer
|
class UserSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
include AvatarHelper # mixes in your helper method as class method
|
include AvatarHelper # mixes in your helper method as class method
|
||||||
|
|
||||||
@ -582,7 +641,7 @@ module AvatarHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
class UserSerializer
|
class UserSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
extend AvatarHelper # mixes in your helper method as class method
|
extend AvatarHelper # mixes in your helper method as class method
|
||||||
|
|
||||||
@ -612,36 +671,27 @@ serializer | Set custom Serializer for a relationship | `has_many :actors, seria
|
|||||||
polymorphic | Allows different record types for a polymorphic association | `has_many :targets, polymorphic: true`
|
polymorphic | Allows different record types for a polymorphic association | `has_many :targets, polymorphic: true`
|
||||||
polymorphic | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
|
polymorphic | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
|
||||||
|
|
||||||
### 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
|
```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:
|
[Skylight](https://www.skylight.io/) integration is also available and
|
||||||
|
supported by us, follow the Skylight documentation to enable it.
|
||||||
```ruby
|
|
||||||
require 'fast_jsonapi/instrumentation'
|
|
||||||
```
|
|
||||||
|
|
||||||
The two instrumented notifications are supplied by these two constants:
|
|
||||||
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
|
|
||||||
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`
|
|
||||||
|
|
||||||
It is also possible to instrument one method without the other by using one of the following require statements:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
require 'fast_jsonapi/instrumentation/serializable_hash'
|
|
||||||
require 'fast_jsonapi/instrumentation/serialized_json'
|
|
||||||
```
|
|
||||||
|
|
||||||
Same goes for the Skylight integration:
|
|
||||||
```ruby
|
|
||||||
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
|
|
||||||
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
The project has and requires unit tests, functional tests and performance
|
The project has and requires unit tests, functional tests and performance
|
||||||
@ -651,6 +701,61 @@ tests. To run tests use the following command:
|
|||||||
rspec
|
rspec
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Deserialization
|
||||||
|
We currently do not support deserialization, but we recommend to use any of the next gems:
|
||||||
|
|
||||||
|
### [JSONAPI.rb](https://github.com/stas/jsonapi.rb)
|
||||||
|
|
||||||
|
This gem provides the next features alongside deserialization:
|
||||||
|
- Collection meta
|
||||||
|
- Error handling
|
||||||
|
- Includes and sparse fields
|
||||||
|
- Filtering and sorting
|
||||||
|
- Pagination
|
||||||
|
|
||||||
|
## Migrating from Netflix/fast_jsonapi
|
||||||
|
|
||||||
|
If you come from [Netflix/fast_jsonapi](https://github.com/Netflix/fast_jsonapi), here is the instructions to switch.
|
||||||
|
|
||||||
|
### Modify your Gemfile
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- gem 'fast_jsonapi'
|
||||||
|
+ gem 'jsonapi-serializer'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace all constant references
|
||||||
|
|
||||||
|
```diff
|
||||||
|
class MovieSerializer
|
||||||
|
- include FastJsonapi::ObjectSerializer
|
||||||
|
+ include JSONAPI::Serializer
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace removed methods
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- json_string = MovieSerializer.new(movie).serialized_json
|
||||||
|
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace require references
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- require 'fast_jsonapi'
|
||||||
|
+ require 'jsonapi/serializer'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update your cache options
|
||||||
|
|
||||||
|
See [docs](https://github.com/jsonapi-serializer/jsonapi-serializer#caching).
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- cache_options enabled: true, cache_length: 12.hours
|
||||||
|
+ cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please follow the instructions we provide as part of the issue and
|
Please follow the instructions we provide as part of the issue and
|
||||||
@ -658,4 +763,4 @@ pull request creation processes.
|
|||||||
|
|
||||||
This project is intended to be a safe, welcoming space for collaboration, and
|
This project is intended to be a safe, welcoming space for collaboration, and
|
||||||
contributors are expected to adhere to the
|
contributors are expected to adhere to the
|
||||||
[Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
[Contributor Covenant](https://contributor-covenant.org) code of conduct.
|
||||||
|
@ -13,7 +13,7 @@ require 'oj'
|
|||||||
require 'fast_jsonapi'
|
require 'fast_jsonapi'
|
||||||
|
|
||||||
class BaseSerializer
|
class BaseSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
def to_json
|
def to_json
|
||||||
Oj.dump(serializable_hash)
|
Oj.dump(serializable_hash)
|
||||||
|
@ -23,7 +23,7 @@ cases.
|
|||||||
|
|
||||||
We came up with patterns that we can rely upon such as:
|
We came up with patterns that we can rely upon such as:
|
||||||
|
|
||||||
* We always use [JSON:API](http://jsonapi.org/) for our APIs
|
* We always use [JSON:API](https://jsonapi.org/) for our APIs
|
||||||
* We almost always serialize a homogenous list of objects (Example: An array of
|
* We almost always serialize a homogenous list of objects (Example: An array of
|
||||||
movies)
|
movies)
|
||||||
|
|
||||||
|
@ -1,25 +1,19 @@
|
|||||||
lib = File.expand_path('lib', __dir__)
|
lib = File.expand_path('lib', __dir__)
|
||||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||||
|
|
||||||
require 'fast_jsonapi/version'
|
require 'jsonapi/serializer/version'
|
||||||
|
|
||||||
Gem::Specification.new do |gem|
|
Gem::Specification.new do |gem|
|
||||||
gem.name = 'fast_jsonapi'
|
gem.name = 'jsonapi-serializer'
|
||||||
gem.version = FastJsonapi::VERSION
|
gem.version = JSONAPI::Serializer::VERSION
|
||||||
|
|
||||||
gem.authors = [
|
gem.authors = ['JSON:API Serializer Community']
|
||||||
'Shishir Kakaraddi',
|
|
||||||
'Srinivas Raghunathan',
|
|
||||||
'Adam Gross',
|
|
||||||
'github/fast-jsonapi community'
|
|
||||||
]
|
|
||||||
gem.email = ''
|
gem.email = ''
|
||||||
|
|
||||||
gem.summary = 'Fast JSON:API (jsonapi.org) serialization library'
|
gem.summary = 'Fast JSON:API serialization library'
|
||||||
gem.description =
|
gem.description = 'Fast, simple and easy to use '\
|
||||||
'Fast JSON:API (jsonapi.org) serialization library ' \
|
'JSON:API serialization library (also known as fast_jsonapi).'
|
||||||
'to work with any kind of objects'
|
gem.homepage = 'https://github.com/jsonapi-serializer/jsonapi-serializer'
|
||||||
gem.homepage = 'http://github.com/fast-jsonapi/fast_jsonapi'
|
|
||||||
gem.licenses = ['Apache-2.0']
|
gem.licenses = ['Apache-2.0']
|
||||||
gem.files = Dir['lib/**/*']
|
gem.files = Dir['lib/**/*']
|
||||||
gem.require_paths = ['lib']
|
gem.require_paths = ['lib']
|
||||||
@ -31,7 +25,7 @@ Gem::Specification.new do |gem|
|
|||||||
gem.add_development_dependency('bundler')
|
gem.add_development_dependency('bundler')
|
||||||
gem.add_development_dependency('byebug')
|
gem.add_development_dependency('byebug')
|
||||||
gem.add_development_dependency('ffaker')
|
gem.add_development_dependency('ffaker')
|
||||||
gem.add_development_dependency('jsonapi-rspec')
|
gem.add_development_dependency('jsonapi-rspec', '>= 0.0.5')
|
||||||
gem.add_development_dependency('rake')
|
gem.add_development_dependency('rake')
|
||||||
gem.add_development_dependency('rspec')
|
gem.add_development_dependency('rspec')
|
||||||
gem.add_development_dependency('rubocop')
|
gem.add_development_dependency('rubocop')
|
||||||
@ -39,4 +33,5 @@ Gem::Specification.new do |gem|
|
|||||||
gem.add_development_dependency('rubocop-rspec')
|
gem.add_development_dependency('rubocop-rspec')
|
||||||
gem.add_development_dependency('simplecov')
|
gem.add_development_dependency('simplecov')
|
||||||
gem.add_development_dependency('sqlite3')
|
gem.add_development_dependency('sqlite3')
|
||||||
|
gem.metadata['rubygems_mfa_required'] = 'true'
|
||||||
end
|
end
|
@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'jsonapi/serializer/errors'
|
||||||
|
|
||||||
module FastJsonapi
|
module FastJsonapi
|
||||||
require 'fast_jsonapi/object_serializer'
|
require 'fast_jsonapi/object_serializer'
|
||||||
if defined?(::Rails)
|
if defined?(::Rails)
|
||||||
|
@ -6,7 +6,16 @@ module FastJsonapi
|
|||||||
# @param [Array<Object>] *params any number of parameters to be passed to the Proc
|
# @param [Array<Object>] *params any number of parameters to be passed to the Proc
|
||||||
# @return [Object] the result of the Proc call with the supplied parameters
|
# @return [Object] the result of the Proc call with the supplied parameters
|
||||||
def call_proc(proc, *params)
|
def call_proc(proc, *params)
|
||||||
|
# The parameters array for a lambda created from a symbol (&:foo) differs
|
||||||
|
# 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))
|
proc.call(*params.take(proc.parameters.length))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,2 +1,7 @@
|
|||||||
require 'fast_jsonapi/instrumentation/serializable_hash'
|
require 'jsonapi/serializer/instrumentation'
|
||||||
require 'fast_jsonapi/instrumentation/serialized_json'
|
|
||||||
|
warn(
|
||||||
|
'DEPRECATION: Performance instrumentation is no longer automatic. See: ' \
|
||||||
|
'https://github.com/jsonapi-serializer/jsonapi-serializer' \
|
||||||
|
'#performance-instrumentation'
|
||||||
|
)
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
require 'active_support/notifications'
|
|
||||||
|
|
||||||
module FastJsonapi
|
|
||||||
module ObjectSerializer
|
|
||||||
alias 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
|
|
@ -1,13 +0,0 @@
|
|||||||
require 'active_support/notifications'
|
|
||||||
|
|
||||||
module FastJsonapi
|
|
||||||
module ObjectSerializer
|
|
||||||
alias 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
|
|
@ -1,2 +1,3 @@
|
|||||||
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
|
require 'skylight'
|
||||||
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
|
|
||||||
|
warn('DEPRECATION: Skylight support was moved into the `skylight` gem.')
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
require 'skylight'
|
|
||||||
|
|
||||||
SKYLIGHT_NORMALIZER_BASE_CLASS = begin
|
|
||||||
::Skylight::Core::Normalizers::Normalizer
|
|
||||||
rescue NameError
|
|
||||||
::Skylight::Normalizers::Normalizer
|
|
||||||
end
|
|
@ -1,20 +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
|
|
@ -1,20 +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
|
|
@ -1,5 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'active_support'
|
||||||
require 'active_support/time'
|
require 'active_support/time'
|
||||||
require 'active_support/concern'
|
require 'active_support/concern'
|
||||||
require 'active_support/inflector'
|
require 'active_support/inflector'
|
||||||
@ -15,8 +16,6 @@ module FastJsonapi
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include SerializationCore
|
include SerializationCore
|
||||||
|
|
||||||
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
|
|
||||||
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
|
|
||||||
TRANSFORMS_MAPPING = {
|
TRANSFORMS_MAPPING = {
|
||||||
camel: :camelize,
|
camel: :camelize,
|
||||||
camel_lower: [:camelize, :lower],
|
camel_lower: [:camelize, :lower],
|
||||||
@ -36,7 +35,9 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def serializable_hash
|
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
|
hash_for_one_record
|
||||||
end
|
end
|
||||||
@ -72,15 +73,6 @@ module FastJsonapi
|
|||||||
serializable_hash
|
serializable_hash
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialized_json(*args)
|
|
||||||
warn(
|
|
||||||
'DEPRECATION: `#serialized_json` will be removed in the next release. '\
|
|
||||||
'More details: https://github.com/fast-jsonapi/fast_jsonapi/pull/44'
|
|
||||||
)
|
|
||||||
serializable_hash.to_json(*args)
|
|
||||||
end
|
|
||||||
alias to_json serialized_json
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_options(options)
|
def process_options(options)
|
||||||
@ -89,7 +81,7 @@ module FastJsonapi
|
|||||||
|
|
||||||
return if options.blank?
|
return if options.blank?
|
||||||
|
|
||||||
@known_included_objects = {}
|
@known_included_objects = Set.new
|
||||||
@meta = options[:meta]
|
@meta = options[:meta]
|
||||||
@links = options[:links]
|
@links = options[:links]
|
||||||
@is_collection = options[:is_collection]
|
@is_collection = options[:is_collection]
|
||||||
@ -114,13 +106,16 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
# Detects a collection/enumerable
|
||||||
|
#
|
||||||
|
# @return [TrueClass] on a successful detection
|
||||||
def is_collection?(resource, force_is_collection = nil)
|
def is_collection?(resource, force_is_collection = nil)
|
||||||
return force_is_collection unless force_is_collection.nil?
|
return force_is_collection unless force_is_collection.nil?
|
||||||
|
|
||||||
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
|
resource.is_a?(Enumerable) && !resource.respond_to?(:each_pair)
|
||||||
end
|
end
|
||||||
|
|
||||||
class_methods do
|
|
||||||
def inherited(subclass)
|
def inherited(subclass)
|
||||||
super(subclass)
|
super(subclass)
|
||||||
subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
|
subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
|
||||||
@ -139,9 +134,7 @@ module FastJsonapi
|
|||||||
def reflected_record_type
|
def reflected_record_type
|
||||||
return @reflected_record_type if defined?(@reflected_record_type)
|
return @reflected_record_type if defined?(@reflected_record_type)
|
||||||
|
|
||||||
@reflected_record_type ||= begin
|
@reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer'))
|
||||||
name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_key_transform(transform_name)
|
def set_key_transform(transform_name)
|
||||||
@ -234,10 +227,10 @@ module FastJsonapi
|
|||||||
|
|
||||||
# TODO: Remove this undocumented option.
|
# TODO: Remove this undocumented option.
|
||||||
# Delegate the caching to the serializer exclusively.
|
# Delegate the caching to the serializer exclusively.
|
||||||
if !relationship.cached
|
if relationship.cached
|
||||||
uncachable_relationships_to_serialize[relationship.name] = relationship
|
|
||||||
else
|
|
||||||
cachable_relationships_to_serialize[relationship.name] = relationship
|
cachable_relationships_to_serialize[relationship.name] = relationship
|
||||||
|
else
|
||||||
|
uncachable_relationships_to_serialize[relationship.name] = relationship
|
||||||
end
|
end
|
||||||
relationships_to_serialize[relationship.name] = relationship
|
relationships_to_serialize[relationship.name] = relationship
|
||||||
end
|
end
|
||||||
@ -292,6 +285,7 @@ module FastJsonapi
|
|||||||
polymorphic: polymorphic,
|
polymorphic: polymorphic,
|
||||||
conditional_proc: options[:if],
|
conditional_proc: options[:if],
|
||||||
transform_method: @transform_method,
|
transform_method: @transform_method,
|
||||||
|
meta: options[:meta],
|
||||||
links: options[:links],
|
links: options[:links],
|
||||||
lazy_load_data: options[:lazy_load_data]
|
lazy_load_data: options[:lazy_load_data]
|
||||||
)
|
)
|
||||||
@ -307,7 +301,7 @@ module FastJsonapi
|
|||||||
|
|
||||||
def serializer_for(name)
|
def serializer_for(name)
|
||||||
namespace = self.name.gsub(/()?\w+Serializer$/, '')
|
namespace = self.name.gsub(/()?\w+Serializer$/, '')
|
||||||
serializer_name = name.to_s.demodulize.classify + 'Serializer'
|
serializer_name = "#{name.to_s.demodulize.classify}Serializer"
|
||||||
serializer_class_name = namespace + serializer_name
|
serializer_class_name = namespace + serializer_name
|
||||||
begin
|
begin
|
||||||
serializer_class_name.constantize
|
serializer_class_name.constantize
|
||||||
@ -345,21 +339,11 @@ module FastJsonapi
|
|||||||
def validate_includes!(includes)
|
def validate_includes!(includes)
|
||||||
return if includes.blank?
|
return if includes.blank?
|
||||||
|
|
||||||
includes.each do |include_item|
|
parse_includes_list(includes).each_key do |include_item|
|
||||||
klass = self
|
relationship_to_include = relationships_to_serialize[include_item]
|
||||||
parse_include_item(include_item).each do |parsed_include|
|
raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_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
|
|
||||||
|
|
||||||
if relationship_to_include.static_serializer
|
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
|
||||||
klass = relationship_to_include.static_serializer
|
|
||||||
else
|
|
||||||
# the serializer may change based on the object (e.g. polymorphic relationships),
|
|
||||||
# so inner relationships cannot be validated
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
module FastJsonapi
|
module FastJsonapi
|
||||||
class Relationship
|
class Relationship
|
||||||
attr_reader :owner, :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :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(
|
def initialize(
|
||||||
owner:,
|
owner:,
|
||||||
@ -12,11 +12,12 @@ module FastJsonapi
|
|||||||
object_block:,
|
object_block:,
|
||||||
serializer:,
|
serializer:,
|
||||||
relationship_type:,
|
relationship_type:,
|
||||||
cached: false,
|
|
||||||
polymorphic:,
|
polymorphic:,
|
||||||
conditional_proc:,
|
conditional_proc:,
|
||||||
transform_method:,
|
transform_method:,
|
||||||
links:,
|
links:,
|
||||||
|
meta:,
|
||||||
|
cached: false,
|
||||||
lazy_load_data: false
|
lazy_load_data: false
|
||||||
)
|
)
|
||||||
@owner = owner
|
@owner = owner
|
||||||
@ -33,6 +34,7 @@ module FastJsonapi
|
|||||||
@conditional_proc = conditional_proc
|
@conditional_proc = conditional_proc
|
||||||
@transform_method = transform_method
|
@transform_method = transform_method
|
||||||
@links = links || {}
|
@links = links || {}
|
||||||
|
@meta = meta || {}
|
||||||
@lazy_load_data = lazy_load_data
|
@lazy_load_data = lazy_load_data
|
||||||
@record_types_for = {}
|
@record_types_for = {}
|
||||||
@serializers_for_name = {}
|
@serializers_for_name = {}
|
||||||
@ -44,6 +46,8 @@ module FastJsonapi
|
|||||||
|
|
||||||
output_hash[key] = {}
|
output_hash[key] = {}
|
||||||
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case unless lazy_load_data && !included
|
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?
|
add_links_hash(record, serialization_params, output_hash) if links.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -146,11 +150,19 @@ module FastJsonapi
|
|||||||
record.public_send(links)
|
record.public_send(links)
|
||||||
else
|
else
|
||||||
links.each_with_object({}) do |(key, method), hash|
|
links.each_with_object({}) do |(key, method), hash|
|
||||||
Link.new(key: key, method: method).serialize(record, params, hash)\
|
Link.new(key: key, method: method).serialize(record, params, hash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
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)
|
def run_key_transform(input)
|
||||||
if transform_method.present?
|
if transform_method.present?
|
||||||
input.to_s.send(*transform_method).to_sym
|
input.to_s.send(*transform_method).to_sym
|
||||||
@ -212,8 +224,13 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
|
|
||||||
def compute_static_record_type
|
def compute_static_record_type
|
||||||
return run_key_transform(record_type) if record_type
|
if polymorphic
|
||||||
return run_key_transform(@static_serializer.record_type) if @static_serializer
|
nil
|
||||||
|
elsif record_type
|
||||||
|
run_key_transform(record_type)
|
||||||
|
elsif @static_serializer
|
||||||
|
run_key_transform(@static_serializer.record_type)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'active_support'
|
||||||
require 'active_support/concern'
|
require 'active_support/concern'
|
||||||
|
require 'digest/sha1'
|
||||||
|
|
||||||
module FastJsonapi
|
module FastJsonapi
|
||||||
MandatoryField = Class.new(StandardError)
|
MandatoryField = Class.new(StandardError)
|
||||||
@ -66,15 +68,15 @@ module FastJsonapi
|
|||||||
|
|
||||||
def record_hash(record, fieldset, includes_list, params = {})
|
def record_hash(record, fieldset, includes_list, params = {})
|
||||||
if cache_store_instance
|
if cache_store_instance
|
||||||
record_hash = cache_store_instance.fetch(record, **cache_store_options) do
|
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 = id_hash(id_from_record(record, params), record_type, true)
|
||||||
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
|
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
|
||||||
temp_hash[:relationships] = {}
|
|
||||||
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
|
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
|
||||||
temp_hash[:links] = links_hash(record, params) if data_links.present?
|
temp_hash[:links] = links_hash(record, params) if data_links.present?
|
||||||
temp_hash
|
temp_hash
|
||||||
end
|
end
|
||||||
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
|
record_hash[:relationships] = (record_hash[:relationships] || {}).merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
|
||||||
else
|
else
|
||||||
record_hash = id_hash(id_from_record(record, params), record_type, true)
|
record_hash = id_hash(id_from_record(record, params), record_type, true)
|
||||||
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
|
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
|
||||||
@ -86,6 +88,37 @@ module FastJsonapi
|
|||||||
record_hash
|
record_hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Cache options helper. Use it to adapt cache keys/rules.
|
||||||
|
#
|
||||||
|
# If a fieldset is specified, it modifies the namespace to include the
|
||||||
|
# fields from the fieldset.
|
||||||
|
#
|
||||||
|
# @param options [Hash] default cache options
|
||||||
|
# @param fieldset [Array, nil] passed fieldset values
|
||||||
|
# @param includes_list [Array, nil] passed included values
|
||||||
|
# @param params [Hash] the serializer params
|
||||||
|
#
|
||||||
|
# @return [Hash] processed options hash
|
||||||
|
# rubocop:disable Lint/UnusedMethodArgument
|
||||||
|
def record_cache_options(options, fieldset, includes_list, params)
|
||||||
|
return options unless fieldset
|
||||||
|
|
||||||
|
options = options ? options.dup : {}
|
||||||
|
options[:namespace] ||= 'jsonapi-serializer'
|
||||||
|
|
||||||
|
fieldset_key = fieldset.join('_')
|
||||||
|
|
||||||
|
# Use a fixed-length fieldset key if the current length is more than
|
||||||
|
# the length of a SHA1 digest
|
||||||
|
if fieldset_key.length > 40
|
||||||
|
fieldset_key = Digest::SHA1.hexdigest(fieldset_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
options[:namespace] = "#{options[:namespace]}-fieldset:#{fieldset_key}"
|
||||||
|
options
|
||||||
|
end
|
||||||
|
# rubocop:enable Lint/UnusedMethodArgument
|
||||||
|
|
||||||
def id_from_record(record, params)
|
def id_from_record(record, params)
|
||||||
return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc)
|
return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc)
|
||||||
return record.send(record_id) if record_id
|
return record.send(record_id) if record_id
|
||||||
@ -94,38 +127,54 @@ module FastJsonapi
|
|||||||
record.id
|
record.id
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_include_item(include_item)
|
# It chops out the root association (first part) from each include.
|
||||||
return [include_item.to_sym] unless include_item.to_s.include?('.')
|
#
|
||||||
|
# It keeps an unique list and collects all of the rest of the include
|
||||||
include_item.to_s.split('.').map!(&:to_sym)
|
# 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
|
||||||
|
|
||||||
def remaining_items(items)
|
|
||||||
return unless items.size > 1
|
|
||||||
|
|
||||||
[items[1..-1].join('.').to_sym]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# includes handler
|
# includes handler
|
||||||
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
|
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
|
||||||
return unless includes_list.present?
|
return unless includes_list.present?
|
||||||
|
return [] unless relationships_to_serialize
|
||||||
|
|
||||||
includes_list.sort.each_with_object([]) do |include_item, included_records|
|
includes_list = parse_includes_list(includes_list)
|
||||||
items = parse_include_item(include_item)
|
|
||||||
remaining_items = remaining_items(items)
|
|
||||||
|
|
||||||
items.each do |item|
|
includes_list.each_with_object([]) do |include_item, included_records|
|
||||||
next unless relationships_to_serialize && relationships_to_serialize[item]
|
relationship_item = relationships_to_serialize[include_item.first]
|
||||||
|
|
||||||
relationship_item = relationships_to_serialize[item]
|
next unless relationship_item&.include_relationship?(record, params)
|
||||||
next unless relationship_item.include_relationship?(record, params)
|
|
||||||
|
|
||||||
relationship_type = relationship_item.relationship_type
|
included_objects = Array(relationship_item.fetch_associated_object(record, params))
|
||||||
|
next if included_objects.empty?
|
||||||
included_objects = relationship_item.fetch_associated_object(record, params)
|
|
||||||
next if included_objects.blank?
|
|
||||||
|
|
||||||
included_objects = [included_objects] unless relationship_type == :has_many
|
|
||||||
|
|
||||||
static_serializer = relationship_item.static_serializer
|
static_serializer = relationship_item.static_serializer
|
||||||
static_record_type = relationship_item.static_record_type
|
static_record_type = relationship_item.static_record_type
|
||||||
@ -134,15 +183,15 @@ module FastJsonapi
|
|||||||
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
|
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
|
||||||
record_type = static_record_type || serializer.record_type
|
record_type = static_record_type || serializer.record_type
|
||||||
|
|
||||||
if remaining_items.present?
|
if include_item.last.any?
|
||||||
serializer_records = serializer.get_included_records(inc_obj, remaining_items, known_included_objects, fieldsets, params)
|
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?
|
included_records.concat(serializer_records) unless serializer_records.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
|
code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
|
||||||
next if known_included_objects.key?(code)
|
next if known_included_objects.include?(code)
|
||||||
|
|
||||||
known_included_objects[code] = inc_obj
|
known_included_objects << code
|
||||||
|
|
||||||
included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params)
|
included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params)
|
||||||
end
|
end
|
||||||
@ -150,5 +199,4 @@ module FastJsonapi
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
module FastJsonapi
|
module FastJsonapi
|
||||||
VERSION = '1.7.1'.freeze
|
VERSION = JSONAPI::Serializer::VERSION
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<% module_namespacing do -%>
|
<% module_namespacing do -%>
|
||||||
class <%= class_name %>Serializer
|
class <%= class_name %>Serializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
attributes <%= attributes_names.join(", ") %>
|
attributes <%= attributes_names.join(", ") %>
|
||||||
end
|
end
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
12
lib/jsonapi/serializer.rb
Normal file
12
lib/jsonapi/serializer.rb
Normal 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
|
21
lib/jsonapi/serializer/errors.rb
Normal file
21
lib/jsonapi/serializer/errors.rb
Normal 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
|
28
lib/jsonapi/serializer/instrumentation.rb
Normal file
28
lib/jsonapi/serializer/instrumentation.rb
Normal 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
|
5
lib/jsonapi/serializer/version.rb
Normal file
5
lib/jsonapi/serializer/version.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module JSONAPI
|
||||||
|
module Serializer
|
||||||
|
VERSION = '2.2.0'.freeze
|
||||||
|
end
|
||||||
|
end
|
14
spec/fixtures/_user.rb
vendored
14
spec/fixtures/_user.rb
vendored
@ -1,3 +1,6 @@
|
|||||||
|
require 'active_support'
|
||||||
|
require 'active_support/cache'
|
||||||
|
|
||||||
class User
|
class User
|
||||||
attr_accessor :uid, :first_name, :last_name, :email
|
attr_accessor :uid, :first_name, :last_name, :email
|
||||||
|
|
||||||
@ -15,7 +18,7 @@ class NoSerializerUser < User
|
|||||||
end
|
end
|
||||||
|
|
||||||
class UserSerializer
|
class UserSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
set_id :uid
|
set_id :uid
|
||||||
attributes :first_name, :last_name, :email
|
attributes :first_name, :last_name, :email
|
||||||
@ -26,3 +29,12 @@ class UserSerializer
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
module Cached
|
||||||
|
class UserSerializer < ::UserSerializer
|
||||||
|
cache_options(
|
||||||
|
store: ActiveSupport::Cache::MemoryStore.new,
|
||||||
|
namespace: 'test'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
10
spec/fixtures/actor.rb
vendored
10
spec/fixtures/actor.rb
vendored
@ -1,4 +1,6 @@
|
|||||||
|
require 'active_support'
|
||||||
require 'active_support/cache'
|
require 'active_support/cache'
|
||||||
|
require 'jsonapi/serializer/instrumentation'
|
||||||
|
|
||||||
class Actor < User
|
class Actor < User
|
||||||
attr_accessor :movies, :movie_ids
|
attr_accessor :movies, :movie_ids
|
||||||
@ -33,7 +35,7 @@ class ActorSerializer < UserSerializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
class CamelCaseActorSerializer
|
class CamelCaseActorSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
set_key_transform :camel
|
set_key_transform :camel
|
||||||
|
|
||||||
@ -70,3 +72,9 @@ module Cached
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
module Instrumented
|
||||||
|
class ActorSerializer < ::ActorSerializer
|
||||||
|
include ::JSONAPI::Serializer::Instrumentation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
17
spec/fixtures/movie.rb
vendored
17
spec/fixtures/movie.rb
vendored
@ -3,6 +3,7 @@ class Movie
|
|||||||
:id,
|
:id,
|
||||||
:name,
|
:name,
|
||||||
:year,
|
:year,
|
||||||
|
:actor_or_user,
|
||||||
:actors,
|
:actors,
|
||||||
:actor_ids,
|
:actor_ids,
|
||||||
:polymorphics,
|
:polymorphics,
|
||||||
@ -25,7 +26,7 @@ class Movie
|
|||||||
@url ||= FFaker::Internet.http_url
|
@url ||= FFaker::Internet.http_url
|
||||||
return @url if obj.nil?
|
return @url if obj.nil?
|
||||||
|
|
||||||
@url + '?' + obj.hash.to_s
|
"#{@url}?#{obj.hash}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def owner=(ownr)
|
def owner=(ownr)
|
||||||
@ -43,20 +44,30 @@ class Movie
|
|||||||
end
|
end
|
||||||
|
|
||||||
class MovieSerializer
|
class MovieSerializer
|
||||||
include FastJsonapi::ObjectSerializer
|
include JSONAPI::Serializer
|
||||||
|
|
||||||
set_type :movie
|
set_type :movie
|
||||||
|
|
||||||
|
attribute :released_in_year, &:year
|
||||||
attributes :name
|
attributes :name
|
||||||
attribute :release_year do |object|
|
attribute :release_year do |object, _params|
|
||||||
object.year
|
object.year
|
||||||
end
|
end
|
||||||
|
|
||||||
link :self, :url
|
link :self, :url
|
||||||
|
|
||||||
belongs_to :owner, serializer: UserSerializer
|
belongs_to :owner, serializer: UserSerializer
|
||||||
|
|
||||||
|
belongs_to :actor_or_user,
|
||||||
|
id_method_name: :uid,
|
||||||
|
polymorphic: {
|
||||||
|
Actor => :actor,
|
||||||
|
User => :user
|
||||||
|
}
|
||||||
|
|
||||||
has_many(
|
has_many(
|
||||||
:actors,
|
:actors,
|
||||||
|
meta: proc { |record, _| { count: record.actors.length } },
|
||||||
links: {
|
links: {
|
||||||
actors_self: :url,
|
actors_self: :url,
|
||||||
related: ->(obj) { obj.url(obj) }
|
related: ->(obj) { obj.url(obj) }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:actor) do
|
let(:actor) do
|
||||||
act = Actor.fake
|
act = Actor.fake
|
||||||
act.movies = [Movie.fake]
|
act.movies = [Movie.fake]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:actor) do
|
let(:actor) do
|
||||||
faked = Actor.fake
|
faked = Actor.fake
|
||||||
movie = Movie.fake
|
movie = Movie.fake
|
||||||
@ -25,5 +25,56 @@ RSpec.describe FastJsonapi::ObjectSerializer do
|
|||||||
cache_store.delete(actor.movies[0].owner, namespace: 'test')
|
cache_store.delete(actor.movies[0].owner, namespace: 'test')
|
||||||
).to be(false)
|
).to be(false)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:actor) { Actor.fake }
|
let(:actor) { Actor.fake }
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ RSpec.describe FastJsonapi::ObjectSerializer do
|
|||||||
it do
|
it do
|
||||||
expect { ActorSerializer.new(actor, include: ['bad_include']) }
|
expect { ActorSerializer.new(actor, include: ['bad_include']) }
|
||||||
.to raise_error(
|
.to raise_error(
|
||||||
ArgumentError, /bad_include is not specified as a relationship/
|
JSONAPI::Serializer::UnsupportedIncludeError, /bad_include is not specified as a relationship/
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
29
spec/integration/instrumentation_spec.rb
Normal file
29
spec/integration/instrumentation_spec.rb
Normal 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
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:actor) { Actor.fake }
|
let(:actor) { Actor.fake }
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
let(:serialized) do
|
let(:serialized) do
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:movie) do
|
let(:movie) do
|
||||||
faked = Movie.fake
|
faked = Movie.fake
|
||||||
faked.actors = [Actor.fake]
|
faked.actors = [Actor.fake]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:user) { User.fake }
|
let(:user) { User.fake }
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
let(:serialized) do
|
let(:serialized) do
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe FastJsonapi::ObjectSerializer do
|
RSpec.describe JSONAPI::Serializer do
|
||||||
let(:movie) do
|
let(:movie) do
|
||||||
mov = Movie.fake
|
mov = Movie.fake
|
||||||
mov.actors = rand(2..5).times.map { Actor.fake }
|
mov.actors = rand(2..5).times.map { Actor.fake }
|
||||||
@ -8,6 +8,7 @@ RSpec.describe FastJsonapi::ObjectSerializer do
|
|||||||
poly_act = Actor.fake
|
poly_act = Actor.fake
|
||||||
poly_act.movies = [Movie.fake]
|
poly_act.movies = [Movie.fake]
|
||||||
mov.polymorphics = [User.fake, poly_act]
|
mov.polymorphics = [User.fake, poly_act]
|
||||||
|
mov.actor_or_user = Actor.fake
|
||||||
mov
|
mov
|
||||||
end
|
end
|
||||||
let(:params) { {} }
|
let(:params) { {} }
|
||||||
@ -58,6 +59,13 @@ RSpec.describe FastJsonapi::ObjectSerializer do
|
|||||||
)
|
)
|
||||||
end
|
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
|
context 'with include' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ include: [:actors] }
|
{ include: [:actors] }
|
||||||
@ -93,7 +101,7 @@ RSpec.describe FastJsonapi::ObjectSerializer do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with polymorphic' do
|
context 'with has_many polymorphic' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ include: ['actors_and_users.played_movies'] }
|
{ include: ['actors_and_users.played_movies'] }
|
||||||
end
|
end
|
||||||
@ -121,6 +129,18 @@ RSpec.describe FastJsonapi::ObjectSerializer do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,8 +6,9 @@ SimpleCov.start do
|
|||||||
end
|
end
|
||||||
SimpleCov.minimum_coverage 90
|
SimpleCov.minimum_coverage 90
|
||||||
|
|
||||||
|
require 'active_support'
|
||||||
require 'active_support/core_ext/object/json'
|
require 'active_support/core_ext/object/json'
|
||||||
require 'fast_jsonapi'
|
require 'jsonapi/serializer'
|
||||||
require 'ffaker'
|
require 'ffaker'
|
||||||
require 'rspec'
|
require 'rspec'
|
||||||
require 'jsonapi/rspec'
|
require 'jsonapi/rspec'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user