Skip to content

Commit fdcaed6

Browse files
authored
Merge pull request #342 from Netflix/release-1.5
Release 1.5
2 parents 92bcab0 + ee76e0c commit fdcaed6

11 files changed

+351
-40
lines changed

README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ end
221221
```
222222

223223
### Links Per Object
224-
Links are defined in FastJsonapi using the `link` method. By default, link 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.
224+
Links are defined in FastJsonapi using the `link` method. By default, links are read directly from the model property of the same name. In this example, `public_url` is expected to be a property of the object being serialized.
225225

226226
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.
227227

@@ -245,15 +245,38 @@ class MovieSerializer
245245
end
246246
```
247247

248-
### Meta Per Resource
249-
250-
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.
248+
#### Links on a Relationship
251249

250+
You can specify [relationship links](http://jsonapi.org/format/#document-resource-object-relationships) by using the `links:` option on the serializer. Relationship links in JSON API are useful if you want to load a parent document and then load associated documents later due to size constraints (see [related resource links](http://jsonapi.org/format/#document-resource-object-related-resource-links))
252251

253252
```ruby
254253
class MovieSerializer
255254
include FastJsonapi::ObjectSerializer
256255

256+
has_many :actors, links: {
257+
self: :url,
258+
related: -> (object) {
259+
"https://movies.com/#{object.id}/actors"
260+
}
261+
}
262+
end
263+
```
264+
265+
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:
266+
267+
```ruby
268+
has_many :actors, lazy_load_data: true, links: {
269+
self: :url,
270+
related: -> (object) {
271+
"https://movies.com/#{object.id}/actors"
272+
}
273+
}
274+
```
275+
276+
### Meta Per Resource
277+
278+
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.
279+
```ruby
257280
meta do |movie|
258281
{
259282
years_since_release: Date.current.year - movie.year
@@ -427,9 +450,9 @@ Option | Purpose | Example
427450
------------ | ------------- | -------------
428451
set_type | Type name of Object | ```set_type :movie ```
429452
key | Key of Object | ```belongs_to :owner, key: :user ```
430-
set_id | ID of Object | ```set_id :owner_id ```
453+
set_id | ID of Object | ```set_id :owner_id ``` or ```set_id { |record| "#{record.name.downcase}-#{record.id}" }```
431454
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds```
432-
id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ```
455+
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 ```
433456
object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ```
434457
record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user```
435458
serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor``` or ```has_many :actors, serializer: MyApp::Api::V1::ActorSerializer```

lib/fast_jsonapi/object_serializer.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# frozen_string_literal: true
22

3+
require 'active_support/time'
34
require 'active_support/json'
45
require 'active_support/concern'
56
require 'active_support/inflector'
7+
require 'active_support/core_ext/numeric/time'
68
require 'fast_jsonapi/attribute'
79
require 'fast_jsonapi/relationship'
810
require 'fast_jsonapi/link'
@@ -72,6 +74,7 @@ def serialized_json
7274

7375
def process_options(options)
7476
@fieldsets = deep_symbolize(options[:fields].presence || {})
77+
@params = {}
7578

7679
return if options.blank?
7780

@@ -117,7 +120,7 @@ def inherited(subclass)
117120
subclass.transform_method = transform_method
118121
subclass.cache_length = cache_length
119122
subclass.race_condition_ttl = race_condition_ttl
120-
subclass.data_links = data_links
123+
subclass.data_links = data_links.dup if data_links.present?
121124
subclass.cached = cached
122125
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
123126
subclass.meta_to_serialize = meta_to_serialize
@@ -143,7 +146,11 @@ def set_key_transform(transform_name)
143146
self.transform_method = mapping[transform_name.to_sym]
144147

145148
# ensure that the record type is correctly transformed
146-
set_type(reflected_record_type) if reflected_record_type
149+
if record_type
150+
set_type(record_type)
151+
elsif reflected_record_type
152+
set_type(reflected_record_type)
153+
end
147154
end
148155

149156
def run_key_transform(input)
@@ -163,8 +170,8 @@ def set_type(type_name)
163170
self.record_type = run_key_transform(type_name)
164171
end
165172

166-
def set_id(id_name)
167-
self.record_id = id_name
173+
def set_id(id_name = nil, &block)
174+
self.record_id = block || id_name
168175
end
169176

170177
def cache_options(cache_options)
@@ -250,7 +257,9 @@ def create_relationship(base_key, relationship_type, options, block)
250257
cached: options[:cached],
251258
polymorphic: fetch_polymorphic_option(options),
252259
conditional_proc: options[:if],
253-
transform_method: @transform_method
260+
transform_method: @transform_method,
261+
links: options[:links],
262+
lazy_load_data: options[:lazy_load_data]
254263
)
255264
end
256265

lib/fast_jsonapi/relationship.rb

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module FastJsonapi
22
class Relationship
3-
attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method
3+
attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :lazy_load_data
44

55
def initialize(
66
key:,
@@ -14,7 +14,9 @@ def initialize(
1414
cached: false,
1515
polymorphic:,
1616
conditional_proc:,
17-
transform_method:
17+
transform_method:,
18+
links:,
19+
lazy_load_data: false
1820
)
1921
@key = key
2022
@name = name
@@ -28,14 +30,19 @@ def initialize(
2830
@polymorphic = polymorphic
2931
@conditional_proc = conditional_proc
3032
@transform_method = transform_method
33+
@links = links || {}
34+
@lazy_load_data = lazy_load_data
3135
end
3236

3337
def serialize(record, serialization_params, output_hash)
3438
if include_relationship?(record, serialization_params)
3539
empty_case = relationship_type == :has_many ? [] : nil
36-
output_hash[key] = {
37-
data: ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
38-
}
40+
41+
output_hash[key] = {}
42+
unless lazy_load_data
43+
output_hash[key][:data] = ids_hash_from_record_and_relationship(record, serialization_params) || empty_case
44+
end
45+
add_links_hash(record, serialization_params, output_hash) if links.present?
3946
end
4047
end
4148

@@ -96,6 +103,12 @@ def fetch_id(record, params)
96103
record.public_send(id_method_name)
97104
end
98105

106+
def add_links_hash(record, params, output_hash)
107+
output_hash[key][:links] = links.each_with_object({}) do |(key, method), hash|
108+
Link.new(key: key, method: method).serialize(record, params, hash)\
109+
end
110+
end
111+
99112
def run_key_transform(input)
100113
if self.transform_method.present?
101114
input.to_s.send(*self.transform_method).to_sym

lib/fast_jsonapi/serialization_core.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ def record_hash(record, fieldset, params = {})
8686
end
8787

8888
def id_from_record(record)
89-
return record.send(record_id) if record_id
90-
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
91-
record.id
89+
return record_id.call(record) if record_id.is_a?(Proc)
90+
return record.send(record_id) if record_id
91+
raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
92+
record.id
9293
end
9394

9495
# Override #to_json for alternative implementation

lib/fast_jsonapi/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module FastJsonapi
2-
VERSION = "1.4"
2+
VERSION = "1.5"
33
end

spec/lib/extensions/active_record_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,85 @@ class Account < ActiveRecord::Base
7171
File.delete(@db_file) if File.exist?(@db_file)
7272
end
7373
end
74+
75+
describe 'active record has_one through' do
76+
# Setup DB
77+
before(:all) do
78+
@db_file = "test_two.db"
79+
80+
# Open a database
81+
db = SQLite3::Database.new @db_file
82+
83+
# Create tables
84+
db.execute_batch <<-SQL
85+
create table forests (
86+
id int primary key,
87+
name varchar(30)
88+
);
89+
90+
create table trees (
91+
id int primary key,
92+
forest_id int,
93+
name varchar(30),
94+
95+
FOREIGN KEY (forest_id) REFERENCES forests(id)
96+
);
97+
98+
create table fruits (
99+
id int primary key,
100+
tree_id int,
101+
name varchar(30),
102+
103+
FOREIGN KEY (tree_id) REFERENCES trees(id)
104+
);
105+
SQL
106+
107+
# Insert records
108+
db.execute_batch <<-SQL
109+
insert into forests values (1, 'sherwood');
110+
insert into trees values (2, 1,'pine');
111+
insert into fruits values (3, 2, 'pine nut');
112+
113+
insert into fruits(id,name) values (4,'apple');
114+
SQL
115+
end
116+
117+
# Setup Active Record
118+
before(:all) do
119+
class Forest < ActiveRecord::Base
120+
has_many :trees
121+
end
122+
123+
class Tree < ActiveRecord::Base
124+
belongs_to :forest
125+
end
126+
127+
class Fruit < ActiveRecord::Base
128+
belongs_to :tree
129+
has_one :forest, through: :tree
130+
end
131+
132+
ActiveRecord::Base.establish_connection(
133+
:adapter => 'sqlite3',
134+
:database => @db_file
135+
)
136+
end
137+
138+
context 'revenue' do
139+
it 'has an forest_id' do
140+
expect(Fruit.find(3).respond_to?(:forest_id)).to be true
141+
expect(Fruit.find(3).forest_id).to eq 1
142+
expect(Fruit.find(3).forest.name).to eq "sherwood"
143+
end
144+
145+
it 'has nil if tree id not available' do
146+
expect(Fruit.find(4).respond_to?(:tree_id)).to be true
147+
expect(Fruit.find(4).forest_id).to eq nil
148+
end
149+
end
150+
151+
# Clean up DB
152+
after(:all) do
153+
File.delete(@db_file) if File.exist?(@db_file)
154+
end
155+
end

spec/lib/object_serializer_attribute_param_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def viewed?(user)
1515

1616
class MovieSerializer
1717
attribute :viewed do |movie, params|
18-
params ? movie.viewed?(params[:user]) : false
18+
params[:user] ? movie.viewed?(params[:user]) : false
1919
end
2020

2121
attribute :no_param_attribute do |movie|

0 commit comments

Comments
 (0)