Skip to content

Include data key when lazy-loaded relationships are included #425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ee76e0c
bump up version to 1.5
shishirmk Nov 3, 2018
fdcaed6
Merge pull request #342 from Netflix/release-1.5
shishirmk Nov 3, 2018
a160d67
Fix Documentation of Meta Per Resource
manojmj92 Nov 15, 2018
e0228da
Add documentation on how to use helper methods in serializers
manojmj92 Nov 14, 2018
4b1c2fb
Include `data` key when lazy-loaded relationships are specified with …
dpikt Jun 10, 2019
3df917f
Update README.md
Jun 11, 2019
d64b1b5
Test against Ruby 2.6 and fix Travis (#3)
krzysiek1507 Oct 4, 2019
d8e69a2
Merge branch 'dev' into include-lazy-loaded-relationships
dpikt Oct 4, 2019
1373eb4
Compute remaining_items once
krzysiek1507 May 2, 2019
2b6c816
Allow relationship links to be declared as object method (#2)
jopotts Oct 4, 2019
9ec89d4
Map split include_item in-place
krzysiek1507 May 2, 2019
dce1faf
Move transforms mapping to constant
krzysiek1507 May 2, 2019
0dc332d
Create CHANGELOG.md
kpheasey Oct 4, 2019
9e83c1e
Highlight that this is for JSON:API spec only
Mar 21, 2019
f0142d9
Fix formatting of `set_id` example in README
thisismydesign Mar 4, 2019
209c925
Fix typo in README
caristomenopoulos Feb 22, 2019
021db27
Update Readme
Feb 19, 2019
1a407c0
[#365] Support frozen array in option
dapi Dec 21, 2018
21ae4aa
Allow fieldsets to specify no attributes/relationships
Oct 8, 2018
8d8e5c3
Fix error on defining anonymous serializer class, fixes #353
daniel-illi Nov 22, 2018
267b706
validate all include items instead of just the first
Jan 4, 2019
5767664
add specs for multiple include options
Jan 4, 2019
b9a86a0
pass array of unique movies to serializer
sdsd08013 Dec 20, 2018
e68dbee
update document/use let statement
sdsd08013 Jan 21, 2019
b24af1f
Test against Ruby 2.6 and fix Travis (#3)
krzysiek1507 Oct 4, 2019
f04abfd
Compute remaining_items once
krzysiek1507 May 2, 2019
83e99b2
Allow relationship links to be declared as object method (#2)
jopotts Oct 4, 2019
fd17386
Map split include_item in-place
krzysiek1507 May 2, 2019
e4c65a2
Move transforms mapping to constant
krzysiek1507 May 2, 2019
5f86298
Merge remote-tracking branch 'origin/dev' into dev
kpheasey Oct 4, 2019
44a896d
Take items of original array instead of dup and delete
krzysiek1507 May 2, 2019
f2a1934
Use each_with_object instead of Hash[map]
krzysiek1507 May 2, 2019
37206dd
Update CHANGELOG.md
kpheasey Oct 4, 2019
7270a9a
Merge branch 'dev' into include-lazy-loaded-relationships
kpheasey Oct 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,12 @@ rvm:
- 2.3.6
- 2.4.3
- 2.5.0
- 2.6

before_install:
- "travis_retry gem update --system 2.7.9"
- "travis_retry gem install bundler -v '1.17.3'"
install: BUNDLER_VERSION=1.17.3 bundle install --path=vendor/bundle --retry=3 --jobs=3

script:
- bundle exec rspec
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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]
### 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))
### 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))

[Unreleased]: https://github.com/fast-jsonapi/fast_jsonapi/compare/dev...HEAD
80 changes: 74 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Fast JSON API serialized 250 records in 3.01 ms
* [Conditional Attributes](#conditional-attributes)
* [Conditional Relationships](#conditional-relationships)
* [Sparse Fieldsets](#sparse-fieldsets)
* [Using helper methods](#using-helper-methods)
* [Contributing](#contributing)


Expand Down Expand Up @@ -277,6 +278,12 @@ class MovieSerializer
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
Expand All @@ -291,7 +298,12 @@ This will create a `self` reference for the relationship, and a `related` link f
### 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 FastJsonapi::ObjectSerializer

meta do |movie|
{
years_since_release: Date.current.year - movie.year
Expand Down Expand Up @@ -459,6 +471,68 @@ 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 FastJsonapi::ObjectSerializer

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 FastJsonapi::ObjectSerializer

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
Expand Down Expand Up @@ -526,9 +600,3 @@ To run tests only performance tests:
```bash
rspec spec --tag performance:true
```

### We're Hiring!

Join the Netflix Studio Engineering team and help us build gems like this!

* [Senior Ruby Engineer](https://jobs.netflix.com/jobs/864893)
24 changes: 12 additions & 12 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ module ObjectSerializer

SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
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
Expand All @@ -43,7 +49,7 @@ def hash_for_one_record

return serializable_hash unless @resource

serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @params)
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
Expand All @@ -55,7 +61,7 @@ def hash_for_collection
included = []
fieldset = @fieldsets[self.class.record_type.to_sym]
@resource.each do |record|
data << self.class.record_hash(record, fieldset, @params)
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

Expand Down Expand Up @@ -93,9 +99,9 @@ def process_options(options)

def deep_symbolize(collection)
if collection.is_a? Hash
Hash[collection.map do |k, v|
[k.to_sym, deep_symbolize(v)]
end]
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
Expand Down Expand Up @@ -137,13 +143,7 @@ def reflected_record_type
end

def set_key_transform(transform_name)
mapping = {
camel: :camelize,
camel_lower: [:camelize, :lower],
dash: :dasherize,
underscore: :underscore
}
self.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
Expand Down
12 changes: 8 additions & 4 deletions lib/fast_jsonapi/relationship.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ def initialize(
@lazy_load_data = lazy_load_data
end

def serialize(record, serialization_params, output_hash)
def serialize(record, included, serialization_params, output_hash)
if include_relationship?(record, serialization_params)
empty_case = relationship_type == :has_many ? [] : nil

output_hash[key] = {}
unless lazy_load_data
unless (lazy_load_data && !included)
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
end
add_links_hash(record, serialization_params, output_hash) if links.present?
Expand Down Expand Up @@ -104,8 +104,12 @@ def fetch_id(record, params)
end

def add_links_hash(record, params, output_hash)
output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash)\
if links.is_a?(Symbol)
output_hash[key][:links] = record.public_send(links)
else
output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash|
Link.new(key: key, method: method).serialize(record, params, hash)\
end
end
end

Expand Down
30 changes: 16 additions & 14 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,37 +51,38 @@ def attributes_hash(record, fieldset = nil, params = {})
end
end

def relationships_hash(record, relationships = nil, fieldset = nil, params = {})
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|
relationship.serialize(record, params, hash)
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 meta_hash(record, params = {})
meta_to_serialize.call(record, params)
end

def record_hash(record, fieldset, params = {})
def record_hash(record, fieldset, includes_list, params = {})
if cached
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
temp_hash = id_hash(id_from_record(record), record_type, true)
temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
temp_hash[:relationships] = {}
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, params) if cachable_relationships_to_serialize.present?
temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, includes_list, params) if cachable_relationships_to_serialize.present?
temp_hash[:links] = links_hash(record, params) if data_links.present?
temp_hash
end
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, params)) if uncachable_relationships_to_serialize.present?
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, includes_list, params)) if uncachable_relationships_to_serialize.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
else
record_hash = id_hash(id_from_record(record), record_type, true)
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, fieldset, params) if relationships_to_serialize.present?
record_hash[:relationships] = relationships_hash(record, nil, fieldset, includes_list, params) if relationships_to_serialize.present?
record_hash[:links] = links_hash(record, params) if data_links.present?
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
record_hash
Expand All @@ -102,15 +103,14 @@ def to_json(payload)

def parse_include_item(include_item)
return [include_item.to_sym] unless include_item.to_s.include?('.')
include_item.to_s.split('.').map { |item| item.to_sym }

include_item.to_s.split('.').map!(&:to_sym)
end

def remaining_items(items)
return unless items.size > 1

items_copy = items.dup
items_copy.delete_at(0)
[items_copy.join('.').to_sym]
[items[1..-1].join('.').to_sym]
end

# includes handler
Expand All @@ -119,6 +119,8 @@ def get_included_records(record, includes_list, known_included_objects, fieldset

includes_list.sort.each_with_object([]) do |include_item, included_records|
items = parse_include_item(include_item)
remaining_items = remaining_items(items)

items.each do |item|
next unless relationships_to_serialize && relationships_to_serialize[item]
relationship_item = relationships_to_serialize[item]
Expand All @@ -139,8 +141,8 @@ def get_included_records(record, includes_list, known_included_objects, fieldset
serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize
end

if remaining_items(items)
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets, params)
if remaining_items.present?
serializer_records = serializer.get_included_records(inc_obj, remaining_items, known_included_objects, fieldsets, params)
included_records.concat(serializer_records) unless serializer_records.empty?
end

Expand All @@ -149,7 +151,7 @@ def get_included_records(record, includes_list, known_included_objects, fieldset

known_included_objects[code] = inc_obj

included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], params)
included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], includes_list, params)
end
end
end
Expand Down
41 changes: 41 additions & 0 deletions spec/lib/object_serializer_relationship_links_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,46 @@ class LazyLoadingMovieSerializer < MovieSerializer
expect(actor_hash).not_to have_key(:data)
end
end

context "including lazy loaded relationships" do
before(:context) do
class LazyLoadingMovieSerializer < MovieSerializer
has_many :actors, lazy_load_data: true, links: {
related: :actors_relationship_url
}
end
end

let(:serializer) { LazyLoadingMovieSerializer.new(movie, include: [:actors]) }
let(:actor_hash) { hash[:data][:relationships][:actors] }

it "includes the :data key" do
expect(actor_hash).to be_present
expect(actor_hash).to have_key(:data)
end
end

context "relationship links defined by a method on the object" do
before(:context) do
class Movie
def relationship_links
{ self: "http://movies.com/#{id}/relationships/actors" }
end
end

class LinksPassingMovieSerializer < MovieSerializer
has_many :actors, links: :relationship_links
end
end

let(:serializer) { LinksPassingMovieSerializer.new(movie) }
let(:links) { hash[:data][:relationships][:actors][:links] }
let(:relationship_url) { "http://movies.com/#{movie.id}/relationships/actors" }

it "generates relationship links in the object" do
expect(links).to be_present
expect(links[:self]).to eq(relationship_url)
end
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/serialization_core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
end

it 'returns correct hash when record_hash is called' do
record_hash = MovieSerializer.send(:record_hash, movie, nil)
record_hash = MovieSerializer.send(:record_hash, movie, nil, nil)
expect(record_hash[:id]).to eq movie.id.to_s
expect(record_hash[:type]).to eq MovieSerializer.record_type
expect(record_hash).to have_key(:attributes) if MovieSerializer.attributes_to_serialize.present?
Expand Down