Compare commits

...

292 Commits

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

* Fix rubocop complaints

* Remove ruby 3 from CI actions

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

which would probably look something like this 

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

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

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

* Fix Rubocop's warnings.

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

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

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

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

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

* fixes

* specifically test the relationship name and the resource type

* support object blocks without a serializer defined

* stop validation gracefully if the relationship is polymorphic

* improve performance by using instance variables instead of accessor methods

* force initialization up front to avoid the iterative calls

* serializer procs should act like polymorphic when determining the id_method_name

* specs for serializer procs

* updated with more details and examples for relationship serializer options

* adjust specs to define the serializers

* avoid extra method calls for performance

* name change

* one less function call for better performance

* do not require lazy loaded relationships to resolve the serializer

* give polymorphic precedence for backwards compatibility

* move serializer inference into ObjectSerializer to allow for overriding

* move method for better diff

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

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

* Update README.md
2019-11-20 12:05:04 +00:00
Kevin Pheasey
6a08165347
Merge pull request #18 from fast-jsonapi/dev
1.6.0
2019-11-04 17:38:13 -05:00
Kevin Pheasey
1cd3f97485 1.6.0 2019-11-04 17:31:35 -05:00
Kevin Pheasey
8fe376c8a3
Update CHANGELOG.md 2019-10-25 09:36:02 -04:00
Brad Grzesiak
ddc261d8dc Allow "if: Proc..." on link (#15) 2019-10-25 09:32:56 -04:00
Henning Vogt
587fb2c5fe Add params to set_id block (#16)
* Add params to set_id block arguments

Pull request #331 added a block to the ObjectSerializer.set_id class
method, which allows passing a block to the set_id method. Currently
this block takes only one argument `record`:

```
set_id do |record|
  "#{record.name.downcase}-#{record.id}"
end
```

This PR adds another argument `params` to the block:

```
set id do |record, params|
  params[:admin] ?  record.id : "#{record.name.downcase}-#{record.id}"
end
```

This customization can be useful in situation where we serve different
clients that may need different IDs. One nice side effect is also that
the `set_id` method has the same method signature as the `attribute`
method.

* Update the README
2019-10-15 14:45:46 -04:00
Kevin Pheasey
691c8ac632 Merge branch 'master' into dev 2019-10-08 11:31:01 -04:00
Thomas
330dc89e62 do not enforce lastest bundler version 2019-10-08 11:29:59 -04:00
Thomas
0c49f29e75 update bundle version 2019-10-08 11:29:59 -04:00
Kevin Pheasey
145b4ce23a
Update CHANGELOG.md 2019-10-08 08:31:25 -04:00
David Pickart
8e2383128e Include data key when lazy-loaded relationships are specified with includes (#10) 2019-10-08 08:30:21 -04:00
Kevin Pheasey
37206ddf0b
Update CHANGELOG.md 2019-10-04 19:45:43 -04:00
Krzysztof Rybka
f2a1934b76 Use each_with_object instead of Hash[map] 2019-10-04 19:44:17 -04:00
Krzysztof Rybka
44a896dda5 Take items of original array instead of dup and delete 2019-10-04 19:41:22 -04:00
Kevin Pheasey
5f8629873a Merge remote-tracking branch 'origin/dev' into dev 2019-10-04 12:51:05 -04:00
Krzysztof Rybka
e4c65a2567 Move transforms mapping to constant 2019-10-04 12:50:22 -04:00
Krzysztof Rybka
fd17386b51 Map split include_item in-place 2019-10-04 12:50:22 -04:00
Jo Potts
83e99b2923 Allow relationship links to be declared as object method (#2)
* Allow relationship links to be declared as object method

* Relationship links note added to readme
2019-10-04 12:50:22 -04:00
Krzysztof Rybka
f04abfd2fe Compute remaining_items once 2019-10-04 12:50:22 -04:00
Krzysztof Rybka
b24af1f912 Test against Ruby 2.6 and fix Travis (#3)
* Test against Ruby 2.6

* Enforce bundler 1.17.3
2019-10-04 12:50:22 -04:00
iwsksky
e68dbee806 update document/use let statement 2019-10-04 12:50:22 -04:00
iwsksky
b9a86a002a pass array of unique movies to serializer 2019-10-04 12:50:22 -04:00
Daniel Duke
5767664c8a add specs for multiple include options 2019-10-04 12:50:22 -04:00
Daniel Duke
267b706366 validate all include items instead of just the first 2019-10-04 12:50:22 -04:00
Daniel Illi
8d8e5c3059 Fix error on defining anonymous serializer class, fixes #353 2019-10-04 12:50:22 -04:00
Matt Eddy
21ae4aaa0a Allow fieldsets to specify no attributes/relationships 2019-10-04 12:50:22 -04:00
Danil Pismenny
1a407c0030 [#365] Support frozen array in option 2019-10-04 12:50:22 -04:00
Maxime Orefice
021db27605 Update Readme
Fix typo
2019-10-04 12:50:22 -04:00
Charalampos Aristomenopoulos
209c925723 Fix typo in README 2019-10-04 12:50:22 -04:00
Csaba Apagyi
f0142d948c Fix formatting of set_id example in README 2019-10-04 12:50:22 -04:00
Jessie A. Young
9e83c1e0a5 Highlight that this is for JSON:API spec only
* The link was already there but I skipped over it on my first read.
This update makes the fact more prominent.
* I was testing to see if we wanted to move from AM Serializers to
fast_jsonapi but our API is not written according to the JSON:API spec
so, after converting one serializer over, I learned that this would not work for me.
* This update might save someone in my position the ~30 mins or so it
takes to bundle and write a serializer in the future. :)
2019-10-04 12:50:22 -04:00
Kevin Pheasey
0dc332dc32
Create CHANGELOG.md 2019-10-04 12:46:53 -04:00
Krzysztof Rybka
dce1faf1e2 Move transforms mapping to constant 2019-10-04 12:44:22 -04:00
Krzysztof Rybka
9ec89d4cf5 Map split include_item in-place 2019-10-04 12:42:42 -04:00
Jo Potts
2b6c81692f Allow relationship links to be declared as object method (#2)
* Allow relationship links to be declared as object method

* Relationship links note added to readme
2019-10-04 12:39:34 -04:00
Krzysztof Rybka
1373eb436c Compute remaining_items once 2019-10-04 12:28:02 -04:00
Krzysztof Rybka
d64b1b5f4f Test against Ruby 2.6 and fix Travis (#3)
* Test against Ruby 2.6

* Enforce bundler 1.17.3
2019-10-04 08:42:12 -04:00
Srinivas R
3df917f407
Update README.md 2019-06-11 14:59:22 -07:00
iwsksky
83e7fb62f9 update document/use let statement 2019-04-07 12:50:43 -07:00
iwsksky
4077a23c45 pass array of unique movies to serializer 2019-04-07 12:50:43 -07:00
Daniel Duke
9f0608d4c9 add specs for multiple include options 2019-04-07 12:33:16 -07:00
Daniel Duke
3668625882 validate all include items instead of just the first 2019-04-07 12:33:16 -07:00
Daniel Illi
dd379a02ca Fix error on defining anonymous serializer class, fixes #353 2019-04-07 12:29:56 -07:00
Matt Eddy
ae93b85103 Allow fieldsets to specify no attributes/relationships 2019-04-07 12:28:44 -07:00
Danil Pismenny
36b8ea2dfc [#365] Support frozen array in option 2019-03-28 21:15:08 -07:00
Shishir Kakaraddi
6aefeb7556 bump up version to 1.5 2019-03-28 21:15:08 -07:00
Manoj M J
e0228dacdc Add documentation on how to use helper methods in serializers 2019-03-28 21:12:36 -07:00
Manoj M J
a160d6746f Fix Documentation of Meta Per Resource
Some part of the documentation for this got removed during last merge, so fixing it.
2019-03-28 21:11:48 -07:00
Maxime Orefice
91e2beec8e Update Readme
Fix typo
2019-03-28 15:00:06 -07:00
Charalampos Aristomenopoulos
513eaca3dc Fix typo in README 2019-03-28 14:59:25 -07:00
Csaba Apagyi
bf7a7e35c8 Fix formatting of set_id example in README 2019-03-28 14:59:07 -07:00
Jessie A. Young
fcecda7dd8 Highlight that this is for JSON:API spec only
* The link was already there but I skipped over it on my first read.
This update makes the fact more prominent.
* I was testing to see if we wanted to move from AM Serializers to
fast_jsonapi but our API is not written according to the JSON:API spec
so, after converting one serializer over, I learned that this would not work for me.
* This update might save someone in my position the ~30 mins or so it
takes to bundle and write a serializer in the future. :)
2019-03-28 14:58:20 -07:00
Shishir Kakaraddi
fdcaed6f0d
Merge pull request #342 from Netflix/release-1.5
Release 1.5
2018-11-03 12:32:22 -07:00
Shishir Kakaraddi
ee76e0c69b bump up version to 1.5 2018-11-03 12:11:56 -07:00
Shishir Kakaraddi
cc7f88843a
Merge pull request #331 from larissa/set-id-block
Allow block for id customization
2018-11-03 11:37:56 -07:00
Shishir Kakaraddi
326f9784ca
Merge pull request #294 from nikz/add-links-option-to-relationship-serializer
Adds a :links option to the relationship macros
2018-11-03 11:35:39 -07:00
Shishir Kakaraddi
9d19f32c78
Merge pull request #334 from hmcfletch/fix/as-core_ext-time
require AS core extension for time
2018-11-03 11:30:54 -07:00
Shishir Kakaraddi
a105bac3d9
Merge pull request #336 from coderbydesign/patch-1
Minor fix in README.md
2018-11-03 11:30:35 -07:00
Shishir Kakaraddi
74bb9d6f6d
Merge pull request #339 from mhluska/fix-params-not-being-default
Fix params not being hash by default
2018-11-03 11:30:20 -07:00
Larissa Reis
0ba5f231fe Add set_id block syntax example to README 2018-10-18 22:04:52 -06:00
Maros Hluska
d5ea95370f Fix params not being hash by default 2018-10-18 22:59:33 +07:00
Keith Walsh
dbda6b6153
Update README.md
Resolve minor typo
2018-10-17 09:29:49 -04:00
Shishir Kakaraddi
ece607af5f
Merge pull request #326 from gorenje/spec_for_has_one_through
added spec for has_one-through relationship
2018-10-14 08:32:44 -07:00
Shishir Kakaraddi
935fc05beb
Merge pull request #329 from zinosama/improve-readme-with-id_method_name
Improve documentation with id_method_name
2018-10-14 08:31:01 -07:00
Shishir Kakaraddi
f3368dee2d
Merge pull request #330 from matzko/dev
Don't share data_links among inherited serializers.
2018-10-14 08:29:59 -07:00
Larissa Reis
9fa26fa588 Allow block for id customization
Allow an ID of object to be customized directly on the serializer by
passing a block to `set_id` as opposed to only through a model property.

We already allow for attributes that do not have a model property of the
same name to be customized directly on the serializer using a block.

This customization can be useful in situation in which you have
different classes being serialized using the same serializer. For
example, if we have `HorrorMovie`, `ComedyMovie` and `DramaMovie` using
the same `MovieSerializer`, we can unify their IDs using

```
class MovieSerializer
  include FastJsonapi::ObjectSerializer

  attributes :name, :year
  set_id do |record|
    "#{record.name.downcase}-#{record.id}"
  end
```

which is preferable to creating a `#serialized_id` method in every model
that will use `MovieSerializer` to encapsulate the customization.

Closes #315
2018-10-10 22:26:57 -06:00
Les Fletcher
05ad93084b
require 'active_support/core_ext/numeric/time’
`5.minutes` was failing in the performance spec
2018-10-09 14:53:57 -07:00
nikz
1ad20d6b7b Merge branch 'upstream-dev' into add-links-option-to-relationship-serializer 2018-10-07 21:29:43 +01:00
nikz
85b41c45d4 Adds :lazy_load_data option
If you include a default empty `data` option in your JSON API response,
many frontend frameworks will ignore your `related` link that could be
used to load relationship records, and will instead treat the
relationship as empty.

This adds a `lazy_load_data` option which will:

  * stop the serializer attempting to load the data and;
  * exclude the `data` key from the final response

This allows you to lazy load a JSON API relationship.
2018-10-07 21:23:36 +01:00
Austin Matzko
be701f3e06 Don't attempt to dup a nil 2018-10-03 17:59:58 -04:00
Austin Matzko
1ab5cd387a Don't share data_links among inherited serializers. 2018-10-03 17:51:37 -04:00
nikz
1efdd3372d Fixes dangling comma and unused param 2018-10-02 22:09:15 +01:00
zino
467024f8fd Improve readme with id_method_name 2018-10-02 11:36:21 -04:00
Gerrit Riessen
9bff454806 added spec for has_one-through relationship 2018-09-26 12:03:29 +02:00
Shishir Kakaraddi
11b5255010
Merge pull request #313 from sakuraineed/fix_set_key_transform
Fix set_key_transform's set_type to give priority to pre-set value
2018-09-19 21:56:59 -07:00
Shishir Kakaraddi
92bcab0a3f
Merge pull request #320 from Netflix/release-1.4
Release 1.4
2018-09-19 21:41:53 -07:00
Shishir Kakaraddi
90e0feef3c Merge branch 'release-1.4' of https://github.com/Netflix/fast_jsonapi into release-1.4 2018-09-19 21:33:01 -07:00
Shishir Kakaraddi
b674909830 bump up version to 1.4 2018-09-19 21:32:56 -07:00
Shishir Kakaraddi
eb15334146
Merge pull request #311 from Netflix/dev
Dev
2018-09-19 21:25:56 -07:00
Shishir Kakaraddi
fced516356 transforms type for polymorphic relationships too 2018-09-19 20:24:40 -07:00
Kenji Sakurai
64f7b6c50d Add spec for singular and plural, so remove same checking example. 2018-09-16 14:18:10 +09:00
Kenji Sakurai
57f09c7d71 Fix method order in spec after 2018-09-10 11:07:06 +09:00
Kenji Sakurai
6dc34cd4d4 Fix set_key_transform's set_type to give priority to pre-set value 2018-09-08 23:48:44 +09:00
Matthew Lanigan
5a70b1a686 Do not use ActiveSupport core extensions
Core extensions do not play well with many other gems; especially
considering that they only seem to be included for one `to_json` call,
they should be avoided.
2018-09-03 12:01:45 -07:00
Orhan Toy
8357acd6a7 [ci skip] Fix punctuation 2018-09-03 12:01:45 -07:00
François Pradel
955f4f234d Add support for polymorphic includes 2018-08-31 19:17:45 -07:00
François Pradel
3973b312a7 Demodulize relationship record class name 2018-08-31 19:17:45 -07:00
Alessandro Dal Grande
42d9203796 Fix confusing error when using include and no relationship was set
If you forgot to set any `has_many` in the serializer and tried to serialize with an `include` you would get:
```
NoMethodError (undefined method `[]' for nil:NilClass):
```

That is not very helpful. Setting the variable with a default case makes sure the right error message gets displayed.
2018-08-22 20:08:04 -07:00
Orhan Toy
4afa5b81d8 [ci skip] Fix punctuation 2018-08-22 20:06:47 -07:00
Igor Khodyrev
e3c45d9b1b Fix weak entities case (id is not meaningful attribute) with include. 2018-08-22 20:05:51 -07:00
Shishir Kakaraddi
2eaaa71bd8
Merge pull request #299 from g13ydson/feature/add_option_key
add the option "key" in customizable options in the README
2018-08-13 18:05:05 -07:00
Gleydson Tavares
ce2c8003ee add the "option key" in customizable options 2018-08-10 13:15:02 -03:00
nikz
ef04bc377e Removes Hash#dig usage 2018-08-05 21:03:13 +01:00
Nik Wakelin
100f850416
Merge branch 'dev' into add-links-option-to-relationship-serializer 2018-08-05 20:58:39 +01:00
nikz
8eef7a0bb1 Adds README documentation for relationship links 2018-08-05 20:51:56 +01:00
nikz
89f007d069 Adds a :links option to the relationship macros
This allows specifying a `:links` option to a has_many/has_one
relationship, which means you can specify `self` or `related` links as
per the JSON API spec (these are often useful for not loading all
associated objects in a single payload)
2018-08-05 20:37:44 +01:00
Shishir Kakaraddi
a5414c6b8f
Merge pull request #280 from manojmj92/add_meta
Introduce the ability to add `meta` tag for every resource in the collection
2018-07-23 13:30:39 -07:00
Shishir Kakaraddi
daf4030bb0
Merge pull request #274 from manojmj92/master
Evaluate ids via the specified 'id_method_name' when relationships are evaluated via a block
2018-07-23 13:26:53 -07:00
Trevor Hinesley
5a5a5e5125 Fixed conditional attributes documentation 2018-07-23 13:23:41 -07:00
Manoj M J
6d03db3a0c
Merge branch 'dev' into master 2018-07-21 10:06:56 +05:30
Manoj M J
0c367d2574 Minor code refactor 2018-07-21 10:05:01 +05:30
Manoj M J
099eb606bd
Merge branch 'dev' into add_meta 2018-07-21 09:39:33 +05:30
Shishir Kakaraddi
40125072c5
Merge pull request #278 from TrevorHinesley/params-in-nested-includes
Params are now passed to nested includes
2018-07-20 19:16:23 -07:00
Shishir Kakaraddi
75c959af81 Merge branch 'master' of https://github.com/Netflix/fast_jsonapi into dev 2018-07-20 18:57:07 -07:00
Erol
e1f782e79f Add missing fieldset parameter 2018-07-20 18:49:51 -07:00
Manoj M J
dd71bc15d6 Introduce the ability to add meta tag for every resource in the collection 2018-07-20 10:33:27 +05:30
Trevor Hinesley
dfcbe263fb
Merge branch 'master' into params-in-nested-includes 2018-07-19 09:58:25 -05:00
Trevor Hinesley
07b6e614ac Params are now passed to nested includes 2018-07-19 09:57:22 -05:00
Manoj M J
9c659839e4 Evaluate ids via the specified ‘id_method_name’ when relationships are evaluated via a block 2018-07-19 15:40:20 +05:30
Shishir Kakaraddi
5ff3fa97da
Merge pull request #273 from Netflix/release-1.3
Release 1.3
2018-07-16 22:01:17 -07:00
Shishir Kakaraddi
115a01a7c2 bump up version to 1.3 2018-07-16 21:52:58 -07:00
Erol
9aec7c58ed Add README instructions for using sparse fieldsets 2018-07-16 21:10:10 -07:00
Erol
fa194133fa Use record type instead of reflected record type 2018-07-16 21:10:10 -07:00
Erol
e2bf5411a2 Set the record type for inherited serializers 2018-07-16 21:10:10 -07:00
Erol
ab652c4400 Remove unused code 2018-07-16 21:10:10 -07:00
Erol
e683bbfb78 Update spec with included documents with no explicitly given fields 2018-07-16 21:10:10 -07:00
Erol
a363c90bfb Allow the serializer to return sparse fieldsets 2018-07-16 21:10:10 -07:00
Erol
41c1e0a106 Add proc shortcut use case to README 2018-07-16 21:10:10 -07:00
Erol
e05193fb5e Add spec for proc methods with optional arguments 2018-07-16 21:10:10 -07:00
Erol
449c1bf05f Allow passing procs with variable arguments when declaring an attribute 2018-07-16 21:10:10 -07:00
Erol
3df48cd4cb Add README instructions for using sparse fieldsets 2018-07-16 21:07:57 -07:00
Erol
5905497314 Use record type instead of reflected record type 2018-07-16 21:07:57 -07:00
Erol
77c7af2a5e Set the record type for inherited serializers 2018-07-16 21:07:57 -07:00
Erol
5aa5dc511c Remove unused code 2018-07-16 21:07:57 -07:00
Erol
d427a157ee Update spec with included documents with no explicitly given fields 2018-07-16 21:07:57 -07:00
Erol
7b44620018 Allow the serializer to return sparse fieldsets 2018-07-16 21:07:57 -07:00
Erol
abc830b41e Add proc shortcut use case to README 2018-07-16 09:05:04 -07:00
Erol
5c8e9358f2 Add spec for proc methods with optional arguments 2018-07-16 09:05:04 -07:00
Erol
77a3a0bb5b Allow passing procs with variable arguments when declaring an attribute 2018-07-16 09:05:04 -07:00
Shishir Kakaraddi
49193ab8f3
Merge pull request #265 from Netflix/dev
Dev
2018-07-03 21:14:18 -07:00
Shishir Kakaraddi
dc2b78bbe4 merging with release branch 2018-07-04 09:35:33 +05:30
Oleksiy Babich
ecb92f07f5 add is_collection parameter to force corresponding serialization (#239)
* add is_collection parameter to force corresponding serialization

* add documentation for is_collection purpose, behavior
and notes re. default autodetect logic
2018-07-03 19:35:06 -07:00
Kyle Reeves
af38b30179 remove options param from Link class 2018-07-03 19:33:34 -07:00
Kyle Reeves
01477e9c5b fix relationship id_hash method 2018-07-03 19:33:34 -07:00
Kyle Reeves
699630d812 create link class 2018-07-03 19:33:34 -07:00
Kyle Reeves
f86a8926f5 make include_relationship? a public method and use it in get_included_records method 2018-07-03 19:33:34 -07:00
Kyle Reeves
30596c4488 move add_relationship to each class method 2018-07-03 19:33:34 -07:00
Kyle Reeves
6e7d8b7ee0 make fetch_associated_object a public method on relationship class so it can be called from SerilizationCore class 2018-07-03 19:33:34 -07:00
Kyle Reeves
22d412246f WIP 2018-07-03 19:33:34 -07:00
Kyle Reeves
d47b74f71f all tests are passing, but still need to write tests for relationship class 2018-07-03 19:33:34 -07:00
Kyle Reeves
7b23adddc4 working on new relationship class 2018-07-03 19:33:34 -07:00
Trevor Hinesley
f864099761 Conditional relationships should be removed from included when proc evaluates to false 2018-07-03 19:33:34 -07:00
Kyle Reeves
0b70657a41 update test for conditional relationships 2018-07-03 19:33:34 -07:00
Kyle Reeves
25c099e923 add documentation for conditional relationships 2018-07-03 19:33:34 -07:00
Kyle Reeves
5558dcd703 allow conditional relationships 2018-07-03 19:33:34 -07:00
Guillermo Iguaran
2b01d8ce70 Use a Railtie to extend Rails 2018-06-21 18:40:42 -07:00
Darren Johnson
4a333d7276 Set type value when setting key transform 2018-06-21 18:40:24 -07:00
Trevor Hinesley
ba4e112829 Since attributes are an instantiated class now, renamed AttributeSerializer to Attribute 2018-06-21 18:38:42 -07:00
Trevor Hinesley
5c820695b3 Split attribute serialization into its own class 2018-06-21 18:38:42 -07:00
Trevor Hinesley
f1df3f4a2d Added documentation about conditional attributes 2018-06-21 18:38:42 -07:00
Trevor Hinesley
bad004fd42 Allow conditional attributes 2018-06-21 18:38:42 -07:00
Shishir Kakaraddi
75229fdfbf
Dev (#232)
* Remove extra 'class MovieSerializer' from an example in the README

* Fix serialization for nested nil includes with block
2018-06-10 13:37:19 -07:00
homer
44d5e0f9c5 Fix serialization for nested nil includes with block 2018-05-31 17:16:54 -07:00
Ray Walters
f54e6242ff Remove extra 'class MovieSerializer' from an example in the README 2018-05-31 17:16:38 -07:00
Shishir Kakaraddi
39fdc6f66c merge with master 2018-05-31 17:13:34 -07:00
Shishir Kakaraddi
81375cfcf7 bump up version to 1.2 2018-05-31 16:36:16 -07:00
Shishir Kakaraddi
ef42fb3031 Changes for version 1.2 (#220)
params support in blocks, nested includes etc

Co-authored-by: Jodi Showers <jodi@nnovation.ca>
Co-authored-by: Ryan O'Donnell <ryan@gocleary.com>
Co-authored-by: Les Fletcher <les.fletcher@gmail.com>
Co-authored-by: Ankit gupta <ankit.gupta8898@gmail.com>
Co-authored-by: Masato Ohba <over.rye@gmail.com>
Co-authored-by: Shuhei Kitagawa <shuhei.kitagawa@c-fo.com>
Co-authored-by: Zino <rhu5@u.rochester.edu>
Co-authored-by: Carlos Solares <csolares23@gmail.com>
Co-authored-by: Brandon Buck <lordizuriel@gmail.com>
Co-authored-by: Daniel Roux <xuoroux@gmail.com>
Co-authored-by: Dillon Welch <daw0328@gmail.com>
2018-05-31 16:35:59 -07:00
Shishir Kakaraddi
cd1bc0968e dont create a object for a has one relationship unnecessarily just to fetch id 2018-05-20 15:11:07 -07:00
Shishir Kakaraddi
a018f1d32f minor fixes to data links feature 2018-05-20 15:08:47 -07:00
Shishir Kakaraddi
ea5296ac25 making object level links similar to attributes 2018-05-20 15:08:47 -07:00
Jodi Showers
74f27ccdf0 Links within data (#161) 2018-05-20 13:14:46 -07:00
Ryan O'Donnell
b090391551 Fix serialization for nested nil includes 2018-05-18 19:06:16 -07:00
Les Fletcher
077817ecec fix skylight normalizers issue 2018-05-16 18:43:05 -07:00
Ankit gupta
ac136b988c
Merge pull request #213 from ohbarye/patch-1
Remove dead link
2018-05-14 21:42:53 -07:00
Masato Ohba
7263aba777
Remove dead link
Because the position is closed.

Refs: https://github.com/Netflix/fast_jsonapi/issues/209
2018-05-15 12:39:23 +09:00
Shishir Kakaraddi
3fb975602b fixes some unnecessary performance test failures 2018-05-14 19:27:50 -07:00
Guillermo Iguaran
190bedaa05 Refactor compute_serializer_name to follow DRY 2018-05-14 17:54:33 -07:00
Shishir Kakaraddi
00d3aa4997 adding NotImplementedError when trying to include polymorphic relationships 2018-05-10 23:22:21 -07:00
Shishir Kakaraddi
d7f5c34404 fixes a syntax error 2018-05-10 22:13:59 -07:00
Jodi Showers
3ebf34928c Serialize nested includes (#152) 2018-05-10 21:17:32 -07:00
Shishir Kakaraddi
966b3509a4 update readme 2018-05-08 22:11:22 -07:00
Shishir Kakaraddi
63f905ab36 adds params to relatinoship blocks and tests 2018-05-08 22:11:22 -07:00
Shuhei Kitagawa
5b64e90956 Add tests for block relationship 2018-05-06 10:26:38 -07:00
Shuhei Kitagawa
e39de8c8c4 Enable to use block to define relationship 2018-05-06 10:26:38 -07:00
Shishir Kakaraddi
4523508c5b inherits attributes, relationships and other settings from parent serializer 2018-05-05 19:12:45 -07:00
Zino
f4f289a0bc Remove duplicate id_hash call from #record_hash 2018-05-03 20:50:03 -07:00
Zino
faa8fe6caf Use string for serializer name 2018-05-02 10:22:22 -07:00
Shuhei Kitagawa
5d8e1ce9e7 Refactor tests for key_transform method 2018-05-01 23:28:07 -07:00
Carlos Solares
fe5ecb5b28 Use the serializer_key directly if it is not a symbol in relationships (#198) 2018-05-01 10:15:43 -07:00
Brandon Buck
a585497161 Add in options[:scope] and receiving scope in attribute blocks (#153) 2018-05-01 09:58:01 -07:00
Daniel Roux
7b48340a7c Require 'logger' to avoid exception 2018-05-01 09:57:04 -07:00
Mark
2fe3b8ab99 Add ruby version requirement '>= 2.0.0'
With ruby 1.9.3 #serialized_json raises an exception:
```
NameError:
       undefined local variable or method `caller_locations' for FastJsonapi::MultiToJson:Module
```
`Kernel#caller_locations` was added in ruby '2.0.0'
Source: https://docs.ruby-lang.org/en/2.2.0/NEWS-2_0_0.html
2018-05-01 09:56:09 -07:00
Shishir Kakaraddi
c943683141 second attempt to fix the has one name collision issue 2018-04-26 22:08:56 -07:00
Shishir Kakaraddi
901801fa80 checks if method is defined before defining it 2018-04-26 21:31:56 -07:00
Dillon Welch
1b3b533b40 Don't allocate variables if we're not going to use them 2018-04-26 19:33:20 -07:00
Dillon Welch
da275e189d Add default hash option to id_hash 2018-04-26 19:33:20 -07:00
Dillon Welch
ca0f600ed9 Use id_hash method instead of duplicating logic 2018-04-26 19:33:20 -07:00
Dillon Welch
a29b2c6184 Use alias for belongs_to because code is the same
Addresses https://github.com/Netflix/fast_jsonapi/issues/73
2018-04-26 10:37:44 -07:00
Dillon Welch
5428820d73 Add extra variables to cache to_sym calls 2018-04-26 10:37:44 -07:00
Dillon Welch
5b65608142 Move more of the logic inside the hash method 2018-04-26 10:37:44 -07:00
Dillon Welch
1f6fca522e Compute relationship_hash on a separate line for clarity 2018-04-26 10:37:44 -07:00
Dillon Welch
e8f276c44f Get rid of unnecessary freezes 2018-04-26 10:37:44 -07:00
Dillon Welch
f0cc24ed06 DRY up ObjectSerializer code 2018-04-26 10:37:44 -07:00
Konstantin
bc8996c04d fix ActiveRecord ConnectionNotEstablished when ActiveRecord isnt required in a project 2018-04-21 15:26:31 -07:00
Shuhei Kitagawa
af0aed4414 Clean up group_context.rb 2018-04-21 14:54:41 -07:00
Ankit Gupta
0008c5a165 Making a version file to manage version. Also not adding date as it defaults to current UTC date
https://ruby-doc.org/stdlib-2.2.3/libdoc/rubygems/rdoc/Gem/Specification.html#method-i-date
2018-04-20 15:26:44 -07:00
Ankit gupta
5543a5c3e8
Merge pull request #178 from FanaHOVA/patch-1
Use proper casing on generator example
2018-04-20 10:56:36 -07:00
Alessio Fanelli
01ac9ccbe7
Use proper casing on generator example
`rails g Serializer` isn't a valid command, serializer shouldn't be capitalized.
2018-04-20 14:37:56 +02:00
Shuhei Kitagawa
4fdf5a221c Enable to set race_condition_ttl for cache_options 2018-04-08 22:53:31 -07:00
Shuhei Kitagawa
fb7d01368a Integrate tests for #set_id and #attribute to object_serializer_class_methods_spec 2018-03-29 17:22:51 -07:00
Shuhei Kitagawa
0d8bbedcdd Change transform_method to accessor 2018-03-29 17:22:51 -07:00
Dmitriy Ivliev
1196db46e5 add exception for missing id method 2018-03-26 22:38:14 -07:00
Dmitriy Ivliev
c2e4c01bf1 fix behaviour for struct without id 2018-03-26 22:38:14 -07:00
Shuhei Kitagawa
cdfac8743d Refactor object serializer class methods spec (#134)
* Add object_serializer_class_methods_examples

* Change to require spec/shared/examples in every spec files

* Refactor object_serializer_class_methods_spec
2018-03-26 22:16:37 -07:00
Shuhei Kitagawa
4f3a903e64 Remove redundant fetch from ids_hash_from_record_and_relationship 2018-03-26 22:12:07 -07:00
Shishir Kakaraddi
e486a962e1 merge with master 2018-03-25 12:09:15 -07:00
Marten
18d30f228e Update .gitignore 2018-03-25 12:03:42 -07:00
Marten
9134930d76 Delete Gemfile.lock 2018-03-25 12:03:42 -07:00
Shuhei Kitagawa
b18da3da59 Remove unused variable from object_serializer_class_methods_spec 2018-03-21 07:39:55 -07:00
Rob Wise
fea384b4c6 update README to mention cache_key is required
see https://github.com/Netflix/fast_jsonapi/issues/99#issuecomment-374060813
2018-03-20 10:35:51 -07:00
sojan v jose
b387f94a13 Fix typo in the readme sample command (#131)
fix typo in the readme sample command
2018-03-19 12:53:24 -07:00
Roberto Quintanilla
88553cf9ab Updated README with details about polymorphic associations 2018-03-18 22:32:01 -07:00
mnauage
1e6d127aec links doc 2018-03-18 22:31:02 -07:00
mnauage
ecd7bbc793 links key support 2018-03-18 22:31:02 -07:00
79 changed files with 2457 additions and 2770 deletions

View File

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

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

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

7
.gitignore vendored
View File

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

102
.rubocop.yml Normal file
View File

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

View File

@ -1,8 +0,0 @@
language: ruby
rvm:
- 2.2.9
- 2.3.6
- 2.4.3
- 2.5.0
script:
- bundle exec rspec

79
CHANGELOG.md Normal file
View File

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

View File

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

View File

@ -1,117 +0,0 @@
PATH
remote: .
specs:
fast_jsonapi (1.1.1)
activesupport (>= 4.2)
GEM
remote: https://rubygems.org/
specs:
actionpack (5.1.4)
actionview (= 5.1.4)
activesupport (= 5.1.4)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.4)
activesupport (= 5.1.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.7)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activemodel (5.1.4)
activesupport (= 5.1.4)
activerecord (5.1.4)
activemodel (= 5.1.4)
activesupport (= 5.1.4)
arel (~> 8.0)
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (8.0.0)
benchmark-perf (0.2.1)
builder (3.2.3)
byebug (9.1.0)
case_transform (0.2)
activesupport
concurrent-ruby (1.0.5)
crass (1.0.3)
diff-lcs (1.3)
erubi (1.7.0)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
jsonapi-deserializable (0.2.0)
jsonapi-rb (0.5.0)
jsonapi-deserializable (~> 0.2.0)
jsonapi-serializable (~> 0.3.0)
jsonapi-renderer (0.2.0)
jsonapi-serializable (0.3.0)
jsonapi-renderer (~> 0.2.0)
jsonapi-serializers (1.0.0)
activesupport
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mini_portile2 (2.3.0)
minitest (5.10.3)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
oj (3.4.0)
rack (2.0.3)
rack-test (0.8.2)
rack (>= 1.0, < 3)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rspec (3.5.0)
rspec-core (~> 3.5.0)
rspec-expectations (~> 3.5.0)
rspec-mocks (~> 3.5.0)
rspec-benchmark (0.3.0)
benchmark-perf (~> 0.2.0)
rspec (>= 3.0.0, < 4.0.0)
rspec-core (3.5.4)
rspec-support (~> 3.5.0)
rspec-expectations (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-mocks (3.5.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.5.0)
rspec-support (3.5.0)
skylight (1.5.0)
activesupport (>= 3.0.0)
sqlite3 (1.3.13)
thread_safe (0.3.6)
tzinfo (1.2.4)
thread_safe (~> 0.1)
PLATFORMS
ruby
DEPENDENCIES
active_model_serializers (~> 0.10.7)
activerecord (>= 4.2)
bundler (~> 1.0)
byebug
fast_jsonapi!
jsonapi-rb (~> 0.5.0)
jsonapi-serializers (~> 1.0.0)
oj (~> 3.3)
rspec (~> 3.5.0)
rspec-benchmark (~> 0.3.0)
skylight (~> 1.3)
sqlite3 (~> 1.3)
BUNDLED WITH
1.16.1

View File

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

View File

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

615
README.md
View File

@ -1,20 +1,29 @@
# Fast JSON API
# JSON:API Serialization Library
[![Build Status](https://travis-ci.org/Netflix/fast_jsonapi.svg?branch=master)](https://travis-ci.org/Netflix/fast_jsonapi)
## :warning: :construction: v2 (the `master` branch) is in maintenance mode! :construction: :warning:
A lightning fast [JSON:API](http://jsonapi.org/) serializer for Ruby Objects.
We'll gladly accept bugfixes and security-related fixes for v2 (the `master` branch), but at this stage, contributions for new features/improvements are welcome only for v3. Please feel free to leave comments in the [v3 Pull Request](https://github.com/jsonapi-serializer/jsonapi-serializer/pull/141).
---
A fast [JSON:API](https://jsonapi.org/) serializer for Ruby Objects.
Previously this project was called **fast_jsonapi**, we forked the project
and renamed it to **jsonapi/serializer** in order to keep it alive.
We would like to thank the Netflix team for the initial work and to all our
contributors and users for the continuous support!
# Performance Comparison
We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least `25 times` faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the [performance document](https://github.com/Netflix/fast_jsonapi/blob/master/performance_methodology.md) for any questions related to methodology.
We compare serialization times with `ActiveModelSerializer` and alternative
implementations as part of performance tests available at
[jsonapi-serializer/comparisons](https://github.com/jsonapi-serializer/comparisons).
## Benchmark times for 250 records
```bash
$ rspec
Active Model Serializer serialized 250 records in 138.71 ms
Fast JSON API serialized 250 records in 3.01 ms
```
We want to ensure that with every
change on this library, serialization time stays significantly faster than
the performance provided by the alternatives. Please read the performance
article in the `docs` folder for any questions related to methodology.
# Table of Contents
@ -29,6 +38,15 @@ Fast JSON API serialized 250 records in 3.01 ms
* [Key Transforms](#key-transforms)
* [Collection Serialization](#collection-serialization)
* [Caching](#caching)
* [Params](#params)
* [Conditional Attributes](#conditional-attributes)
* [Conditional Relationships](#conditional-relationships)
* [Specifying a Relationship Serializer](#specifying-a-relationship-serializer)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods)
* [Performance Instrumentation](#performance-instrumentation)
* [Deserialization](#deserialization)
* [Migrating from Netflix/fast_jsonapi](#migrating-from-netflixfast_jsonapi)
* [Contributing](#contributing)
@ -45,7 +63,7 @@ Fast JSON API serialized 250 records in 3.01 ms
Add this line to your application's Gemfile:
```ruby
gem 'fast_jsonapi'
gem 'jsonapi-serializer'
```
Execute:
@ -60,7 +78,7 @@ $ bundle install
You can use the bundled generator if you are using the library inside of
a Rails project:
rails g Serializer Movie name year
rails g serializer Movie name year
This will create a new serializer in `app/serializers/movie_serializer.rb`
@ -76,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
@ -96,6 +115,17 @@ movie.actor_ids = [1, 2, 3]
movie.owner_id = 3
movie.movie_type_id = 1
movie
movies =
2.times.map do |i|
m = Movie.new
m.id = i + 1
m.name = "test movie #{i}"
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
```
### Object Serialization
@ -107,7 +137,7 @@ hash = MovieSerializer.new(movie).serializable_hash
#### Return Serialized JSON
```ruby
json_string = MovieSerializer.new(movie).serialized_json
json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
#### Serialized Output
@ -146,12 +176,16 @@ json_string = MovieSerializer.new(movie).serialized_json
```
#### The Optionality of `set_type`
By default fast_jsonapi will try to figure the type based on the name of the serializer class. For example `class MovieSerializer` will automatically have a type of `:movie`. If your serializer class name does not follow this format, you have to manually state the `set_type` at the serializer.
### Key Transforms
By default fast_jsonapi underscores the key names. It supports the same key transforms that are supported by AMS. Here is the syntax of specifying a key transform
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer
# Available options :camel, :camel_lower, :dash, :underscore(default)
set_key_transform :camel
end
@ -166,14 +200,14 @@ 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
```
@ -182,10 +216,10 @@ 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
attribute :name_with_year do |object|
"#{object.name} (#{object.year})"
end
@ -196,112 +230,537 @@ 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"
end
end
```
Attributes can also use a different name by passing the original method or accessor with a proc shortcut:
```ruby
class MovieSerializer
include JSONAPI::Serializer
attributes :name
attribute :released_in_year, &:year
end
```
### Links Per Object
Links are defined using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.
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 also use a block to define a url as shown in `custom_url`. You can access params in these blocks as well as shown in `personalized_url`
```ruby
class MovieSerializer
include JSONAPI::Serializer
link :public_url
link :self, :url
link :custom_url do |object|
"https://movies.com/#{object.name}-(#{object.year})"
end
link :personalized_url do |object, params|
"https://movies.com/#{object.name}-#{params[:user].reference_code}"
end
end
```
#### Links on a Relationship
You can specify [relationship links](https://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](https://jsonapi.org/format/#document-resource-object-related-resource-links))
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
end
```
Relationship links can also be configured to be defined as a method on the object.
```ruby
has_many :actors, links: :actor_relationship_links
```
This will create a `self` reference for the relationship, and a `related` link for loading the actors relationship later. NB: This will not automatically disable loading the data in the relationship, you'll need to do that using the `lazy_load_data` option:
```ruby
has_many :actors, lazy_load_data: true, links: {
self: :url,
related: -> (object) {
"https://movies.com/#{object.id}/actors"
}
}
```
### Meta Per Resource
For every resource in the collection, you can include a meta object containing non-standard meta-information about a resource that can not be represented as an attribute or relationship.
```ruby
class MovieSerializer
include JSONAPI::Serializer
meta do |movie|
{
years_since_release: Date.current.year - movie.year
}
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
Support for top-level included member through ` options[:include] `.
Support for top-level and nested included associations through `options[:include]`.
```ruby
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
MovieSerializer.new([movie, movie], options).serialized_json
options[:links] = {
self: '...',
next: '...',
prev: '...'
}
options[:include] = [:actors, :'actors.agency', :'actors.agency.state']
MovieSerializer.new(movies, options).serializable_hash.to_json
```
### Collection Serialization
```ruby
options[:meta] = { total: 2 }
hash = MovieSerializer.new([movie, movie], options).serializable_hash
json_string = MovieSerializer.new([movie, movie], options).serialized_json
options[:links] = {
self: '...',
next: '...',
prev: '...'
}
hash = MovieSerializer.new(movies, options).serializable_hash
json_string = MovieSerializer.new(movies, options).serializable_hash.to_json
```
#### Control Over Collection Serialization
You can use `is_collection` option to have better control over collection serialization.
If this option is not provided or `nil` autodetect logic is used to try understand
if provided resource is a single object or collection.
Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but
**cannot** guarantee that single vs collection will be always detected properly.
```ruby
options[:is_collection]
```
was introduced to be able to have precise control this behavior
- `nil` or not provided: will try to autodetect single vs collection (please, see notes above)
- `true` will always treat input resource as *collection*
- `false` will always treat input resource as *single object*
### Caching
To enable caching, use `cache_options store: <cache_store>`:
```ruby
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie # optional
cache_options enabled: true, cache_length: 12.hours
include JSONAPI::Serializer
# use rails cache with a separate namespace and fixed expiry
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
```
`store` is required can be anything that implements a
`#fetch(record, **options, &block)` method:
- `record` is the record that is currently serialized
- `options` is everything that was passed to `cache_options` except `store`, so it can be everyhing the cache store supports
- `&block` should be executed to fetch new data if cache is empty
So for the example above it will call the cache instance like this:
```ruby
Rails.cache.fetch(record, namespace: 'jsonapi-serializer', expires_in: 1.hour) { ... }
```
#### Caching and Sparse Fieldsets
If caching is enabled and fields are provided to the serializer, the fieldset will be appended to the cache key's namespace.
For example, given the following serializer definition and instance:
```ruby
class ActorSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name
cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
end
serializer = ActorSerializer.new(actor, { fields: { actor: [:first_name] } })
```
The following cache namespace will be generated: `'jsonapi-serializer-fieldset:first_name'`.
### Params
In some cases, attribute values might require more information than what is
available on the record, for example, access privileges or other information
related to a current authenticated user. The `options[:params]` value covers these
cases by allowing you to pass in a hash of additional parameters necessary for
your use case.
Leveraging the new params is easy, when you define a custom id, attribute or
relationship with a block you opt-in to using params by adding it as a block
parameter.
```ruby
class MovieSerializer
include JSONAPI::Serializer
set_id do |movie, params|
# in here, params is a hash containing the `:admin` key
params[:admin] ? movie.owner_id : "movie-#{movie.id}"
end
attributes :name, :year
attribute :can_view_early do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user].is_employee? ? true : false
end
belongs_to :primary_agent do |movie, params|
# in here, params is a hash containing the `:current_user` key
params[:current_user]
end
end
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, {params: {current_user: current_user}})
serializer.serializable_hash
```
Custom attributes and relationships that only receive the resource are still possible by defining
the block to only receive one argument.
### Conditional Attributes
Conditional attributes can be defined by passing a Proc to the `if` key on the `attribute` method. Return `true` if the attribute should be serialized, and `false` if not. 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
attributes :name, :year
attribute :release_year, if: Proc.new { |record|
# Release year will only be serialized if it's greater than 1990
record.release_year > 1990
}
attribute :director, if: Proc.new { |record, params|
# 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
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
serializer.serializable_hash
```
### Conditional Relationships
Conditional relationships can be defined by passing a Proc to the `if` key. Return `true` if the relationship should be serialized, and `false` if not. 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
# Actors will only be serialized if the record has any associated actors
has_many :actors, if: Proc.new { |record| record.actors.any? }
# Owner will only be serialized if the :admin key of params is true
belongs_to :owner, if: Proc.new { |record, params| params && params[:admin] == true }
end
# ...
current_user = User.find(cookies[:current_user_id])
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
serializer.serializable_hash
```
### Specifying a Relationship Serializer
In many cases, the relationship can automatically detect the serializer to use.
```ruby
class MovieSerializer
include JSONAPI::Serializer
# resolves to StudioSerializer
belongs_to :studio
# resolves to ActorSerializer
has_many :actors
end
```
At other times, such as when a property name differs from the class name, you may need to explicitly state the serializer to use. You can do so by specifying a different symbol or the serializer class itself (which is the recommended usage):
```ruby
class MovieSerializer
include JSONAPI::Serializer
# resolves to MovieStudioSerializer
belongs_to :studio, serializer: :movie_studio
# resolves to PerformerSerializer
has_many :actors, serializer: PerformerSerializer
end
```
For more advanced cases, such as polymorphic relationships and Single Table Inheritance, you may need even greater control to select the serializer based on the specific object or some specified serialization parameters. You can do by defining the serializer as a `Proc`:
```ruby
class MovieSerializer
include JSONAPI::Serializer
has_many :actors, serializer: Proc.new do |record, params|
if record.comedian?
ComedianSerializer
elsif params[:use_drama_serializer]
DramaSerializer
else
ActorSerializer
end
end
end
```
### Sparse Fieldsets
Attributes and relationships can be selectively returned per record type by using the `fields` option.
```ruby
class MovieSerializer
include JSONAPI::Serializer
attributes :name, :year
end
serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } })
serializer.serializable_hash
```
### Using helper methods
You can mix-in code from another ruby module into your serializer class to reuse functions across your app.
Since a serializer is evaluated in a the context of a `class` rather than an `instance` of a class, you need to make sure that your methods act as `class` methods when mixed in.
##### Using ActiveSupport::Concern
``` ruby
module AvatarHelper
extend ActiveSupport::Concern
class_methods do
def avatar_url(user)
user.image.url
end
end
end
class UserSerializer
include JSONAPI::Serializer
include AvatarHelper # mixes in your helper method as class method
set_type :user
attributes :name, :email
attribute :avatar do |user|
avatar_url(user)
end
end
```
##### Using Plain Old Ruby
``` ruby
module AvatarHelper
def avatar_url(user)
user.image.url
end
end
class UserSerializer
include JSONAPI::Serializer
extend AvatarHelper # mixes in your helper method as class method
set_type :user
attributes :name, :email
attribute :avatar do |user|
avatar_url(user)
end
end
```
### Customizable Options
Option | Purpose | Example
------------ | ------------- | -------------
set_type | Type name of Object | ```set_type :movie ```
set_id | ID of Object | ```set_id :owner_id ```
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours```
id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ```
object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ```
record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user```
serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor```
set_type | Type name of Object | `set_type :movie`
key | Key of Object | `belongs_to :owner, key: :user`
set_id | ID of Object | `set_id :owner_id` or `set_id { \|record, params\| params[:admin] ? record.id : "#{record.name.downcase}-#{record.id}" }`
cache_options | Hash with store to enable caching and optional further cache options | `cache_options store: ActiveSupport::Cache::MemoryStore.new, expires_in: 5.minutes`
id_method_name | Set custom method name to get ID of an object (If block is provided for the relationship, `id_method_name` is invoked on the return value of the block instead of the resource object) | `has_many :locations, id_method_name: :place_ids`
object_method_name | Set custom method name to get related objects | `has_many :locations, object_method_name: :places`
record_type | Set custom Object Type for a relationship | `belongs_to :owner, record_type: :user`
serializer | Set custom Serializer for a relationship | `has_many :actors, serializer: :custom_actor`, `has_many :actors, serializer: MyApp::Api::V1::ActorSerializer`, or `has_many :actors, serializer -> (object, params) { (return a serializer class) }`
polymorphic | Allows different record types for a polymorphic association | `has_many :targets, polymorphic: true`
polymorphic | Sets custom record types for each object class in a polymorphic association | `has_many :targets, polymorphic: { Person => :person, Group => :group }`
### Instrumentation
### Performance Instrumentation
`fast_jsonapi` also has builtin [Skylight](https://www.skylight.io/) integration. To enable, add the following to an initializer:
Performance instrumentation is available by using the
`active_support/notifications`.
To enable it, include the module in your serializer class:
```ruby
require 'fast_jsonapi/instrumentation/skylight'
require 'jsonapi/serializer'
require 'jsonapi/serializer/instrumentation'
class MovieSerializer
include JSONAPI::Serializer
include JSONAPI::Serializer::Instrumentation
# ...
end
```
Skylight relies on `ActiveSupport::Notifications` to track these two core methods. If you would like to use these notifications without using Skylight, simply require the instrumentation integration:
```ruby
require 'fast_jsonapi/instrumentation'
```
The two instrumented notifcations are supplied by these two constants:
* `FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION`
* `FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION`
It is also possible to instrument one method without the other by using one of the following require statements:
```ruby
require 'fast_jsonapi/instrumentation/serializable_hash'
require 'fast_jsonapi/instrumentation/serialized_json'
```
Same goes for the Skylight integration:
```ruby
require 'fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash'
require 'fast_jsonapi/instrumentation/skylight/normalizers/serialized_json'
```
## Contributing
Please see [contribution check](https://github.com/Netflix/fast_jsonapi/blob/master/CONTRIBUTING.md) for more details on contributing
[Skylight](https://www.skylight.io/) integration is also available and
supported by us, follow the Skylight documentation to enable it.
### Running Tests
We use [RSpec](http://rspec.info/) for testing. We have unit tests, functional tests and performance tests. To run tests use the following command:
The project has and requires unit tests, functional tests and performance
tests. To run tests use the following command:
```bash
rspec
```
To run tests without the performance tests (for quicker test runs):
## Deserialization
We currently do not support deserialization, but we recommend to use any of the next gems:
```bash
rspec spec --tag ~performance:true
### [JSONAPI.rb](https://github.com/stas/jsonapi.rb)
This gem provides the next features alongside deserialization:
- Collection meta
- Error handling
- Includes and sparse fields
- Filtering and sorting
- Pagination
## Migrating from Netflix/fast_jsonapi
If you come from [Netflix/fast_jsonapi](https://github.com/Netflix/fast_jsonapi), here is the instructions to switch.
### Modify your Gemfile
```diff
- gem 'fast_jsonapi'
+ gem 'jsonapi-serializer'
```
To run tests only performance tests:
### Replace all constant references
```bash
rspec spec --tag performance:true
```diff
class MovieSerializer
- include FastJsonapi::ObjectSerializer
+ include JSONAPI::Serializer
end
```
### We're Hiring!
### Replace removed methods
Join the Netflix Studio Engineering team and help us build gems like this!
```diff
- json_string = MovieSerializer.new(movie).serialized_json
+ json_string = MovieSerializer.new(movie).serializable_hash.to_json
```
* [Senior Ruby Engineer](https://jobs.netflix.com/jobs/864893)
* [Senior Platform Engineer](https://jobs.netflix.com/jobs/865783)
### Replace require references
```diff
- require 'fast_jsonapi'
+ require 'jsonapi/serializer'
```
### Update your cache options
See [docs](https://github.com/jsonapi-serializer/jsonapi-serializer#caching).
```diff
- cache_options enabled: true, cache_length: 12.hours
+ cache_options store: Rails.cache, namespace: 'jsonapi-serializer', expires_in: 1.hour
```
## Contributing
Please follow the instructions we provide as part of the issue and
pull request creation processes.
This project is intended to be a safe, welcoming space for collaboration, and
contributors are expected to adhere to the
[Contributor Covenant](https://contributor-covenant.org) code of conduct.

15
Rakefile Normal file
View File

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

View File

@ -1 +0,0 @@
1.0.18

16
docs/ISSUE_TEMPLATE.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,18 @@
# frozen_string_literal: true
begin
require 'active_record'
::ActiveRecord::Associations::Builder::HasOne.class_eval do
# Based on
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11
def self.define_accessors(mixin, reflection)
super
name = reflection.name
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_id
association(:#{name}).reader.try(:id)
end
CODE
end
::ActiveRecord::Associations::Builder::HasOne.class_eval do
# Based on
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11
def self.define_accessors(mixin, reflection)
super
name = reflection.name
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_id
# if an attribute is already defined with this methods name we should just use it
return read_attribute(__method__) if has_attribute?(__method__)
association(:#{name}).reader.try(:id)
end
CODE
end
rescue LoadError
# active_record can't be loaded so we shouldn't try to monkey-patch it.
end

View File

@ -1,6 +1,12 @@
# frozen_string_literal: true
require 'jsonapi/serializer/errors'
module FastJsonapi
require 'fast_jsonapi/object_serializer'
require 'extensions/has_one'
if defined?(::Rails)
require 'fast_jsonapi/railtie'
elsif defined?(::ActiveRecord)
require 'extensions/has_one'
end
end

View File

@ -0,0 +1,5 @@
require 'fast_jsonapi/scalar'
module FastJsonapi
class Attribute < Scalar; end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +0,0 @@
require 'skylight'
require 'fast_jsonapi/instrumentation/serializable_hash'
module FastJsonapi
module Instrumentation
module Skylight
module Normalizers
class SerializedJson < Skylight::Normalizers::Normalizer
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

5
lib/fast_jsonapi/link.rb Normal file
View File

@ -0,0 +1,5 @@
require 'fast_jsonapi/scalar'
module FastJsonapi
class Link < Scalar; end
end

View File

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

View File

@ -1,8 +1,14 @@
# frozen_string_literal: true
require 'active_support/core_ext/object'
require 'active_support'
require 'active_support/time'
require 'active_support/concern'
require 'active_support/inflector'
require 'active_support/core_ext/numeric/time'
require 'fast_jsonapi/helpers'
require 'fast_jsonapi/attribute'
require 'fast_jsonapi/relationship'
require 'fast_jsonapi/link'
require 'fast_jsonapi/serialization_core'
module FastJsonapi
@ -10,8 +16,12 @@ module FastJsonapi
extend ActiveSupport::Concern
include SerializationCore
SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'.freeze
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'.freeze
TRANSFORMS_MAPPING = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}.freeze
included do
# Set record_type based on the name of the serializer class
@ -25,20 +35,23 @@ module FastJsonapi
end
def serializable_hash
return hash_for_collection if is_collection?(@resource)
if self.class.is_collection?(@resource, @is_collection)
return hash_for_collection
end
hash_for_one_record
end
alias_method :to_hash, :serializable_hash
alias to_hash serializable_hash
def hash_for_one_record
serializable_hash = { data: nil }
serializable_hash[:meta] = @meta if @meta.present?
serializable_hash[:links] = @links if @links.present?
return serializable_hash unless @resource
serializable_hash[:data] = self.class.record_hash(@resource)
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects) if @includes.present?
serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @includes, @params)
serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
serializable_hash
end
@ -47,72 +60,97 @@ module FastJsonapi
data = []
included = []
fieldset = @fieldsets[self.class.record_type.to_sym]
@resource.each do |record|
data << self.class.record_hash(record)
included.concat self.class.get_included_records(record, @includes, @known_included_objects) if @includes.present?
data << self.class.record_hash(record, fieldset, @includes, @params)
included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present?
end
serializable_hash[:data] = data
serializable_hash[:included] = included if @includes.present?
serializable_hash[:meta] = @meta if @meta.present?
serializable_hash[:links] = @links if @links.present?
serializable_hash
end
def serialized_json
self.class.to_json(serializable_hash)
end
private
def process_options(options)
@fieldsets = deep_symbolize(options[:fields].presence || {})
@params = {}
return if options.blank?
@known_included_objects = {}
@known_included_objects = Set.new
@meta = options[:meta]
@links = options[:links]
@is_collection = options[:is_collection]
@params = options[:params] || {}
raise ArgumentError, '`params` option passed to serializer must be a hash' unless @params.is_a?(Hash)
if options[:include].present?
@includes = options[:include].delete_if(&:blank?).map(&:to_sym)
validate_includes!(@includes)
@includes = options[:include].reject(&:blank?).map(&:to_sym)
self.class.validate_includes!(@includes)
end
end
def validate_includes!(includes)
return if includes.blank?
existing_relationships = self.class.relationships_to_serialize.keys.to_set
unless existing_relationships.superset?(includes.to_set)
raise ArgumentError, "One of keys from #{includes} is not specified as a relationship on the serializer"
def deep_symbolize(collection)
if collection.is_a? Hash
collection.each_with_object({}) do |(k, v), hsh|
hsh[k.to_sym] = deep_symbolize(v)
end
elsif collection.is_a? Array
collection.map { |i| deep_symbolize(i) }
else
collection.to_sym
end
end
def is_collection?(resource)
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?
subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
subclass.transform_method = transform_method
subclass.data_links = data_links.dup if data_links.present?
subclass.cache_store_instance = cache_store_instance
subclass.cache_store_options = cache_store_options
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
subclass.meta_to_serialize = meta_to_serialize
subclass.record_id = record_id
end
def reflected_record_type
return @reflected_record_type if defined?(@reflected_record_type)
@reflected_record_type ||= begin
if self.name.end_with?('Serializer')
self.name.split('::').last.chomp('Serializer').underscore.to_sym
end
end
@reflected_record_type ||= (name.split('::').last.chomp('Serializer').underscore.to_sym if name&.end_with?('Serializer'))
end
def set_key_transform(transform_name)
mapping = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}
@transform_method = mapping[transform_name.to_sym]
self.transform_method = TRANSFORMS_MAPPING[transform_name.to_sym]
# ensure that the record type is correctly transformed
if record_type
set_type(record_type)
# TODO: Remove dead code
elsif reflected_record_type
set_type(reflected_record_type)
end
end
def run_key_transform(input)
if @transform_method.present?
if transform_method.present?
input.to_s.send(*@transform_method).to_sym
else
input.to_sym
@ -128,109 +166,186 @@ module FastJsonapi
self.record_type = run_key_transform(type_name)
end
def set_id(id_name)
self.record_id = id_name
def set_id(id_name = nil, &block)
self.record_id = block || id_name
end
def cache_options(cache_options)
self.cached = cache_options[:enabled] || false
self.cache_length = cache_options[:cache_length] || 5.minutes
# FIXME: remove this if block once deprecated cache_options are not supported anymore
unless cache_options.key?(:store)
# fall back to old, deprecated behaviour because no store was passed.
# we assume the user explicitly wants new behaviour if he passed a
# store because this is the new syntax.
deprecated_cache_options(cache_options)
return
end
self.cache_store_instance = cache_options[:store]
self.cache_store_options = cache_options.except(:store)
end
# FIXME: remove this method once deprecated cache_options are not supported anymore
def deprecated_cache_options(cache_options)
warn('DEPRECATION WARNING: `store:` is a required cache option, we will default to `Rails.cache` for now. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.')
%i[enabled cache_length].select { |key| cache_options.key?(key) }.each do |key|
warn("DEPRECATION WARNING: `#{key}` is a deprecated cache option and will have no effect soon. See https://github.com/fast-jsonapi/fast_jsonapi#caching for more information.")
end
self.cache_store_instance = cache_options[:enabled] ? Rails.cache : nil
self.cache_store_options = {
expires_in: cache_options[:cache_length] || 5.minutes,
race_condition_ttl: cache_options[:race_condition_ttl] || 5.seconds
}
end
def attributes(*attributes_list, &block)
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
self.attributes_to_serialize = {} if attributes_to_serialize.nil?
# to support calling `attribute` with a lambda, e.g `attribute :key, ->(object) { ... }`
block = attributes_list.pop if attributes_list.last.is_a?(Proc)
attributes_list.each do |attr_name|
method_name = attr_name
key = run_key_transform(method_name)
attributes_to_serialize[key] = block || method_name
attributes_to_serialize[key] = Attribute.new(
key: key,
method: block || method_name,
options: options
)
end
end
alias_method :attribute, :attributes
def add_relationship(name, relationship)
def add_relationship(relationship)
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
if !relationship[:cached]
self.uncachable_relationships_to_serialize[name] = relationship
# TODO: Remove this undocumented option.
# Delegate the caching to the serializer exclusively.
if relationship.cached
cachable_relationships_to_serialize[relationship.name] = relationship
else
self.cachable_relationships_to_serialize[name] = relationship
uncachable_relationships_to_serialize[relationship.name] = relationship
end
self.relationships_to_serialize[name] = relationship
end
def has_many(relationship_name, options = {})
name = relationship_name.to_sym
singular_name = relationship_name.to_s.singularize
serializer_key = options[:serializer] || singular_name.to_sym
key = options[:key] || run_key_transform(relationship_name)
record_type = options[:record_type] || run_key_transform(singular_name)
relationship = {
key: key,
name: name,
id_method_name: options[:id_method_name] || (singular_name + '_ids').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_many,
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
}
add_relationship(name, relationship)
relationships_to_serialize[relationship.name] = relationship
end
def belongs_to(relationship_name, options = {})
name = relationship_name.to_sym
serializer_key = options[:serializer] || relationship_name.to_sym
key = options[:key] || run_key_transform(relationship_name)
record_type = options[:record_type] || run_key_transform(relationship_name)
add_relationship(name, {
key: key,
name: name,
id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :belongs_to,
cached: options[:cached] || true,
polymorphic: fetch_polymorphic_option(options)
})
def has_many(relationship_name, options = {}, &block)
relationship = create_relationship(relationship_name, :has_many, options, block)
add_relationship(relationship)
end
def has_one(relationship_name, options = {})
name = relationship_name.to_sym
serializer_key = options[:serializer] || name
key = options[:key] || run_key_transform(relationship_name)
record_type = options[:record_type] || run_key_transform(relationship_name)
add_relationship(name, {
key: key,
name: name,
id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym,
record_type: record_type,
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_one,
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
})
def has_one(relationship_name, options = {}, &block)
relationship = create_relationship(relationship_name, :has_one, options, block)
add_relationship(relationship)
end
def compute_serializer_name(serializer_key)
def belongs_to(relationship_name, options = {}, &block)
relationship = create_relationship(relationship_name, :belongs_to, options, block)
add_relationship(relationship)
end
def meta(meta_name = nil, &block)
self.meta_to_serialize = block || meta_name
end
def create_relationship(base_key, relationship_type, options, block)
name = base_key.to_sym
if relationship_type == :has_many
base_serialization_key = base_key.to_s.singularize
id_postfix = '_ids'
else
base_serialization_key = base_key
id_postfix = '_id'
end
polymorphic = fetch_polymorphic_option(options)
Relationship.new(
owner: self,
key: options[:key] || run_key_transform(base_key),
name: name,
id_method_name: compute_id_method_name(
options[:id_method_name],
"#{base_serialization_key}#{id_postfix}".to_sym,
polymorphic,
options[:serializer],
block
),
record_type: options[:record_type],
object_method_name: options[:object_method_name] || name,
object_block: block,
serializer: options[:serializer],
relationship_type: relationship_type,
cached: options[:cached],
polymorphic: polymorphic,
conditional_proc: options[:if],
transform_method: @transform_method,
meta: options[:meta],
links: options[:links],
lazy_load_data: options[:lazy_load_data]
)
end
def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, polymorphic, serializer, block)
if block.present? || serializer.is_a?(Proc) || polymorphic
custom_id_method_name || :id
else
custom_id_method_name || id_method_name_from_relationship
end
end
def serializer_for(name)
namespace = self.name.gsub(/()?\w+Serializer$/, '')
serializer_name = serializer_key.to_s.classify + 'Serializer'
return (namespace + serializer_name).to_sym if namespace.present?
(serializer_key.to_s.classify + 'Serializer').to_sym
serializer_name = "#{name.to_s.demodulize.classify}Serializer"
serializer_class_name = namespace + serializer_name
begin
serializer_class_name.constantize
rescue NameError
raise NameError, "#{self.name} cannot resolve a serializer class for '#{name}'. " \
"Attempted to find '#{serializer_class_name}'. " \
'Consider specifying the serializer directly through options[:serializer].'
end
end
def fetch_polymorphic_option(options)
option = options[:polymorphic]
return false unless option.present?
return option if option.respond_to? :keys
{}
end
# def link(link_name, link_method_name = nil, &block)
def link(*params, &block)
self.data_links = {} if data_links.nil?
options = params.last.is_a?(Hash) ? params.pop : {}
link_name = params.first
link_method_name = params[-1]
key = run_key_transform(link_name)
data_links[key] = Link.new(
key: key,
method: block || link_method_name,
options: options
)
end
def validate_includes!(includes)
return if includes.blank?
parse_includes_list(includes).each_key do |include_item|
relationship_to_include = relationships_to_serialize[include_item]
raise(JSONAPI::Serializer::UnsupportedIncludeError.new(include_item, name)) unless relationship_to_include
relationship_to_include.static_serializer # called for a side-effect to check for a known serializer class.
end
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'rails/railtie'
class Railtie < Rails::Railtie
initializer 'fast_jsonapi.active_record' do
ActiveSupport.on_load :active_record do
require 'extensions/has_one'
end
end
end

View File

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

View File

@ -0,0 +1,29 @@
module FastJsonapi
class Scalar
attr_reader :key, :method, :conditional_proc
def initialize(key:, method:, options: {})
@key = key
@method = method
@conditional_proc = options[:if]
end
def serialize(record, serialization_params, output_hash)
if conditionally_allowed?(record, serialization_params)
if method.is_a?(Proc)
output_hash[key] = FastJsonapi.call_proc(method, record, serialization_params)
else
output_hash[key] = record.public_send(method)
end
end
end
def conditionally_allowed?(record, serialization_params)
if conditional_proc.present?
FastJsonapi.call_proc(conditional_proc, record, serialization_params)
else
true
end
end
end
end

View File

@ -1,9 +1,12 @@
# frozen_string_literal: true
require 'active_support'
require 'active_support/concern'
require 'fast_jsonapi/multi_to_json'
require 'digest/sha1'
module FastJsonapi
MandatoryField = Class.new(StandardError)
module SerializationCore
extend ActiveSupport::Concern
@ -13,107 +16,184 @@ module FastJsonapi
:relationships_to_serialize,
:cachable_relationships_to_serialize,
:uncachable_relationships_to_serialize,
:transform_method,
:record_type,
:record_id,
:cache_length,
:cached
:cache_store_instance,
:cache_store_options,
:data_links,
:meta_to_serialize
end
end
class_methods do
def id_hash(id, record_type)
return { id: id.to_s, type: record_type } if id.present?
end
def ids_hash(ids, record_type)
return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
id_hash(ids, record_type) # ids variable is just a single id here
end
def id_hash_from_record(record, record_types)
# memoize the record type within the record_types dictionary, then assigning to record_type:
record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
{ id: record.id.to_s, type: record_type }
end
def ids_hash_from_record_and_relationship(record, relationship)
polymorphic = relationship[:polymorphic]
return ids_hash(
record.public_send(relationship[:id_method_name]),
relationship[:record_type]
) unless polymorphic
object_method_name = relationship.fetch(:object_method_name, relationship[:name])
return unless associated_object = record.send(object_method_name)
return associated_object.map do |object|
id_hash_from_record object, polymorphic
end if associated_object.respond_to? :map
id_hash_from_record associated_object, polymorphic
end
def attributes_hash(record)
attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash|
attr_hash[key] = method.is_a?(Proc) ? method.call(record) : record.public_send(method)
def id_hash(id, record_type, default_return = false)
if id.present?
{ id: id.to_s, type: record_type }
else
default_return ? { id: nil, type: record_type } : nil
end
end
def relationships_hash(record, relationships = nil)
def links_hash(record, params = {})
data_links.each_with_object({}) do |(_k, link), hash|
link.serialize(record, params, hash)
end
end
def attributes_hash(record, fieldset = nil, params = {})
attributes = attributes_to_serialize
attributes = attributes.slice(*fieldset) if fieldset.present?
attributes = {} if fieldset == []
attributes.each_with_object({}) do |(_k, attribute), hash|
attribute.serialize(record, params, hash)
end
end
def relationships_hash(record, relationships = nil, fieldset = nil, includes_list = nil, params = {})
relationships = relationships_to_serialize if relationships.nil?
relationships = relationships.slice(*fieldset) if fieldset.present?
relationships = {} if fieldset == []
relationships.each_with_object({}) do |(_k, relationship), hash|
name = relationship[:key]
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
hash[name] = {
data: ids_hash_from_record_and_relationship(record, relationship) || empty_case
}
relationships.each_with_object({}) do |(key, relationship), hash|
included = includes_list.present? && includes_list.include?(key)
relationship.serialize(record, included, params, hash)
end
end
def record_hash(record)
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do
id = record_id ? record.send(record_id) : record.id
temp_hash = id_hash(id, record_type) || { id: nil, type: record_type }
temp_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
temp_hash[:relationships] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present?
def meta_hash(record, params = {})
FastJsonapi.call_proc(meta_to_serialize, record, params)
end
def record_hash(record, fieldset, includes_list, params = {})
if cache_store_instance
cache_opts = record_cache_options(cache_store_options, fieldset, includes_list, params)
record_hash = cache_store_instance.fetch(record, **cache_opts) do
temp_hash = id_hash(id_from_record(record, params), record_type, true)
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
temp_hash[:relationships] = 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)) if uncachable_relationships_to_serialize.present?
record_hash
record_hash[:relationships] = (record_hash[:relationships] || {}).merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
else
id = record_id ? record.send(record_id) : record.id
record_hash = id_hash(id, record_type) || { id: nil, type: record_type }
record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record) if relationships_to_serialize.present?
record_hash
record_hash = id_hash(id_from_record(record, params), record_type, true)
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, fieldset, includes_list, params) if relationships_to_serialize.present?
record_hash[:links] = links_hash(record, params) if data_links.present?
end
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
end
# Override #to_json for alternative implementation
def to_json(payload)
FastJsonapi::MultiToJson.to_json(payload) if payload.present?
# 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
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
record.id
end
# 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 = parse_includes_list(includes_list)
includes_list.each_with_object([]) do |include_item, included_records|
relationship_item = relationships_to_serialize[include_item.first]
next unless relationship_item&.include_relationship?(record, params)
included_objects = Array(relationship_item.fetch_associated_object(record, params))
next if included_objects.empty?
static_serializer = relationship_item.static_serializer
static_record_type = relationship_item.static_record_type
def get_included_records(record, includes_list, known_included_objects)
includes_list.each_with_object([]) do |item, included_records|
object_method_name = @relationships_to_serialize[item][:object_method_name]
record_type = @relationships_to_serialize[item][:record_type]
serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
relationship_type = @relationships_to_serialize[item][:relationship_type]
included_objects = record.send(object_method_name)
next if included_objects.blank?
included_objects = [included_objects] unless relationship_type == :has_many
included_objects.each do |inc_obj|
code = "#{record_type}_#{inc_obj.id}"
next if known_included_objects.key?(code)
known_included_objects[code] = inc_obj
included_records << serializer.record_hash(inc_obj)
serializer = static_serializer || relationship_item.serializer_for(inc_obj, params)
record_type = static_record_type || serializer.record_type
if include_item.last.any?
serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end
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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,40 @@
require 'active_support'
require 'active_support/cache'
class User
attr_accessor :uid, :first_name, :last_name, :email
def self.fake(id = nil)
faked = new
faked.uid = id || SecureRandom.uuid
faked.first_name = FFaker::Name.first_name
faked.last_name = FFaker::Name.last_name
faked.email = FFaker::Internet.email
faked
end
end
class NoSerializerUser < User
end
class UserSerializer
include JSONAPI::Serializer
set_id :uid
attributes :first_name, :last_name, :email
meta do |obj|
{
email_length: obj.email.size
}
end
end
module Cached
class UserSerializer < ::UserSerializer
cache_options(
store: ActiveSupport::Cache::MemoryStore.new,
namespace: 'test'
)
end
end

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

@ -0,0 +1,80 @@
require 'active_support'
require 'active_support/cache'
require 'jsonapi/serializer/instrumentation'
class Actor < User
attr_accessor :movies, :movie_ids
def self.fake(id = nil)
faked = super(id)
faked.movies = []
faked.movie_ids = []
faked
end
def movie_urls
{
movie_url: movies[0]&.url
}
end
end
class ActorSerializer < UserSerializer
set_type :actor
attribute :email, if: ->(_object, params) { params[:conditionals_off].nil? }
has_many(
:played_movies,
serializer: :movie,
links: :movie_urls,
if: ->(_object, params) { params[:conditionals_off].nil? }
) do |object|
object.movies
end
end
class CamelCaseActorSerializer
include JSONAPI::Serializer
set_key_transform :camel
set_id :uid
set_type :user_actor
attributes :first_name
link :movie_url do |obj|
obj.movie_urls.values[0]
end
has_many(
:played_movies,
serializer: :movie
) do |object|
object.movies
end
end
class BadMovieSerializerActorSerializer < ActorSerializer
has_many :played_movies, serializer: :bad, object_method_name: :movies
end
module Cached
class ActorSerializer < ::ActorSerializer
# TODO: Fix this, the serializer gets cached on inherited classes...
has_many :played_movies, serializer: :movie do |object|
object.movies
end
cache_options(
store: ActiveSupport::Cache::MemoryStore.new,
namespace: 'test'
)
end
end
module Instrumented
class ActorSerializer < ::ActorSerializer
include ::JSONAPI::Serializer::Instrumentation
end
end

127
spec/fixtures/movie.rb vendored Normal file
View File

@ -0,0 +1,127 @@
class Movie
attr_accessor(
:id,
:name,
:year,
:actor_or_user,
:actors,
:actor_ids,
:polymorphics,
:owner,
:owner_id
)
def self.fake(id = nil)
faked = new
faked.id = id || SecureRandom.uuid
faked.name = FFaker::Movie.title
faked.year = FFaker::Vehicle.year
faked.actors = []
faked.actor_ids = []
faked.polymorphics = []
faked
end
def url(obj = nil)
@url ||= FFaker::Internet.http_url
return @url if obj.nil?
"#{@url}?#{obj.hash}"
end
def owner=(ownr)
@owner = ownr
@owner_id = ownr.uid
end
def actors=(acts)
@actors = acts
@actor_ids = actors.map do |actor|
actor.movies << self
actor.uid
end
end
end
class MovieSerializer
include JSONAPI::Serializer
set_type :movie
attribute :released_in_year, &:year
attributes :name
attribute :release_year do |object, _params|
object.year
end
link :self, :url
belongs_to :owner, serializer: UserSerializer
belongs_to :actor_or_user,
id_method_name: :uid,
polymorphic: {
Actor => :actor,
User => :user
}
has_many(
:actors,
meta: proc { |record, _| { count: record.actors.length } },
links: {
actors_self: :url,
related: ->(obj) { obj.url(obj) }
}
)
has_one(
:creator,
object_method_name: :owner,
id_method_name: :uid,
serializer: ->(object, _params) { UserSerializer if object.is_a?(User) }
)
has_many(
:actors_and_users,
id_method_name: :uid,
polymorphic: {
Actor => :actor,
User => :user
}
) do |obj|
obj.polymorphics
end
has_many(
:dynamic_actors_and_users,
id_method_name: :uid,
polymorphic: true
) do |obj|
obj.polymorphics
end
has_many(
:auto_detected_actors_and_users,
id_method_name: :uid
) do |obj|
obj.polymorphics
end
end
module Cached
class MovieSerializer < ::MovieSerializer
cache_options(
store: ActorSerializer.cache_store_instance,
namespace: 'test'
)
has_one(
:creator,
id_method_name: :uid,
serializer: :actor,
# TODO: Remove this undocumented option.
# Delegate the caching to the serializer exclusively.
cached: false
) do |obj|
obj.owner
end
end
end

View File

@ -0,0 +1,63 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) do
act = Actor.fake
act.movies = [Movie.fake]
act
end
let(:params) { {} }
let(:serialized) do
ActorSerializer.new(actor, params).serializable_hash.as_json
end
describe 'attributes' do
it do
expect(serialized['data']).to have_id(actor.uid)
expect(serialized['data']).to have_type('actor')
expect(serialized['data'])
.to have_jsonapi_attributes('first_name', 'last_name', 'email').exactly
expect(serialized['data']).to have_attribute('first_name')
.with_value(actor.first_name)
expect(serialized['data']).to have_attribute('last_name')
.with_value(actor.last_name)
expect(serialized['data']).to have_attribute('email')
.with_value(actor.email)
end
context 'with nil identifier' do
before { actor.uid = nil }
it { expect(serialized['data']).to have_id(nil) }
end
context 'with `if` conditions' do
let(:params) { { params: { conditionals_off: 'yes' } } }
it do
expect(serialized['data']).not_to have_attribute('email')
end
end
context 'with include and fields' do
let(:params) do
{
include: [:played_movies],
fields: { movie: [:release_year], actor: [:first_name] }
}
end
it do
expect(serialized['data'])
.to have_jsonapi_attributes(:first_name).exactly
expect(serialized['included']).to include(
have_type('movie')
.and(have_id(actor.movies[0].id))
.and(have_jsonapi_attributes('release_year').exactly)
)
end
end
end
end

View File

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

View File

@ -0,0 +1,25 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) { Actor.fake }
let(:params) { {} }
describe 'with errors' do
it do
expect do
BadMovieSerializerActorSerializer.new(
actor, include: ['played_movies']
)
end.to raise_error(
NameError, /cannot resolve a serializer class for 'bad'/
)
end
it do
expect { ActorSerializer.new(actor, include: ['bad_include']) }
.to raise_error(
JSONAPI::Serializer::UnsupportedIncludeError, /bad_include is not specified as a relationship/
)
end
end
end

View File

@ -0,0 +1,29 @@
require 'spec_helper'
# Needed to subscribe to `active_support/notifications`
require 'concurrent'
RSpec.describe JSONAPI::Serializer do
let(:serializer) do
Instrumented::ActorSerializer.new(Actor.fake)
end
it do
payload = event_name = nil
notification_name =
"#{::JSONAPI::Serializer::Instrumentation::NOTIFICATION_NAMESPACE}serializable_hash"
ActiveSupport::Notifications.subscribe(
notification_name
) do |ev_name, _s, _f, _i, ev_payload|
event_name = ev_name
payload = ev_payload
end
expect(serializer.serializable_hash).not_to be_nil
expect(event_name).to eq('render.jsonapi-serializer.serializable_hash')
expect(payload[:name]).to eq(serializer.class.name)
expect(payload[:serializer]).to eq(serializer.class)
end
end

View File

@ -0,0 +1,19 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:actor) { Actor.fake }
let(:params) { {} }
let(:serialized) do
CamelCaseActorSerializer.new(actor, params).serializable_hash.as_json
end
describe 'camel case key tranformation' do
it do
expect(serialized['data']).to have_id(actor.uid)
expect(serialized['data']).to have_type('UserActor')
expect(serialized['data']).to have_attribute('FirstName')
expect(serialized['data']).to have_relationship('PlayedMovies')
expect(serialized['data']).to have_link('MovieUrl').with_value(nil)
end
end
end

View File

@ -0,0 +1,47 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:movie) do
faked = Movie.fake
faked.actors = [Actor.fake]
faked
end
let(:params) { {} }
let(:serialized) do
MovieSerializer.new(movie, params).serializable_hash.as_json
end
describe 'links' do
it do
expect(serialized['data']).to have_link('self').with_value(movie.url)
expect(serialized['data']['relationships']['actors'])
.to have_link('actors_self').with_value(movie.url)
expect(serialized['data']['relationships']['actors'])
.to have_link('related').with_value(movie.url(movie))
end
context 'with included records' do
let(:serialized) do
ActorSerializer.new(movie.actors[0]).serializable_hash.as_json
end
it do
expect(serialized['data']['relationships']['played_movies'])
.to have_link('movie_url').with_value(movie.url)
end
end
context 'with root link' do
let(:params) do
{
links: { 'root_link' => FFaker::Internet.http_url }
}
end
it do
expect(serialized)
.to have_link('root_link').with_value(params[:links]['root_link'])
end
end
end
end

View File

@ -0,0 +1,25 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:user) { User.fake }
let(:params) { {} }
let(:serialized) do
UserSerializer.new(user, params).serializable_hash.as_json
end
it do
expect(serialized['data']).to have_meta('email_length' => user.email.size)
end
context 'with root meta' do
let(:params) do
{
meta: { 'code' => FFaker::Internet.password }
}
end
it do
expect(serialized).to have_meta(params[:meta])
end
end
end

View File

@ -0,0 +1,146 @@
require 'spec_helper'
RSpec.describe JSONAPI::Serializer do
let(:movie) do
mov = Movie.fake
mov.actors = rand(2..5).times.map { Actor.fake }
mov.owner = User.fake
poly_act = Actor.fake
poly_act.movies = [Movie.fake]
mov.polymorphics = [User.fake, poly_act]
mov.actor_or_user = Actor.fake
mov
end
let(:params) { {} }
let(:serialized) do
MovieSerializer.new(movie, params).serializable_hash.as_json
end
describe 'relationships' do
it do
actors_rel = movie.actors.map { |a| { 'id' => a.uid, 'type' => 'actor' } }
expect(serialized['data'])
.to have_relationship('actors').with_data(actors_rel)
expect(serialized['data'])
.to have_relationship('owner')
.with_data('id' => movie.owner.uid, 'type' => 'user')
expect(serialized['data'])
.to have_relationship('creator')
.with_data('id' => movie.owner.uid, 'type' => 'user')
expect(serialized['data'])
.to have_relationship('actors_and_users')
.with_data(
[
{ 'id' => movie.polymorphics[0].uid, 'type' => 'user' },
{ 'id' => movie.polymorphics[1].uid, 'type' => 'actor' }
]
)
expect(serialized['data'])
.to have_relationship('dynamic_actors_and_users')
.with_data(
[
{ 'id' => movie.polymorphics[0].uid, 'type' => 'user' },
{ 'id' => movie.polymorphics[1].uid, 'type' => 'actor' }
]
)
expect(serialized['data'])
.to have_relationship('auto_detected_actors_and_users')
.with_data(
[
{ 'id' => movie.polymorphics[0].uid, 'type' => 'user' },
{ 'id' => movie.polymorphics[1].uid, 'type' => 'actor' }
]
)
end
describe 'has relationship meta' do
it do
expect(serialized['data']['relationships']['actors'])
.to have_meta('count' => movie.actors.length)
end
end
context 'with include' do
let(:params) do
{ include: [:actors] }
end
it do
movie.actors.each do |actor|
expect(serialized['included']).to include(
have_type('actor')
.and(have_id(actor.uid))
.and(have_relationship('played_movies')
.with_data([{ 'id' => actor.movies[0].id, 'type' => 'movie' }]))
)
end
end
context 'with `if` conditions' do
let(:params) do
{
include: ['actors'],
params: { conditionals_off: 'yes' }
}
end
it do
movie.actors.each do |actor|
expect(serialized['included']).not_to include(
have_type('actor')
.and(have_id(actor.uid))
.and(have_relationship('played_movies'))
)
end
end
end
context 'with has_many polymorphic' do
let(:params) do
{ include: ['actors_and_users.played_movies'] }
end
it do
expect(serialized['included']).to include(
have_type('user').and(have_id(movie.polymorphics[0].uid))
)
expect(serialized['included']).to include(
have_type('movie').and(have_id(movie.polymorphics[1].movies[0].id))
)
expect(serialized['included']).to include(
have_type('actor')
.and(have_id(movie.polymorphics[1].uid))
.and(
have_relationship('played_movies').with_data(
[{
'id' => movie.polymorphics[1].movies[0].id,
'type' => 'movie'
}]
)
)
)
end
end
context 'with belongs_to polymorphic' do
let(:params) do
{ include: ['actor_or_user'] }
end
it do
expect(serialized['included']).to include(
have_type('actor').and(have_id(movie.actor_or_user.uid))
)
end
end
end
end
end

View File

@ -1,73 +0,0 @@
require 'spec_helper'
require 'active_record'
require 'sqlite3'
describe 'active record' do
# Setup DB
before(:all) do
@db_file = "test.db"
# Open a database
db = SQLite3::Database.new @db_file
# Create tables
db.execute_batch <<-SQL
create table suppliers (
name varchar(30),
id int primary key
);
create table accounts (
name varchar(30),
id int primary key,
supplier_id int,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
);
SQL
# Insert records
@account_id = 2
@supplier_id = 1
@supplier_id_without_account = 3
db.execute_batch <<-SQL
insert into suppliers values ('Supplier1', #{@supplier_id}),
('SupplierWithoutAccount', #{@supplier_id_without_account});
insert into accounts values ('Dollar Account', #{@account_id}, #{@supplier_id});
SQL
end
# Setup Active Record
before(:all) do
class Supplier < ActiveRecord::Base
has_one :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
end
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => @db_file
)
end
context 'has one patch' do
it 'has account_id method for a supplier' do
expect(Supplier.first.respond_to?(:account_id)).to be true
expect(Supplier.first.account_id).to eq @account_id
end
it 'has account_id method return nil if account not present' do
expect(Supplier.find(@supplier_id_without_account).account_id).to eq nil
end
end
# Clean up DB
after(:all) do
File.delete(@db_file) if File.exist?(@db_file)
end
end

View File

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

View File

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

View File

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

View File

@ -1,68 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when caching has_many' do
before(:each) do
rails = OpenStruct.new
rails.cache = ActiveSupport::Cache::MemoryStore.new
stub_const('Rails', rails)
end
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
serializable_hash = CachingMovieSerializer.new([movie, movie], options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included][0]).to be_instance_of(Hash)
expect(serializable_hash[:included].length).to eq 3
serializable_hash = CachingMovieSerializer.new(movie).serializable_hash
expect(serializable_hash[:data]).to be_instance_of(Hash)
expect(serializable_hash[:meta]).to be nil
expect(serializable_hash[:included]).to be nil
end
it 'uses cached values for the record' do
previous_name = movie.name
previous_actors = movie.actors
CachingMovieSerializer.new(movie).serializable_hash
movie.name = 'should not match'
allow(movie).to receive(:actor_ids).and_return([99])
expect(previous_name).not_to eq(movie.name)
expect(previous_actors).not_to eq(movie.actors)
serializable_hash = CachingMovieSerializer.new(movie).serializable_hash
expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq movie.actors.length
end
it 'uses cached values for has many as specified' do
previous_name = movie.name
previous_actors = movie.actors
CachingMovieWithHasManySerializer.new(movie).serializable_hash
movie.name = 'should not match'
allow(movie).to receive(:actor_ids).and_return([99])
expect(previous_name).not_to eq(movie.name)
expect(previous_actors).not_to eq(movie.actors)
serializable_hash = CachingMovieWithHasManySerializer.new(movie).serializable_hash
expect(serializable_hash[:data][:attributes][:name]).to eq(previous_name)
expect(serializable_hash[:data][:relationships][:actors][:data].length).to eq previous_actors.length
end
end
end

View File

@ -1,76 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when testing class methods of object serializer' do
before(:example) do
MovieSerializer.relationships_to_serialize = {}
end
it 'returns correct relationship hash for a has_many relationship' do
MovieSerializer.has_many :roles
relationship = MovieSerializer.relationships_to_serialize[:roles]
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:id_method_name]).to end_with '_ids'
expect(relationship[:record_type]).to eq 'roles'.singularize.to_sym
end
it 'returns correct relationship hash for a has_many relationship with overrides' do
MovieSerializer.has_many :roles, id_method_name: :roles_only_ids, record_type: :super_role
relationship = MovieSerializer.relationships_to_serialize[:roles]
expect(relationship[:id_method_name]).to be :roles_only_ids
expect(relationship[:record_type]).to be :super_role
end
it 'returns correct relationship hash for a belongs_to relationship' do
MovieSerializer.belongs_to :area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:id_method_name]).to end_with '_id'
expect(relationship[:record_type]).to eq 'area'.singularize.to_sym
end
it 'returns correct relationship hash for a belongs_to relationship with overrides' do
MovieSerializer.has_many :area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship[:id_method_name]).to be :blah_id
expect(relationship[:record_type]).to be :awesome_area
expect(relationship[:serializer]).to be :MyAreaSerializer
end
it 'returns correct relationship hash for a has_one relationship' do
MovieSerializer.has_one :area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship).to be_instance_of(Hash)
expect(relationship.keys).to all(be_instance_of(Symbol))
expect(relationship[:id_method_name]).to end_with '_id'
expect(relationship[:record_type]).to eq 'area'.singularize.to_sym
end
it 'returns correct relationship hash for a has_one relationship with overrides' do
MovieSerializer.has_one :area, id_method_name: :blah_id, record_type: :awesome_area
relationship = MovieSerializer.relationships_to_serialize[:area]
expect(relationship[:id_method_name]).to be :blah_id
expect(relationship[:record_type]).to be :awesome_area
end
it 'returns serializer name correctly with namespaces' do
AppName::V1::MovieSerializer.has_many :area, id_method_name: :blah_id
relationship = AppName::V1::MovieSerializer.relationships_to_serialize[:area]
expect(relationship[:serializer]).to be :'AppName::V1::AreaSerializer'
end
it 'sets the correct transform_method when use_hyphen is used' do
MovieSerializer.use_hyphen
warning_message = 'DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead'
expect { MovieSerializer.use_hyphen }.to output.to_stderr
expect(MovieSerializer.instance_variable_get(:@transform_method)).to eq :dasherize
end
end
end

View File

@ -1,80 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
include_context 'ams movie class'
before(:context) do
[:dash, :camel, :camel_lower, :underscore].each do |transform_type|
movie_serializer_name = "#{transform_type}_movie_serializer".classify
movie_type_serializer_name = "#{transform_type}_movie_type_serializer".classify
# https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
movie_serializer_class = Object.const_set(
movie_serializer_name,
Class.new {
}
)
# https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/67-instance-eval
movie_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_type :movie
set_key_transform transform_type
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
movie_type_serializer_class = Object.const_set(
movie_type_serializer_name,
Class.new {
}
)
movie_type_serializer_class.instance_eval do
include FastJsonapi::ObjectSerializer
set_key_transform transform_type
set_type :movie_type
attributes :name
end
end
end
context 'when using dashes for word separation in the JSON API members' do
it 'returns correct hash when serializable_hash is called' do
serializable_hash = DashMovieSerializer.new([movie, movie]).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:relationships]).to have_key('movie-type'.to_sym)
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:data][0][:attributes]).to have_key("release-year".to_sym)
serializable_hash = DashMovieSerializer.new(movie_struct).serializable_hash
expect(serializable_hash[:data][:relationships].length).to eq 3
expect(serializable_hash[:data][:relationships]).to have_key('movie-type'.to_sym)
expect(serializable_hash[:data][:attributes].length).to eq 2
expect(serializable_hash[:data][:attributes]).to have_key('release-year'.to_sym)
expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s
end
it 'returns type hypenated when trying to serializing a class with multiple words' do
movie_type = MovieType.new
movie_type.id = 3
movie_type.name = "x"
serializable_hash = DashMovieTypeSerializer.new(movie_type).serializable_hash
expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym
end
end
context 'when using other key transforms' do
[:camel, :camel_lower, :underscore, :dash].each do |transform_type|
it "returns same thing as ams when using #{transform_type}" do
ams_movie = build_ams_movies(1).first
movie = build_movies(1).first
movie_serializer_class = "#{transform_type}_movie_serializer".classify.constantize
our_json = movie_serializer_class.new([movie]).serialized_json
ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: transform_type).to_json
expect(our_json.length).to eq (ams_json.length)
end
end
end
end

View File

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

View File

@ -1,30 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when setting id' do
subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash }
before(:all) do
MovieSerializer.set_id :owner_id
end
context 'when one record is given' do
let(:resource) { movie }
it 'returns correct hash which id equals owner_id' do
expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id
end
end
context 'when an array of records is given' do
let(:resource) { [movie, movie] }
it 'returns correct hash which id equals owner_id' do
expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id
expect(serializable_hash[:data][1][:id].to_i).to eq movie.owner_id
end
end
end
end

View File

@ -1,157 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when testing instance methods of object serializer' do
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
serializable_hash = MovieSerializer.new([movie, movie], options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included][0]).to be_instance_of(Hash)
expect(serializable_hash[:included].length).to eq 3
serializable_hash = MovieSerializer.new(movie).serializable_hash
expect(serializable_hash[:data]).to be_instance_of(Hash)
expect(serializable_hash[:meta]).to be nil
expect(serializable_hash[:included]).to be nil
end
it 'returns correct number of records when serialized_json is called for an array' do
options = {}
options[:meta] = { total: 2 }
json = MovieSerializer.new([movie, movie], options).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data'].length).to eq 2
expect(serializable_hash['meta']).to be_instance_of(Hash)
end
it 'returns correct id when serialized_json is called for a single object' do
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['id']).to eq movie.id.to_s
end
it 'returns correct json when serializing nil' do
json = MovieSerializer.new(nil).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']).to eq nil
end
it 'returns correct json when record id is nil' do
movie.id = nil
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['id']).to be nil
end
it 'returns correct json when has_many returns []' do
movie.actor_ids = []
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['actors']['data'].length).to eq 0
end
it 'returns correct json when belongs_to returns nil' do
movie.owner_id = nil
json = MovieSerializer.new(movie).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['owner']['data']).to be nil
end
it 'returns correct json when has_one returns nil' do
supplier.account_id = nil
json = SupplierSerializer.new(supplier).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']['relationships']['account']['data']).to be nil
end
it 'returns correct json when serializing []' do
json = MovieSerializer.new([]).serialized_json
serializable_hash = JSON.parse(json)
expect(serializable_hash['data']).to eq []
end
describe '#as_json' do
it 'returns a json hash' do
json_hash = MovieSerializer.new(movie).as_json
expect(json_hash['data']['id']).to eq movie.id.to_s
end
it 'returns multiple records' do
json_hash = MovieSerializer.new([movie, movie]).as_json
expect(json_hash['data'].length).to eq 2
end
it 'removes non-relevant attributes' do
movie.director = 'steven spielberg'
json_hash = MovieSerializer.new(movie).as_json
expect(json_hash['data']['director']).to eq(nil)
end
end
it 'returns errors when serializing with non-existent includes key' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:blah_blah]
expect { MovieSerializer.new([movie, movie], options).serializable_hash }.to raise_error(ArgumentError)
end
it 'does not throw an error with non-empty string array includes key' do
options = {}
options[:include] = ['actors']
expect { MovieSerializer.new(movie, options) }.not_to raise_error
end
it 'returns keys when serializing with empty string/nil array includes key' do
options = {}
options[:meta] = { total: 2 }
options[:include] = ['']
expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta]
options[:include] = [nil]
expect(MovieSerializer.new([movie, movie], options).serializable_hash.keys).to eq [:data, :meta]
end
end
context 'when testing included do block of object serializer' do
it 'should set default_type based on serializer class name' do
class BlahSerializer
include FastJsonapi::ObjectSerializer
end
expect(BlahSerializer.record_type).to be :blah
end
it 'should set default_type for a multi word class name' do
class BlahBlahSerializer
include FastJsonapi::ObjectSerializer
end
expect(BlahBlahSerializer.record_type).to be :blah_blah
end
it 'shouldnt set default_type for a serializer that doesnt follow convention' do
class BlahBlahSerializerBuilder
include FastJsonapi::ObjectSerializer
end
expect(BlahBlahSerializerBuilder.record_type).to be_nil
end
it 'should set default_type for a namespaced serializer' do
module V1
class BlahSerializer
include FastJsonapi::ObjectSerializer
end
end
expect(V1::BlahSerializer.record_type).to be :blah
end
end
end

View File

@ -1,31 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when testing object serializer with ruby struct' do
it 'returns correct hash when serializable_hash is called' do
options = {}
options[:meta] = { total: 2 }
options[:include] = [:actors]
serializable_hash = MovieSerializer.new([movie_struct, movie_struct], options).serializable_hash
expect(serializable_hash[:data].length).to eq 2
expect(serializable_hash[:data][0][:relationships].length).to eq 3
expect(serializable_hash[:data][0][:attributes].length).to eq 2
expect(serializable_hash[:meta]).to be_instance_of(Hash)
expect(serializable_hash[:included]).to be_instance_of(Array)
expect(serializable_hash[:included][0]).to be_instance_of(Hash)
expect(serializable_hash[:included].length).to eq 3
serializable_hash = MovieSerializer.new(movie_struct).serializable_hash
expect(serializable_hash[:data]).to be_instance_of(Hash)
expect(serializable_hash[:meta]).to be nil
expect(serializable_hash[:included]).to be nil
expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s
end
end
end

View File

@ -1,13 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context 'movie class'
context 'when including attribute blocks' do
it 'returns correct hash when serializable_hash is called' do
serializable_hash = MovieSerializerWithAttributeBlock.new([movie]).serializable_hash
expect(serializable_hash[:data][0][:attributes][:name]).to eq movie.name
expect(serializable_hash[:data][0][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})"
end
end
end

View File

@ -1,90 +0,0 @@
require 'spec_helper'
describe FastJsonapi::ObjectSerializer do
include_context "movie class"
include_context 'group class'
context 'when testing class methods of serialization core' do
it 'returns correct hash when id_hash is called' do
inputs = [{id: 23, record_type: :movie}, {id: 'x', record_type: 'person'}]
inputs.each do |hash|
result_hash = MovieSerializer.send(:id_hash, hash[:id], hash[:record_type])
expect(result_hash[:id]).to eq hash[:id].to_s
expect(result_hash[:type]).to eq hash[:record_type]
end
result_hash = MovieSerializer.send(:id_hash, nil, 'movie')
expect(result_hash).to be nil
end
it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do
relationship = { name: :groupees, relationship_type: :has_many, polymorphic: {} }
results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship
expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group })
end
it 'returns correct hash when ids_hash is called' do
inputs = [{ids: %w(1 2 3), record_type: :movie}, {ids: %w(x y z), record_type: 'person'}]
inputs.each do |hash|
results = MovieSerializer.send(:ids_hash, hash[:ids], hash[:record_type])
expect(results.map{|h| h[:id]}).to eq hash[:ids]
expect(results[0][:type]).to eq hash[:record_type]
end
result = MovieSerializer.send(:ids_hash, [], 'movie')
expect(result).to be_empty
end
it 'returns correct hash when attributes_hash is called' do
attributes_hash = MovieSerializer.send(:attributes_hash, movie)
attribute_names = attributes_hash.keys.sort
expect(attribute_names).to eq MovieSerializer.attributes_to_serialize.keys.sort
MovieSerializer.attributes_to_serialize.each do |key, method_name|
value = attributes_hash[key]
expect(value).to eq movie.send(method_name)
end
end
it 'returns the correct empty result when relationships_hash is called' do
movie.actor_ids = []
movie.owner_id = nil
relationships_hash = MovieSerializer.send(:relationships_hash, movie)
expect(relationships_hash[:actors][:data]).to eq([])
expect(relationships_hash[:owner][:data]).to eq(nil)
end
it 'returns correct keys when relationships_hash is called' do
relationships_hash = MovieSerializer.send(:relationships_hash, movie)
relationship_names = relationships_hash.keys.sort
relationships_hashes = MovieSerializer.relationships_to_serialize.values
expected_names = relationships_hashes.map{|relationship| relationship[:key]}.sort
expect(relationship_names).to eq expected_names
end
it 'returns correct values when relationships_hash is called' do
relationships_hash = MovieSerializer.relationships_hash(movie)
actors_hash = movie.actor_ids.map { |id| {id: id.to_s, type: :actor} }
owner_hash = {id: movie.owner_id.to_s, type: :user}
expect(relationships_hash[:actors][:data]).to match_array actors_hash
expect(relationships_hash[:owner][:data]).to eq owner_hash
end
it 'returns correct hash when record_hash is called' do
record_hash = MovieSerializer.send(:record_hash, movie)
expect(record_hash[:id]).to eq movie.id.to_s
expect(record_hash[:type]).to eq MovieSerializer.record_type
expect(record_hash).to have_key(:attributes) if MovieSerializer.attributes_to_serialize.present?
expect(record_hash).to have_key(:relationships) if MovieSerializer.relationships_to_serialize.present?
end
it 'serializes known included records only once' do
includes_list = [:actors]
known_included_objects = {}
included_records = []
[movie, movie].each do |record|
included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects)
end
expect(included_records.size).to eq 3
end
end
end

View File

@ -1,84 +0,0 @@
RSpec.shared_context 'ams movie class' do
before(:context) do
# models
class AMSModel < ActiveModelSerializers::Model
derive_attributes_from_names_and_fix_accessors
end
class AMSMovie < AMSModel
attributes :id, :name, :release_year, :actors, :owner, :movie_type
end
class AMSActor < AMSModel
attributes :id, :name, :email
end
class AMSUser < AMSModel
attributes :id, :name
end
class AMSMovieType < AMSModel
attributes :id, :name
end
# serializers
class AMSActorSerializer < ActiveModel::Serializer
type 'actor'
attributes :name, :email
end
class AMSUserSerializer < ActiveModel::Serializer
type 'user'
attributes :name
end
class AMSMovieTypeSerializer < ActiveModel::Serializer
type 'movie_type'
attributes :name
end
class AMSMovieSerializer < ActiveModel::Serializer
type 'movie'
attributes :name, :release_year
has_many :actors
has_one :owner
belongs_to :movie_type
end
end
after(:context) do
classes_to_remove = %i[AMSMovie AMSMovieSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:ams_actors) do
3.times.map do |i|
a = AMSActor.new
a.id = i + 1
a.name = "Test #{a.id}"
a.email = "test#{a.id}@test.com"
a
end
end
let(:ams_user) do
ams_user = AMSUser.new
ams_user.id = 3
ams_user
end
let(:ams_movie_type) do
ams_movie_type = AMSMovieType.new
ams_movie_type.id = 1
ams_movie_type.name = 'episode'
ams_movie_type
end
def build_ams_movies(count)
count.times.map do |i|
m = AMSMovie.new
m.id = i + 1
m.name = 'test movie'
m.actors = ams_actors
m.owner = ams_user
m.movie_type = ams_movie_type
m
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,224 +0,0 @@
RSpec.shared_context 'movie class' do
# Movie, Actor Classes and serializers
before(:context) do
# models
class Movie
attr_accessor :id,
:name,
:release_year,
:director,
:actor_ids,
:owner_id,
:movie_type_id
def actors
actor_ids.map do |id|
a = Actor.new
a.id = id
a.name = "Test #{a.id}"
a.email = "test#{a.id}@test.com"
a
end
end
def movie_type
mt = MovieType.new
mt.id = movie_type_id
mt.name = 'Episode'
mt
end
def cache_key
"#{id}"
end
end
class Actor
attr_accessor :id, :name, :email
end
class MovieType
attr_accessor :id, :name
end
class Supplier
attr_accessor :id, :account_id
def account
if account_id
a = Account.new
a.id = account_id
a
end
end
end
class Account
attr_accessor :id
end
# serializers
class MovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
# director attr is not mentioned intentionally
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
end
class CachingMovieSerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
has_many :actors
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
end
class CachingMovieWithHasManySerializer
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
has_many :actors, cached: true
belongs_to :owner, record_type: :user
belongs_to :movie_type
cache_options enabled: true
end
class ActorSerializer
include FastJsonapi::ObjectSerializer
set_type :actor
attributes :name, :email
end
class MovieTypeSerializer
include FastJsonapi::ObjectSerializer
set_type :movie_type
attributes :name
end
class MovieSerializerWithAttributeBlock
include FastJsonapi::ObjectSerializer
set_type :movie
attributes :name, :release_year
attribute :title_with_year do |record|
"#{record.name} (#{record.release_year})"
end
end
class SupplierSerializer
include FastJsonapi::ObjectSerializer
set_type :supplier
has_one :account
end
class AccountSerializer
include FastJsonapi::ObjectSerializer
set_type :account
belongs_to :supplier
end
end
# Namespaced MovieSerializer
before(:context) do
# namespaced model stub
module AppName
module V1
class MovieSerializer
include FastJsonapi::ObjectSerializer
# to test if compute_serializer_name works
end
end
end
end
# Movie and Actor struct
before(:context) do
MovieStruct = Struct.new(
:id,
:name,
:release_year,
:actor_ids,
:actors,
:owner_id,
:owner,
:movie_type_id
)
ActorStruct = Struct.new(:id, :name, :email)
end
after(:context) do
classes_to_remove = %i[
Movie
MovieSerializer
Actor
ActorSerializer
MovieType
MovieTypeSerializer
MovieSerializerWithAttributeBlock
AppName::V1::MovieSerializer
MovieStruct
ActorStruct
HyphenMovieSerializer
]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end
let(:movie_struct) do
actors = []
3.times.each do |id|
actors << ActorStruct.new(id, id.to_s, id.to_s)
end
m = MovieStruct.new
m[:id] = 23
m[:name] = 'struct movie'
m[:release_year] = 1987
m[:actor_ids] = [1,2,3]
m[:owner_id] = 3
m[:movie_type_id] = 2
m[:actors] = actors
m
end
let(:movie) do
m = Movie.new
m.id = 232
m.name = 'test movie'
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
let(:supplier) do
s = Supplier.new
s.id = 1
s.account_id = 1
s
end
def build_movies(count)
count.times.map do |i|
m = Movie.new
m.id = i + 1
m.name = 'test movie'
m.actor_ids = [1, 2, 3]
m.owner_id = 3
m.movie_type_id = 1
m
end
end
end

View File

@ -1,21 +1,30 @@
require 'fast_jsonapi'
require 'rspec-benchmark'
require 'byebug'
require 'active_model_serializers'
require 'oj'
require 'jsonapi/serializable'
require 'jsonapi-serializers'
require 'simplecov'
Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file }
SimpleCov.start do
add_group 'Lib', 'lib'
add_group 'Tests', 'spec'
end
SimpleCov.minimum_coverage 90
require 'active_support'
require 'active_support/core_ext/object/json'
require 'jsonapi/serializer'
require 'ffaker'
require 'rspec'
require 'jsonapi/rspec'
require 'byebug'
require 'securerandom'
Dir[File.expand_path('spec/fixtures/*.rb')].sort.each { |f| require f }
RSpec.configure do |config|
config.include RSpec::Benchmark::Matchers
if ENV['TRAVIS'] == 'true' || ENV['TRAVIS'] == true
config.filter_run_excluding performance: true
config.include JSONAPI::RSpec
config.mock_with :rspec
config.filter_run_when_matching :focus
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
Oj.optimize_rails
ActiveModel::Serializer.config.adapter = :json_api
ActiveModel::Serializer.config.key_transform = :underscore
ActiveModelSerializers.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new('/dev/null'))