Skip to content

Commit 92bcab0

Browse files
authored
Merge pull request #320 from Netflix/release-1.4
Release 1.4
2 parents 4afa5b8 + 90e0fee commit 92bcab0

9 files changed

Lines changed: 257 additions & 32 deletions

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,23 @@ 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.
251+
252+
253+
```ruby
254+
class MovieSerializer
255+
include FastJsonapi::ObjectSerializer
256+
257+
meta do |movie|
258+
{
259+
years_since_release: Date.current.year - movie.year
260+
}
261+
end
262+
end
263+
```
264+
248265
### Compound Document
249266

250267
Support for top-level and nested included associations through ` options[:include] `.
@@ -351,15 +368,15 @@ class MovieSerializer
351368
include FastJsonapi::ObjectSerializer
352369

353370
attributes :name, :year
354-
attribute :release_year, if: Proc.new do |record|
371+
attribute :release_year, if: Proc.new { |record|
355372
# Release year will only be serialized if it's greater than 1990
356373
record.release_year > 1990
357-
end
374+
}
358375

359-
attribute :director, if: Proc.new do |record, params|
376+
attribute :director, if: Proc.new { |record, params|
360377
# The director will be serialized only if the :admin key of params is true
361378
params && params[:admin] == true
362-
end
379+
}
363380
end
364381

365382
# ...
@@ -409,6 +426,7 @@ serializer.serializable_hash
409426
Option | Purpose | Example
410427
------------ | ------------- | -------------
411428
set_type | Type name of Object | ```set_type :movie ```
429+
key | Key of Object | ```belongs_to :owner, key: :user ```
412430
set_id | ID of Object | ```set_id :owner_id ```
413431
cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds```
414432
id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ```

lib/fast_jsonapi/object_serializer.rb

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

3-
require 'active_support/core_ext/object'
3+
require 'active_support/json'
44
require 'active_support/concern'
55
require 'active_support/inflector'
66
require 'fast_jsonapi/attribute'
@@ -65,7 +65,7 @@ def hash_for_collection
6565
end
6666

6767
def serialized_json
68-
self.class.to_json(serializable_hash)
68+
ActiveSupport::JSON.encode(serializable_hash)
6969
end
7070

7171
private
@@ -120,6 +120,7 @@ def inherited(subclass)
120120
subclass.data_links = data_links
121121
subclass.cached = cached
122122
subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type
123+
subclass.meta_to_serialize = meta_to_serialize
123124
end
124125

125126
def reflected_record_type
@@ -194,7 +195,7 @@ def add_relationship(relationship)
194195
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
195196
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
196197
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
197-
198+
198199
if !relationship.cached
199200
self.uncachable_relationships_to_serialize[relationship.name] = relationship
200201
else
@@ -218,6 +219,10 @@ def belongs_to(relationship_name, options = {}, &block)
218219
add_relationship(relationship)
219220
end
220221

222+
def meta(&block)
223+
self.meta_to_serialize = block
224+
end
225+
221226
def create_relationship(base_key, relationship_type, options, block)
222227
name = base_key.to_sym
223228
if relationship_type == :has_many
@@ -232,18 +237,31 @@ def create_relationship(base_key, relationship_type, options, block)
232237
Relationship.new(
233238
key: options[:key] || run_key_transform(base_key),
234239
name: name,
235-
id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
240+
id_method_name: compute_id_method_name(
241+
options[:id_method_name],
242+
"#{base_serialization_key}#{id_postfix}".to_sym,
243+
block
244+
),
236245
record_type: options[:record_type] || run_key_transform(base_key_sym),
237246
object_method_name: options[:object_method_name] || name,
238247
object_block: block,
239248
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
240249
relationship_type: relationship_type,
241250
cached: options[:cached],
242251
polymorphic: fetch_polymorphic_option(options),
243-
conditional_proc: options[:if]
252+
conditional_proc: options[:if],
253+
transform_method: @transform_method
244254
)
245255
end
246256

257+
def compute_id_method_name(custom_id_method_name, id_method_name_from_relationship, block)
258+
if block.present?
259+
custom_id_method_name || :id
260+
else
261+
custom_id_method_name || id_method_name_from_relationship
262+
end
263+
end
264+
247265
def compute_serializer_name(serializer_key)
248266
return serializer_key unless serializer_key.is_a? Symbol
249267
namespace = self.name.gsub(/()?\w+Serializer$/, '')
@@ -275,10 +293,10 @@ def validate_includes!(includes)
275293
includes.detect do |include_item|
276294
klass = self
277295
parse_include_item(include_item).each do |parsed_include|
278-
relationship_to_include = klass.relationships_to_serialize[parsed_include]
296+
relationships_to_serialize = klass.relationships_to_serialize || {}
297+
relationship_to_include = relationships_to_serialize[parsed_include]
279298
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
280-
raise NotImplementedError if relationship_to_include.polymorphic.is_a?(Hash)
281-
klass = relationship_to_include.serializer.to_s.constantize
299+
klass = relationship_to_include.serializer.to_s.constantize unless relationship_to_include.polymorphic.is_a?(Hash)
282300
end
283301
end
284302
end

lib/fast_jsonapi/relationship.rb

Lines changed: 17 additions & 9 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
3+
attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method
44

55
def initialize(
66
key:,
@@ -13,7 +13,8 @@ def initialize(
1313
relationship_type:,
1414
cached: false,
1515
polymorphic:,
16-
conditional_proc:
16+
conditional_proc:,
17+
transform_method:
1718
)
1819
@key = key
1920
@name = name
@@ -26,6 +27,7 @@ def initialize(
2627
@cached = cached
2728
@polymorphic = polymorphic
2829
@conditional_proc = conditional_proc
30+
@transform_method = transform_method
2931
end
3032

3133
def serialize(record, serialization_params, output_hash)
@@ -68,7 +70,7 @@ def ids_hash_from_record_and_relationship(record, params = {})
6870

6971
def id_hash_from_record(record, record_types)
7072
# memoize the record type within the record_types dictionary, then assigning to record_type:
71-
associated_record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
73+
associated_record_type = record_types[record.class] ||= run_key_transform(record.class.name.demodulize.underscore)
7274
id_hash(record.id, associated_record_type)
7375
end
7476

@@ -86,14 +88,20 @@ def id_hash(id, record_type, default_return=false)
8688
end
8789

8890
def fetch_id(record, params)
89-
unless object_block.nil?
91+
if object_block.present?
9092
object = object_block.call(record, params)
91-
92-
return object.map(&:id) if object.respond_to? :map
93-
return object.try(:id)
93+
return object.map { |item| item.public_send(id_method_name) } if object.respond_to? :map
94+
return object.try(id_method_name)
9495
end
95-
9696
record.public_send(id_method_name)
9797
end
98+
99+
def run_key_transform(input)
100+
if self.transform_method.present?
101+
input.to_s.send(*self.transform_method).to_sym
102+
else
103+
input.to_sym
104+
end
105+
end
98106
end
99-
end
107+
end

lib/fast_jsonapi/serialization_core.rb

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class << self
2121
:cache_length,
2222
:race_condition_ttl,
2323
:cached,
24-
:data_links
24+
:data_links,
25+
:meta_to_serialize
2526
end
2627
end
2728

@@ -57,6 +58,10 @@ def relationships_hash(record, relationships = nil, fieldset = nil, params = {})
5758
end
5859
end
5960

61+
def meta_hash(record, params = {})
62+
meta_to_serialize.call(record, params)
63+
end
64+
6065
def record_hash(record, fieldset, params = {})
6166
if cached
6267
record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
@@ -67,13 +72,15 @@ def record_hash(record, fieldset, params = {})
6772
temp_hash[:links] = links_hash(record, params) if data_links.present?
6873
temp_hash
6974
end
70-
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, params)) if uncachable_relationships_to_serialize.present?
75+
record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, fieldset, params)) if uncachable_relationships_to_serialize.present?
76+
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
7177
record_hash
7278
else
7379
record_hash = id_hash(id_from_record(record), record_type, true)
7480
record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present?
7581
record_hash[:relationships] = relationships_hash(record, nil, fieldset, params) if relationships_to_serialize.present?
7682
record_hash[:links] = links_hash(record, params) if data_links.present?
83+
record_hash[:meta] = meta_hash(record, params) if meta_to_serialize.present?
7784
record_hash
7885
end
7986
end
@@ -112,22 +119,28 @@ def get_included_records(record, includes_list, known_included_objects, fieldset
112119
next unless relationships_to_serialize && relationships_to_serialize[item]
113120
relationship_item = relationships_to_serialize[item]
114121
next unless relationship_item.include_relationship?(record, params)
115-
raise NotImplementedError if relationship_item.polymorphic.is_a?(Hash)
116-
record_type = relationship_item.record_type
117-
serializer = relationship_item.serializer.to_s.constantize
122+
unless relationship_item.polymorphic.is_a?(Hash)
123+
record_type = relationship_item.record_type
124+
serializer = relationship_item.serializer.to_s.constantize
125+
end
118126
relationship_type = relationship_item.relationship_type
119127

120128
included_objects = relationship_item.fetch_associated_object(record, params)
121129
next if included_objects.blank?
122130
included_objects = [included_objects] unless relationship_type == :has_many
123131

124132
included_objects.each do |inc_obj|
133+
if relationship_item.polymorphic.is_a?(Hash)
134+
record_type = inc_obj.class.name.demodulize.underscore
135+
serializer = self.compute_serializer_name(inc_obj.class.name.demodulize.to_sym).to_s.constantize
136+
end
137+
125138
if remaining_items(items)
126-
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets)
139+
serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets, params)
127140
included_records.concat(serializer_records) unless serializer_records.empty?
128141
end
129142

130-
code = "#{record_type}_#{inc_obj.id}"
143+
code = "#{record_type}_#{serializer.id_from_record(inc_obj)}"
131144
next if known_included_objects.key?(code)
132145

133146
known_included_objects[code] = inc_obj

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.3"
2+
VERSION = "1.4"
33
end

spec/lib/object_serializer_class_methods_spec.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,31 @@
8787
end
8888
end
8989

90+
describe '#has_many with block and id_method_name' do
91+
before do
92+
MovieSerializer.has_many(:awards, id_method_name: :imdb_award_id) do |movie|
93+
movie.actors.map(&:awards).flatten
94+
end
95+
end
96+
97+
after do
98+
MovieSerializer.relationships_to_serialize.delete(:awards)
99+
end
100+
101+
context 'awards is not included' do
102+
subject(:hash) { MovieSerializer.new(movie).serializable_hash }
103+
104+
it 'returns correct hash where id is obtained from the method specified via `id_method_name`' do
105+
expected_award_data = movie.actors.map(&:awards).flatten.map do |actor|
106+
{ id: actor.imdb_award_id.to_s, type: actor.class.name.downcase.to_sym }
107+
end
108+
serialized_award_data = hash[:data][:relationships][:awards][:data]
109+
110+
expect(serialized_award_data).to eq(expected_award_data)
111+
end
112+
end
113+
end
114+
90115
describe '#belongs_to' do
91116
subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] }
92117

@@ -249,6 +274,34 @@
249274
end
250275
end
251276

277+
describe '#meta' do
278+
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }
279+
280+
before do
281+
movie.release_year = 2008
282+
MovieSerializer.meta do |movie|
283+
{
284+
years_since_release: year_since_release_calculator(movie.release_year)
285+
}
286+
end
287+
end
288+
289+
after do
290+
movie.release_year = nil
291+
MovieSerializer.meta_to_serialize = nil
292+
end
293+
294+
it 'returns correct hash when serializable_hash is called' do
295+
expect(serializable_hash[:data][:meta]).to eq ({ years_since_release: year_since_release_calculator(movie.release_year) })
296+
end
297+
298+
private
299+
300+
def year_since_release_calculator(release_year)
301+
Date.current.year - release_year
302+
end
303+
end
304+
252305
describe '#link' do
253306
subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash }
254307

0 commit comments

Comments
 (0)