Skip to content

Commit 49193ab

Browse files
authored
Merge pull request #265 from Netflix/dev
Dev
2 parents 75229fd + dc2b78b commit 49193ab

15 files changed

Lines changed: 484 additions & 146 deletions

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Fast JSON API serialized 250 records in 3.01 ms
3030
* [Collection Serialization](#collection-serialization)
3131
* [Caching](#caching)
3232
* [Params](#params)
33+
* [Conditional Attributes](#conditional-attributes)
34+
* [Conditional Relationships](#conditional-relationships)
3335
* [Contributing](#contributing)
3436

3537

@@ -259,6 +261,26 @@ hash = MovieSerializer.new([movie, movie], options).serializable_hash
259261
json_string = MovieSerializer.new([movie, movie], options).serialized_json
260262
```
261263

264+
#### Control Over Collection Serialization
265+
266+
You can use `is_collection` option to have better control over collection serialization.
267+
268+
If this option is not provided or `nil` autedetect logic is used to try understand
269+
if provided resource is a single object or collection.
270+
271+
Autodetect logic is compatible with most DB toolkits (ActiveRecord, Sequel, etc.) but
272+
**cannot** guarantee that single vs collection will be always detected properly.
273+
274+
```ruby
275+
options[:is_collection]
276+
```
277+
278+
was introduced to be able to have precise control this behavior
279+
280+
- `nil` or not provided: will try to autodetect single vs collection (please, see notes above)
281+
- `true` will always treat input resource as *collection*
282+
- `false` will always treat input resource as *single object*
283+
262284
### Caching
263285
Requires a `cache_key` method be defined on model:
264286

@@ -307,6 +329,53 @@ serializer.serializable_hash
307329
Custom attributes and relationships that only receive the resource are still possible by defining
308330
the block to only receive one argument.
309331

332+
### Conditional Attributes
333+
334+
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.
335+
336+
```ruby
337+
class MovieSerializer
338+
include FastJsonapi::ObjectSerializer
339+
340+
attributes :name, :year
341+
attribute :release_year, if: Proc.new do |record|
342+
# Release year will only be serialized if it's greater than 1990
343+
record.release_year > 1990
344+
end
345+
346+
attribute :director, if: Proc.new do |record, params|
347+
# The director will be serialized only if the :admin key of params is true
348+
params && params[:admin] == true
349+
end
350+
end
351+
352+
# ...
353+
current_user = User.find(cookies[:current_user_id])
354+
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
355+
serializer.serializable_hash
356+
```
357+
358+
### Conditional Relationships
359+
360+
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.
361+
362+
```ruby
363+
class MovieSerializer
364+
include FastJsonapi::ObjectSerializer
365+
366+
# Actors will only be serialized if the record has any associated actors
367+
has_many :actors, if: Proc.new { |record| record.actors.any? }
368+
369+
# Owner will only be serialized if the :admin key of params is true
370+
belongs_to :owner, if: Proc.new { |record, params| params && params[:admin] == true }
371+
end
372+
373+
# ...
374+
current_user = User.find(cookies[:current_user_id])
375+
serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? }})
376+
serializer.serializable_hash
377+
```
378+
310379
### Customizable Options
311380

312381
Option | Purpose | Example

lib/extensions/has_one.rb

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
# frozen_string_literal: true
22

3-
if defined?(::ActiveRecord)
4-
::ActiveRecord::Associations::Builder::HasOne.class_eval do
5-
# Based on
6-
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
7-
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11
8-
def self.define_accessors(mixin, reflection)
9-
super
10-
name = reflection.name
11-
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
12-
def #{name}_id
13-
# if an attribute is already defined with this methods name we should just use it
14-
return read_attribute(__method__) if has_attribute?(__method__)
15-
association(:#{name}).reader.try(:id)
16-
end
17-
CODE
18-
end
3+
::ActiveRecord::Associations::Builder::HasOne.class_eval do
4+
# Based on
5+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50
6+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/singular_association.rb#L11
7+
def self.define_accessors(mixin, reflection)
8+
super
9+
name = reflection.name
10+
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
11+
def #{name}_id
12+
# if an attribute is already defined with this methods name we should just use it
13+
return read_attribute(__method__) if has_attribute?(__method__)
14+
association(:#{name}).reader.try(:id)
15+
end
16+
CODE
1917
end
2018
end

lib/fast_jsonapi.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@
22

33
module FastJsonapi
44
require 'fast_jsonapi/object_serializer'
5-
require 'extensions/has_one'
5+
if defined?(::Rails)
6+
require 'fast_jsonapi/railtie'
7+
elsif defined?(::ActiveRecord)
8+
require 'extensions/has_one'
9+
end
610
end

lib/fast_jsonapi/attribute.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module FastJsonapi
2+
class Attribute
3+
attr_reader :key, :method, :conditional_proc
4+
5+
def initialize(key:, method:, options: {})
6+
@key = key
7+
@method = method
8+
@conditional_proc = options[:if]
9+
end
10+
11+
def serialize(record, serialization_params, output_hash)
12+
if include_attribute?(record, serialization_params)
13+
output_hash[key] = if method.is_a?(Proc)
14+
method.arity == 1 ? method.call(record) : method.call(record, serialization_params)
15+
else
16+
record.public_send(method)
17+
end
18+
end
19+
end
20+
21+
def include_attribute?(record, serialization_params)
22+
if conditional_proc.present?
23+
conditional_proc.call(record, serialization_params)
24+
else
25+
true
26+
end
27+
end
28+
end
29+
end

lib/fast_jsonapi/link.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module FastJsonapi
2+
class Link
3+
attr_reader :key, :method
4+
5+
def initialize(key:, method:)
6+
@key = key
7+
@method = method
8+
end
9+
10+
def serialize(record, serialization_params, output_hash)
11+
output_hash[key] = if method.is_a?(Proc)
12+
method.arity == 1 ? method.call(record) : method.call(record, serialization_params)
13+
else
14+
record.public_send(method)
15+
end
16+
end
17+
end
18+
end

lib/fast_jsonapi/object_serializer.rb

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
require 'active_support/core_ext/object'
44
require 'active_support/concern'
55
require 'active_support/inflector'
6+
require 'fast_jsonapi/attribute'
7+
require 'fast_jsonapi/relationship'
8+
require 'fast_jsonapi/link'
69
require 'fast_jsonapi/serialization_core'
710

811
module FastJsonapi
@@ -25,7 +28,7 @@ def initialize(resource, options = {})
2528
end
2629

2730
def serializable_hash
28-
return hash_for_collection if is_collection?(@resource)
31+
return hash_for_collection if is_collection?(@resource, @is_collection)
2932

3033
hash_for_one_record
3134
end
@@ -72,6 +75,7 @@ def process_options(options)
7275
@known_included_objects = {}
7376
@meta = options[:meta]
7477
@links = options[:links]
78+
@is_collection = options[:is_collection]
7579
@params = options[:params] || {}
7680
raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
7781

@@ -81,8 +85,10 @@ def process_options(options)
8185
end
8286
end
8387

84-
def is_collection?(resource)
85-
resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
88+
def is_collection?(resource, force_is_collection = nil)
89+
return force_is_collection unless force_is_collection.nil?
90+
91+
resource.respond_to?(:size) && !resource.respond_to?(:each_pair)
8692
end
8793

8894
class_methods do
@@ -118,6 +124,9 @@ def set_key_transform(transform_name)
118124
underscore: :underscore
119125
}
120126
self.transform_method = mapping[transform_name.to_sym]
127+
128+
# ensure that the record type is correctly transformed
129+
set_type(reflected_record_type) if reflected_record_type
121130
end
122131

123132
def run_key_transform(input)
@@ -149,48 +158,51 @@ def cache_options(cache_options)
149158

150159
def attributes(*attributes_list, &block)
151160
attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
161+
options = attributes_list.last.is_a?(Hash) ? attributes_list.pop : {}
152162
self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
163+
153164
attributes_list.each do |attr_name|
154165
method_name = attr_name
155166
key = run_key_transform(method_name)
156-
attributes_to_serialize[key] = block || method_name
167+
attributes_to_serialize[key] = Attribute.new(
168+
key: key,
169+
method: block || method_name,
170+
options: options
171+
)
157172
end
158173
end
159174

160175
alias_method :attribute, :attributes
161176

162-
def add_relationship(name, relationship)
177+
def add_relationship(relationship)
163178
self.relationships_to_serialize = {} if relationships_to_serialize.nil?
164179
self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
165180
self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
166-
167-
if !relationship[:cached]
168-
self.uncachable_relationships_to_serialize[name] = relationship
181+
182+
if !relationship.cached
183+
self.uncachable_relationships_to_serialize[relationship.name] = relationship
169184
else
170-
self.cachable_relationships_to_serialize[name] = relationship
185+
self.cachable_relationships_to_serialize[relationship.name] = relationship
171186
end
172-
self.relationships_to_serialize[name] = relationship
173-
end
187+
self.relationships_to_serialize[relationship.name] = relationship
188+
end
174189

175190
def has_many(relationship_name, options = {}, &block)
176-
name = relationship_name.to_sym
177-
hash = create_relationship_hash(relationship_name, :has_many, options, block)
178-
add_relationship(name, hash)
191+
relationship = create_relationship(relationship_name, :has_many, options, block)
192+
add_relationship(relationship)
179193
end
180194

181195
def has_one(relationship_name, options = {}, &block)
182-
name = relationship_name.to_sym
183-
hash = create_relationship_hash(relationship_name, :has_one, options, block)
184-
add_relationship(name, hash)
196+
relationship = create_relationship(relationship_name, :has_one, options, block)
197+
add_relationship(relationship)
185198
end
186199

187200
def belongs_to(relationship_name, options = {}, &block)
188-
name = relationship_name.to_sym
189-
hash = create_relationship_hash(relationship_name, :belongs_to, options, block)
190-
add_relationship(name, hash)
201+
relationship = create_relationship(relationship_name, :belongs_to, options, block)
202+
add_relationship(relationship)
191203
end
192204

193-
def create_relationship_hash(base_key, relationship_type, options, block)
205+
def create_relationship(base_key, relationship_type, options, block)
194206
name = base_key.to_sym
195207
if relationship_type == :has_many
196208
base_serialization_key = base_key.to_s.singularize
@@ -201,7 +213,7 @@ def create_relationship_hash(base_key, relationship_type, options, block)
201213
base_key_sym = name
202214
id_postfix = '_id'
203215
end
204-
{
216+
Relationship.new(
205217
key: options[:key] || run_key_transform(base_key),
206218
name: name,
207219
id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
@@ -210,9 +222,10 @@ def create_relationship_hash(base_key, relationship_type, options, block)
210222
object_block: block,
211223
serializer: compute_serializer_name(options[:serializer] || base_key_sym),
212224
relationship_type: relationship_type,
213-
cached: options[:cached] || false,
214-
polymorphic: fetch_polymorphic_option(options)
215-
}
225+
cached: options[:cached],
226+
polymorphic: fetch_polymorphic_option(options),
227+
conditional_proc: options[:if]
228+
)
216229
end
217230

218231
def compute_serializer_name(serializer_key)
@@ -233,7 +246,11 @@ def link(link_name, link_method_name = nil, &block)
233246
self.data_links = {} if self.data_links.nil?
234247
link_method_name = link_name if link_method_name.nil?
235248
key = run_key_transform(link_name)
236-
self.data_links[key] = block || link_method_name
249+
250+
self.data_links[key] = Link.new(
251+
key: key,
252+
method: block || link_method_name
253+
)
237254
end
238255

239256
def validate_includes!(includes)
@@ -244,8 +261,8 @@ def validate_includes!(includes)
244261
parse_include_item(include_item).each do |parsed_include|
245262
relationship_to_include = klass.relationships_to_serialize[parsed_include]
246263
raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
247-
raise NotImplementedError if relationship_to_include[:polymorphic].is_a?(Hash)
248-
klass = relationship_to_include[:serializer].to_s.constantize
264+
raise NotImplementedError if relationship_to_include.polymorphic.is_a?(Hash)
265+
klass = relationship_to_include.serializer.to_s.constantize
249266
end
250267
end
251268
end

lib/fast_jsonapi/railtie.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails/railtie'
4+
5+
class Railtie < Rails::Railtie
6+
initializer 'fast_jsonapi.active_record' do
7+
ActiveSupport.on_load :active_record do
8+
require 'extensions/has_one'
9+
end
10+
end
11+
end

0 commit comments

Comments
 (0)