Compare commits

..

47 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
40 changed files with 631 additions and 277 deletions

View File

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

7
.gitignore vendored
View File

@ -40,6 +40,9 @@ test.db
# For those who install gems locally to a vendor dir # For those who install gems locally to a vendor dir
/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

View File

@ -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'

View File

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

View File

@ -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'

View File

@ -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
View File

@ -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.

View File

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

View File

@ -23,7 +23,7 @@ cases.
We came up with patterns that we can rely upon such as: We came up with patterns that we can rely upon such as:
* We always use [JSON:API](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)

View File

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

View File

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

View File

@ -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)
proc.call(*params.take(proc.parameters.length)) # 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 end
end end

View File

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

View File

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

View File

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

View File

@ -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.')

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

View File

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

View File

@ -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
def is_collection?(resource, force_is_collection = nil)
return force_is_collection unless force_is_collection.nil?
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
end
class_methods do 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) 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

View File

@ -1,6 +1,6 @@
module FastJsonapi module FastJsonapi
class Relationship class Relationship
attr_reader :owner, :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :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

View File

@ -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,58 +127,73 @@ 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.
end #
# This method will turn that include array into a Hash that looks like:
def remaining_items(items) #
return unless items.size > 1 # {
# authors: Set.new([
[items[1..-1].join('.').to_sym] # '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 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) static_serializer = relationship_item.static_serializer
next if included_objects.blank? static_record_type = relationship_item.static_record_type
included_objects = [included_objects] unless relationship_type == :has_many included_objects.each do |inc_obj|
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type
static_serializer = relationship_item.static_serializer if include_item.last.any?
static_record_type = relationship_item.static_record_type 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_objects.each do |inc_obj|
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type
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[record_type], includes_list, params)
end end
code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}"
next if known_included_objects.include?(code)
known_included_objects << code
included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params)
end end
end end
end end

View File

@ -1,3 +1,3 @@
module FastJsonapi module FastJsonapi
VERSION = '1.7.1'.freeze VERSION = JSONAPI::Serializer::VERSION
end end

View File

@ -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
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,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

View File

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

View File

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

View File

@ -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]

View File

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

View File

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

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

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

View File

@ -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]

View File

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

View File

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

View File

@ -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'