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:
tests:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
strategy:
matrix:
ruby: [2.4, 2.5, 2.6, 2.7]
ruby: [2.4, 2.7, '3.0', 3.1, truffleruby-head]
steps:
- uses: actions/checkout@master
- name: Sets up the Ruby version
uses: actions/setup-ruby@v1
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
@ -26,16 +26,15 @@ jobs:
- name: Runs code QA and tests
run: bundle exec rake
- name: Publish to GPR
- name: Publish to Rubygems
continue-on-error: true
if: ${{ github.ref == 'refs/heads/master' }}
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n: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 push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
gem push *.gem
env:
GEM_HOST_API_KEY: ${{secrets.GPR_AUTH_TOKEN}}
OWNER: fast-jsonapi
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}

7
.gitignore vendored
View File

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

View File

@ -2,6 +2,10 @@ require:
- rubocop-performance
- rubocop-rspec
AllCops:
NewCops: enable
SuggestExtensions: false
Style/FrozenStringLiteralComment:
Enabled: false
@ -38,6 +42,9 @@ Performance/TimesMap:
Exclude:
- 'spec/**/**.rb'
Gemspec/RequiredRubyVersion:
Enabled: false
# TODO: Fix these...
Style/Documentation:
Enabled: false
@ -76,3 +83,20 @@ Naming/PredicateName:
Naming/AccessorMethodName:
Exclude:
- 'lib/**/**.rb'
Style/CaseLikeIf:
Exclude:
- 'lib/fast_jsonapi/object_serializer.rb'
Style/OptionalBooleanParameter:
Exclude:
- 'lib/fast_jsonapi/serialization_core.rb'
- 'lib/fast_jsonapi/relationship.rb'
Lint/DuplicateBranch:
Exclude:
- 'lib/fast_jsonapi/relationship.rb'
Style/DocumentDynamicEvalDefinition:
Exclude:
- 'lib/extensions/has_one.rb'

View File

@ -4,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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- ...
## [2.2.0] - 2021-03-11
### Added
- Proper error is raised on unsupported includes (#125)
### Changed
- Documentation updates (#137 #139 #143 #146)
### Fixed
- Empty relationships are no longer added to serialized doc (#116)
- Ruby v3 compatibility (#160)
## [2.1.0] - 2020-08-30
### Added
- Optional meta field to relationships (#99 #100)
- Support for `params` on cache keys (#117)
### Changed
- Performance instrumentation (#110 #39)
- Improved collection detection (#112)
### Fixed
- Ensure caching correctly incorporates fieldset information into the cache key to prevent incorrect fieldset caching (#90)
- Performance optimizations for nested includes (#103)
## [2.0.0] - 2020-06-22
The project was renamed to `jsonapi-serializer`! (#94)
### Changed
- Remove `ObjectSerializer#serialized_json` (#91)
## [1.7.2] - 2020-05-18
### Fixed
- Relationship#record_type_for does not assign static record type for polymorphic relationships (#83)
## [1.7.1] - 2020-05-01
### Fixed
- ObjectSerializer#serialized_json accepts arguments for to_json (#80)

View File

@ -2,6 +2,3 @@ source 'https://rubygems.org'
# Specify your gem's dependencies in fast_jsonapi.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
Version 2.0, January 2004
http://www.apache.org/licenses/
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@ -192,7 +192,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

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
We compare serialization times with `ActiveModelSerializer` and alternative
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
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)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods)
* [Performance Instrumentation](#performance-instrumentation)
* [Deserialization](#deserialization)
* [Migrating from Netflix/fast_jsonapi](#migrating-from-netflixfast_jsonapi)
* [Contributing](#contributing)
@ -45,14 +60,10 @@ article in the `docs` folder for any questions related to methodology.
## Installation
The `fast_jsonapi` is hosted on Github Package Registry and not on Rubygems.
Add the GPR source and the gem to your Gemfile:
Add this line to your application's Gemfile:
```ruby
source 'https://rubygems.pkg.github.com/fast-jsonapi'
gem 'fast_jsonapi'
gem 'jsonapi-serializer'
```
Execute:
@ -83,7 +94,8 @@ end
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_type :movie # optional
set_id :owner_id # optional
attributes :name, :year
@ -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
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel
end
@ -184,13 +200,13 @@ set_key_transform :underscore # "some_key" => "some_key"
```
### Attributes
Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
Attributes are defined using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute.
By default, attributes are read directly from the model property of the same name. In this example, `name` is expected to be a property of the object being serialized:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attribute :name
end
@ -200,7 +216,7 @@ Custom attributes that must be serialized but do not exist on the model can be d
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name, :year
@ -214,7 +230,7 @@ The block syntax can also be used to override the property on the object:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attribute :name do |object|
"#{object.name} Part 2"
@ -226,7 +242,7 @@ Attributes can also use a different name by passing the original method or acces
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name
@ -235,7 +251,7 @@ end
```
### Links Per Object
Links are defined in FastJsonapi using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.
Links are defined using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.
You can configure the method to use on the object for example a link with key `self` will get set to the value returned by a method called `url` on the movie object.
@ -243,29 +259,29 @@ You can also use a block to define a url as shown in `custom_url`. You can acces
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
link :public_url
link :self, :url
link :custom_url do |object|
"http://movies.com/#{object.name}-(#{object.year})"
"https://movies.com/#{object.name}-(#{object.year})"
end
link :personalized_url do |object, params|
"http://movies.com/#{object.name}-#{params[:user].reference_code}"
"https://movies.com/#{object.name}-#{params[:user].reference_code}"
end
end
```
#### Links on a Relationship
You can specify [relationship links](http://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](http://jsonapi.org/format/#document-resource-object-related-resource-links))
You can specify [relationship links](https://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](https://jsonapi.org/format/#document-resource-object-related-resource-links))
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
has_many :actors, links: {
self: :url,
@ -300,7 +316,7 @@ For every resource in the collection, you can include a meta object containing n
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
meta do |movie|
{
@ -310,6 +326,23 @@ class MovieSerializer
end
```
#### Meta on a Relationship
You can specify [relationship meta](https://jsonapi.org/format/#document-resource-object-relationships) by using the `meta:` option on the serializer. Relationship meta in JSON API is useful if you wish to provide non-standard meta-information about the relationship.
Meta can be defined either by passing a static hash or by using Proc to the `meta` key. In the latter case, the record and any params passed to the serializer are available inside the Proc as the first and second parameters, respectively.
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, meta: Proc.new do |movie_record, params|
{ count: movie_record.actors.length }
end
end
```
### Compound Document
Support for top-level and nested included associations through `options[:include]`.
@ -365,10 +398,10 @@ To enable caching, use `cache_options store: <cache_store>`:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# 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
```
@ -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
- `&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
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
In some cases, attribute values might require more information than what is
@ -399,7 +451,7 @@ parameter.
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_id do |movie, params|
# in here, params is a hash containing the `:admin` key
@ -414,7 +466,7 @@ class MovieSerializer
belongs_to :primary_agent do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user].is_employee? ? true : false
params[:current_user]
end
end
@ -433,7 +485,7 @@ Conditional attributes can be defined by passing a Proc to the `if` key on the `
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name, :year
attribute :release_year, if: Proc.new { |record|
@ -445,6 +497,13 @@ class MovieSerializer
# The director will be serialized only if the :admin key of params is true
params && params[:admin] == true
}
# Custom attribute `name_year` will only be serialized if both `name` and `year` fields are present
attribute :name_year, if: Proc.new { |record|
record.name.present? && record.year.present?
} do |object|
"#{object.name} - #{object.year}"
end
end
# ...
@ -459,7 +518,7 @@ Conditional relationships can be defined by passing a Proc to the `if` key. Retu
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# Actors will only be serialized if the record has any associated actors
has_many :actors, if: Proc.new { |record| record.actors.any? }
@ -480,7 +539,7 @@ In many cases, the relationship can automatically detect the serializer to use.
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# resolves to StudioSerializer
belongs_to :studio
@ -493,7 +552,7 @@ At other times, such as when a property name differs from the class name, you ma
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# resolves to MovieStudioSerializer
belongs_to :studio, serializer: :movie_studio
@ -506,7 +565,7 @@ For more advanced cases, such as polymorphic relationships and Single Table Inhe
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
has_many :actors, serializer: Proc.new do |record, params|
if record.comedian?
@ -526,7 +585,7 @@ Attributes and relationships can be selectively returned per record type by usin
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
attributes :name, :year
end
@ -557,7 +616,7 @@ module AvatarHelper
end
class UserSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
include AvatarHelper # mixes in your helper method as class method
@ -582,7 +641,7 @@ module AvatarHelper
end
class UserSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
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 | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
### Instrumentation
### Performance Instrumentation
`fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer:
Performance instrumentation is available by using the
`active_support/notifications`.
To enable it, include the module in your serializer class:
```ruby
require 'fast_jsonapi/instrumentation/skylight'
require 'jsonapi/serializer'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
```
Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:
```ruby
require 'fast_jsonapi/instrumentation'
```
The two instrumented notifications are supplied by these two constants:
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`
It is also possible to instrument one method without the other by using one of the following require statements:
```ruby
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'
```
Same goes for the Skylight integration:
```ruby
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
```
[Skylight](https://www.skylight.io/) integration is also available and
supported by us, follow the Skylight documentation to enable it.
### Running Tests
The project has and requires unit tests, functional tests and performance
@ -651,6 +701,61 @@ tests. To run tests use the following command:
rspec
```
## Deserialization
We currently do not support deserialization, but we recommend to use any of the next gems:
### [JSONAPI.rb](https://github.com/stas/jsonapi.rb)
This gem provides the next features alongside deserialization:
- Collection meta
- Error handling
- Includes and sparse fields
- Filtering and sorting
- Pagination
## Migrating from Netflix/fast_jsonapi
If you come from [Netflix/fast_jsonapi](https://github.com/Netflix/fast_jsonapi), here is the instructions to switch.
### Modify your Gemfile
```diff
- gem 'fast_jsonapi'
+ gem 'jsonapi-serializer'
```
### Replace all constant references
```diff
class MovieSerializer
- include FastJsonapi::ObjectSerializer
+ include JSONAPI::Serializer
end
```
### Replace removed methods
```diff
- json_string = MovieSerializer.new(movie).serialized_json
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
### Replace require references
```diff
- require 'fast_jsonapi'
+ require 'jsonapi/serializer'
```
### Update your cache options
See [docs](https://github.com/jsonapi-serializer/jsonapi-serializer#caching).
```diff
- cache_options enabled: true, cache_length: 12.hours
+ cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
```
## Contributing
Please follow the instructions we provide as part of the issue and
@ -658,4 +763,4 @@ pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and
contributors are expected to adhere to the
[Contributor Covenant](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'
class BaseSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
def to_json
Oj.dump(serializable_hash)

View File

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

View File

@ -1,25 +1,19 @@
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'fast_jsonapi/version'
require 'jsonapi/serializer/version'
Gem::Specification.new do |gem|
gem.name = 'fast_jsonapi'
gem.version = FastJsonapi::VERSION
gem.name = 'jsonapi-serializer'
gem.version = JSONAPI::Serializer::VERSION
gem.authors = [
'Shishir Kakaraddi',
'Srinivas Raghunathan',
'Adam Gross',
'github/fast-jsonapi community'
]
gem.authors = ['JSON:API Serializer Community']
gem.email = ''
gem.summary = 'Fast JSON:API (jsonapi.org) serialization library'
gem.description =
'Fast JSON:API (jsonapi.org) serialization library ' \
'to work with any kind of objects'
gem.homepage = 'http://github.com/fast-jsonapi/fast_jsonapi'
gem.summary = 'Fast JSON:API serialization library'
gem.description = 'Fast, simple and easy to use '\
'JSON:API serialization library (also known as fast_jsonapi).'
gem.homepage = 'https://github.com/jsonapi-serializer/jsonapi-serializer'
gem.licenses = ['Apache-2.0']
gem.files = Dir['lib/**/*']
gem.require_paths = ['lib']
@ -31,7 +25,7 @@ Gem::Specification.new do |gem|
gem.add_development_dependency('bundler')
gem.add_development_dependency('byebug')
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('rspec')
gem.add_development_dependency('rubocop')
@ -39,4 +33,5 @@ Gem::Specification.new do |gem|
gem.add_development_dependency('rubocop-rspec')
gem.add_development_dependency('simplecov')
gem.add_development_dependency('sqlite3')
gem.metadata['rubygems_mfa_required'] = 'true'
end

View File

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

View File

@ -6,7 +6,16 @@ module FastJsonapi
# @param [Array<Object>] *params any number of parameters to be passed to the Proc
# @return [Object] the result of the Proc call with the supplied parameters
def call_proc(proc, *params)
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

View File

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

View File

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

View File

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

View File

@ -1,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
require 'active_support'
require 'active_support/time'
require 'active_support/concern'
require 'active_support/inflector'
@ -15,8 +16,6 @@ module FastJsonapi
extend ActiveSupport::Concern
include SerializationCore
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
TRANSFORMS_MAPPING = {
camel: :camelize,
camel_lower: [:camelize, :lower],
@ -36,7 +35,9 @@ module FastJsonapi
end
def serializable_hash
return hash_for_collection if is_collection?(@resource, @is_collection)
if self.class.is_collection?(@resource, @is_collection)
return hash_for_collection
end
hash_for_one_record
end
@ -72,15 +73,6 @@ module FastJsonapi
serializable_hash
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
def process_options(options)
@ -89,7 +81,7 @@ module FastJsonapi
return if options.blank?
@known_included_objects = {}
@known_included_objects = Set.new
@meta = options[:meta]
@links = options[:links]
@is_collection = options[:is_collection]
@ -114,13 +106,16 @@ module FastJsonapi
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
# Detects a collection/enumerable
#
# @return [TrueClass] on a successful detection
def is_collection?(resource, force_is_collection = nil)
return force_is_collection unless force_is_collection.nil?
resource.is_a?(Enumerable) && !resource.respond_to?(:each_pair)
end
def inherited(subclass)
super(subclass)
subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
@ -139,9 +134,7 @@ module FastJsonapi
def reflected_record_type
return @reflected_record_type if defined?(@reflected_record_type)
@reflected_record_type ||= begin
name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer')
end
@reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer'))
end
def set_key_transform(transform_name)
@ -234,10 +227,10 @@ module FastJsonapi
# TODO: Remove this undocumented option.
# Delegate the caching to the serializer exclusively.
if !relationship.cached
uncachable_relationships_to_serialize[relationship.name] = relationship
else
if relationship.cached
cachable_relationships_to_serialize[relationship.name] = relationship
else
uncachable_relationships_to_serialize[relationship.name] = relationship
end
relationships_to_serialize[relationship.name] = relationship
end
@ -292,6 +285,7 @@ module FastJsonapi
polymorphic: polymorphic,
conditional_proc: options[:if],
transform_method: @transform_method,
meta: options[:meta],
links: options[:links],
lazy_load_data: options[:lazy_load_data]
)
@ -307,7 +301,7 @@ module FastJsonapi
def serializer_for(name)
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
begin
serializer_class_name.constantize
@ -345,21 +339,11 @@ module FastJsonapi
def validate_includes!(includes)
return if includes.blank?
includes.each do |include_item|
klass = self
parse_include_item(include_item).each do |parsed_include|
relationships_to_serialize = klass.relationships_to_serialize || {}
relationship_to_include = relationships_to_serialize[parsed_include]
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
parse_includes_list(includes).each_key do |include_item|
relationship_to_include = relationships_to_serialize[include_item]
raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_include
if relationship_to_include.static_serializer
klass = relationship_to_include.static_serializer
else
# the serializer may change based on the object (e.g. polymorphic relationships),
# so inner relationships cannot be validated
break
end
end
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
end
end
end

View File

@ -1,6 +1,6 @@
module FastJsonapi
class Relationship
attr_reader :owner, :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :lazy_load_data
attr_reader :owner, :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :meta, :lazy_load_data
def initialize(
owner:,
@ -12,11 +12,12 @@ module FastJsonapi
object_block:,
serializer:,
relationship_type:,
cached: false,
polymorphic:,
conditional_proc:,
transform_method:,
links:,
meta:,
cached: false,
lazy_load_data: false
)
@owner = owner
@ -33,6 +34,7 @@ module FastJsonapi
@conditional_proc = conditional_proc
@transform_method = transform_method
@links = links || {}
@meta = meta || {}
@lazy_load_data = lazy_load_data
@record_types_for = {}
@serializers_for_name = {}
@ -44,6 +46,8 @@ module FastJsonapi
output_hash[key] = {}
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case unless lazy_load_data && !included
add_meta_hash(record, serialization_params, output_hash) if meta.present?
add_links_hash(record, serialization_params, output_hash) if links.present?
end
end
@ -146,11 +150,19 @@ module FastJsonapi
record.public_send(links)
else
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
def add_meta_hash(record, params, output_hash)
output_hash[key][:meta] = if meta.is_a?(Proc)
FastJsonapi.call_proc(meta, record, params)
else
meta
end
end
def run_key_transform(input)
if transform_method.present?
input.to_s.send(*transform_method).to_sym
@ -212,8 +224,13 @@ module FastJsonapi
end
def compute_static_record_type
return run_key_transform(record_type) if record_type
return run_key_transform(@static_serializer.record_type) if @static_serializer
if polymorphic
nil
elsif record_type
run_key_transform(record_type)
elsif @static_serializer
run_key_transform(@static_serializer.record_type)
end
end
end
end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
require 'active_support'
require 'active_support/concern'
require 'digest/sha1'
module FastJsonapi
MandatoryField = Class.new(StandardError)
@ -66,15 +68,15 @@ module FastJsonapi
def record_hash(record, fieldset, includes_list, params = {})
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[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
temp_hash[:relationships] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
temp_hash[:links] = links_hash(record, params) if data_links.present?
temp_hash
end
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
record_hash[:relationships] = (record_hash[:relationships] || {}).merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
else
record_hash = id_hash(id_from_record(record, params), record_type, true)
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
@ -86,6 +88,37 @@ module FastJsonapi
record_hash
end
# Cache options helper. Use it to adapt cache keys/rules.
#
# If a fieldset is specified, it modifies the namespace to include the
# fields from the fieldset.
#
# @param options [Hash] default cache options
# @param fieldset [Array, nil] passed fieldset values
# @param includes_list [Array, nil] passed included values
# @param params [Hash] the serializer params
#
# @return [Hash] processed options hash
# rubocop:disable Lint/UnusedMethodArgument
def record_cache_options(options, fieldset, includes_list, params)
return options unless fieldset
options = options ? options.dup : {}
options[:namespace] ||= 'jsonapi-serializer'
fieldset_key = fieldset.join('_')
# Use a fixed-length fieldset key if the current length is more than
# the length of a SHA1 digest
if fieldset_key.length > 40
fieldset_key = Digest::SHA1.hexdigest(fieldset_key)
end
options[:namespace] = "#{options[:namespace]}-fieldset:#{fieldset_key}"
options
end
# rubocop:enable Lint/UnusedMethodArgument
def id_from_record(record, params)
return FastJsonapi.call_proc(record_id, record, params) if record_id.is_a?(Proc)
return record.send(record_id) if record_id
@ -94,58 +127,73 @@ module FastJsonapi
record.id
end
def parse_include_item(include_item)
return [include_item.to_sym] unless include_item.to_s.include?('.')
include_item.to_s.split('.').map!(&:to_sym)
end
def remaining_items(items)
return unless items.size > 1
[items[1..-1].join('.').to_sym]
# It chops out the root association (first part) from each include.
#
# It keeps an unique list and collects all of the rest of the include
# value to hand it off to the next related to include serializer.
#
# This method will turn that include array into a Hash that looks like:
#
# {
# authors: Set.new([
# 'books',
# 'books.genre',
# 'books.genre.books',
# 'books.genre.books.authors',
# 'books.genre.books.genre'
# ]),
# genre: Set.new(['books'])
# }
#
# Because the serializer only cares about the root associations
# included, it only needs the first segment of each include
# (for books, it's the "authors" and "genre") and it doesn't need to
# waste cycles parsing the rest of the include value. That will be done
# by the next serializer in line.
#
# @param includes_list [List] to be parsed
# @return [Hash]
def parse_includes_list(includes_list)
includes_list.each_with_object({}) do |include_item, include_sets|
include_base, include_remainder = include_item.to_s.split('.', 2)
include_sets[include_base.to_sym] ||= Set.new
include_sets[include_base.to_sym] << include_remainder if include_remainder
end
end
# includes handler
def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {})
return unless includes_list.present?
return [] unless relationships_to_serialize
includes_list.sort.each_with_object([]) do |include_item, included_records|
items = parse_include_item(include_item)
remaining_items = remaining_items(items)
includes_list = parse_includes_list(includes_list)
items.each do |item|
next unless relationships_to_serialize && relationships_to_serialize[item]
includes_list.each_with_object([]) do |include_item, included_records|
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?
static_serializer = relationship_item.static_serializer
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
static_record_type = relationship_item.static_record_type
included_objects.each do |inc_obj|
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type
if 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)
if include_item.last.any?
serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end
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

View File

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

View File

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

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

@ -0,0 +1,12 @@
require 'fast_jsonapi'
module JSONAPI
module Serializer
# TODO: Move and cleanup the old implementation...
def self.included(base)
base.class_eval do
include FastJsonapi::ObjectSerializer
end
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module JSONAPI
module Serializer
class Error < StandardError; end
class UnsupportedIncludeError < Error
attr_reader :include_item, :klass
def initialize(include_item, klass)
super()
@include_item = include_item
@klass = klass
end
def message
"#{include_item} is not specified as a relationship on #{klass}"
end
end
end
end

View File

@ -0,0 +1,28 @@
require 'active_support'
require 'active_support/notifications'
module JSONAPI
module Serializer
# Support for instrumentation
module Instrumentation
# Performance instrumentation namespace
NOTIFICATION_NAMESPACE = 'render.jsonapi-serializer.'.freeze
# Patch methods to use instrumentation...
%w[
serializable_hash
get_included_records
relationships_hash
].each do |method_name|
define_method(method_name) do |*args|
ActiveSupport::Notifications.instrument(
NOTIFICATION_NAMESPACE + method_name,
{ name: self.class.name, serializer: self.class }
) do
super(*args)
end
end
end
end
end
end

View File

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

View File

@ -1,3 +1,6 @@
require 'active_support'
require 'active_support/cache'
class User
attr_accessor :uid, :first_name, :last_name, :email
@ -15,7 +18,7 @@ class NoSerializerUser < User
end
class UserSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_id :uid
attributes :first_name, :last_name, :email
@ -26,3 +29,12 @@ class UserSerializer
}
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 'jsonapi/serializer/instrumentation'
class Actor < User
attr_accessor :movies, :movie_ids
@ -33,7 +35,7 @@ class ActorSerializer < UserSerializer
end
class CamelCaseActorSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_key_transform :camel
@ -70,3 +72,9 @@ module Cached
)
end
end
module Instrumented
class ActorSerializer < ::ActorSerializer
include ::JSONAPI::Serializer::Instrumentation
end
end

View File

@ -3,6 +3,7 @@ class Movie
:id,
:name,
:year,
:actor_or_user,
:actors,
:actor_ids,
:polymorphics,
@ -25,7 +26,7 @@ class Movie
@url ||= FFaker::Internet.http_url
return @url if obj.nil?
@url + '?' + obj.hash.to_s
"#{@url}?#{obj.hash}"
end
def owner=(ownr)
@ -43,20 +44,30 @@ class Movie
end
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
set_type :movie
attribute :released_in_year, &:year
attributes :name
attribute :release_year do |object|
attribute :release_year do |object, _params|
object.year
end
link :self, :url
belongs_to :owner, serializer: UserSerializer
belongs_to :actor_or_user,
id_method_name: :uid,
polymorphic: {
Actor => :actor,
User => :user
}
has_many(
:actors,
meta: proc { |record, _| { count: record.actors.length } },
links: {
actors_self: :url,
related: ->(obj) { obj.url(obj) }

View File

@ -1,6 +1,6 @@
require 'spec_helper'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:actor) do
act = Actor.fake
act.movies = [Movie.fake]

View File

@ -1,6 +1,6 @@
require 'spec_helper'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:actor) do
faked = Actor.fake
movie = Movie.fake
@ -25,5 +25,56 @@ RSpec.describe FastJsonapi::ObjectSerializer do
cache_store.delete(actor.movies[0].owner, namespace: 'test')
).to be(false)
end
context 'without relationships' do
let(:user) { User.fake }
let(:serialized) { Cached::UserSerializer.new(user).serializable_hash.as_json }
it do
expect(serialized['data']).not_to have_key('relationships')
end
end
end
describe 'with caching and different fieldsets' do
context 'when fieldset is provided' do
it 'includes the fieldset in the namespace' do
expect(cache_store.delete(actor, namespace: 'test')).to be(false)
Cached::ActorSerializer.new(
[actor], fields: { actor: %i[first_name] }
).serializable_hash
# Expect cached keys to match the passed fieldset
expect(cache_store.read(actor, namespace: 'test-fieldset:first_name')[:attributes].keys).to eq(%i[first_name])
Cached::ActorSerializer.new(
[actor]
).serializable_hash
# Expect cached keys to match all valid actor fields (no fieldset)
expect(cache_store.read(actor, namespace: 'test')[:attributes].keys).to eq(%i[first_name last_name email])
expect(cache_store.delete(actor, namespace: 'test')).to be(true)
expect(cache_store.delete(actor, namespace: 'test-fieldset:first_name')).to be(true)
end
end
context 'when long fieldset is provided' do
let(:actor_keys) { %i[first_name last_name more_fields yet_more_fields so_very_many_fields] }
let(:digest_key) { Digest::SHA1.hexdigest(actor_keys.join('_')) }
it 'includes the hashed fieldset in the namespace' do
Cached::ActorSerializer.new(
[actor], fields: { actor: actor_keys }
).serializable_hash
expect(cache_store.read(actor, namespace: "test-fieldset:#{digest_key}")[:attributes].keys).to eq(
%i[first_name last_name]
)
expect(cache_store.delete(actor, namespace: "test-fieldset:#{digest_key}")).to be(true)
end
end
end
end

View File

@ -1,6 +1,6 @@
require 'spec_helper'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:actor) { Actor.fake }
let(:params) { {} }
@ -18,7 +18,7 @@ RSpec.describe FastJsonapi::ObjectSerializer do
it do
expect { ActorSerializer.new(actor, include: ['bad_include']) }
.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

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'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:actor) { Actor.fake }
let(:params) { {} }
let(:serialized) do

View File

@ -1,6 +1,6 @@
require 'spec_helper'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:movie) do
faked = Movie.fake
faked.actors = [Actor.fake]

View File

@ -1,6 +1,6 @@
require 'spec_helper'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:user) { User.fake }
let(:params) { {} }
let(:serialized) do

View File

@ -1,6 +1,6 @@
require 'spec_helper'
RSpec.describe FastJsonapi::ObjectSerializer do
RSpec.describe JSONAPI::Serializer do
let(:movie) do
mov = Movie.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.movies = [Movie.fake]
mov.polymorphics = [User.fake, poly_act]
mov.actor_or_user = Actor.fake
mov
end
let(:params) { {} }
@ -58,6 +59,13 @@ RSpec.describe FastJsonapi::ObjectSerializer do
)
end
describe 'has relationship meta' do
it do
expect(serialized['data']['relationships']['actors'])
.to have_meta('count' => movie.actors.length)
end
end
context 'with include' do
let(:params) do
{ include: [:actors] }
@ -93,7 +101,7 @@ RSpec.describe FastJsonapi::ObjectSerializer do
end
end
context 'with polymorphic' do
context 'with has_many polymorphic' do
let(:params) do
{ include: ['actors_and_users.played_movies'] }
end
@ -121,6 +129,18 @@ RSpec.describe FastJsonapi::ObjectSerializer do
)
end
end
context 'with belongs_to polymorphic' do
let(:params) do
{ include: ['actor_or_user'] }
end
it do
expect(serialized['included']).to include(
have_type('actor').and(have_id(movie.actor_or_user.uid))
)
end
end
end
end
end

View File

@ -6,8 +6,9 @@ SimpleCov.start do
end
SimpleCov.minimum_coverage 90
require 'active_support'
require 'active_support/core_ext/object/json'
require 'fast_jsonapi'
require 'jsonapi/serializer'
require 'ffaker'
require 'rspec'
require 'jsonapi/rspec'