oj_serializers 1.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +364 -195
- data/lib/oj_serializers/compat.rb +4 -19
- data/lib/oj_serializers/controller_serialization.rb +2 -2
- data/lib/oj_serializers/json_string_encoder.rb +12 -28
- data/lib/oj_serializers/serializer.rb +389 -156
- data/lib/oj_serializers/version.rb +1 -1
- metadata +6 -33
@@ -19,7 +19,7 @@ require 'oj_serializers/json_value'
|
|
19
19
|
class OjSerializers::Serializer
|
20
20
|
# Public: Used to validate incorrect memoization during development. Users of
|
21
21
|
# this library might add additional options as needed.
|
22
|
-
ALLOWED_INSTANCE_VARIABLES = %w[memo object]
|
22
|
+
ALLOWED_INSTANCE_VARIABLES = %w[memo object options]
|
23
23
|
|
24
24
|
CACHE = (defined?(Rails) && Rails.cache) ||
|
25
25
|
(defined?(ActiveSupport::Cache::MemoryStore) ? ActiveSupport::Cache::MemoryStore.new : OjSerializers::Memo.new)
|
@@ -35,30 +35,17 @@ class OjSerializers::Serializer
|
|
35
35
|
# Backwards Compatibility: Allows to access options passed through `render json`,
|
36
36
|
# in the same way than ActiveModel::Serializers.
|
37
37
|
def options
|
38
|
-
@
|
39
|
-
end
|
40
|
-
|
41
|
-
# Internal: Used internally to write attributes and associations to JSON.
|
42
|
-
#
|
43
|
-
# NOTE: Binds this instance to the specified object and options and writes
|
44
|
-
# to json using the provided writer.
|
45
|
-
def write_flat(writer, item)
|
46
|
-
@memo.clear if defined?(@memo)
|
47
|
-
@object = item
|
48
|
-
write_to_json(writer)
|
38
|
+
@options || DEFAULT_OPTIONS
|
49
39
|
end
|
50
40
|
|
51
41
|
# NOTE: Helps developers to remember to keep serializers stateless.
|
52
42
|
if DEV_MODE
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')}"
|
58
|
-
end
|
59
|
-
super
|
43
|
+
def _check_instance_variables
|
44
|
+
if instance_values.keys.any? { |key| !ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
45
|
+
bad_keys = instance_values.keys.reject { |key| ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
46
|
+
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')} in #{self.class}"
|
60
47
|
end
|
61
|
-
end
|
48
|
+
end
|
62
49
|
end
|
63
50
|
|
64
51
|
# Internal: Used internally to write a single object to JSON.
|
@@ -70,9 +57,8 @@ class OjSerializers::Serializer
|
|
70
57
|
# NOTE: Binds this instance to the specified object and options and writes
|
71
58
|
# to json using the provided writer.
|
72
59
|
def write_one(writer, item, options = nil)
|
73
|
-
item.define_singleton_method(:options) { options } if options
|
74
60
|
writer.push_object
|
75
|
-
|
61
|
+
write_to_json(writer, item, options)
|
76
62
|
writer.pop
|
77
63
|
end
|
78
64
|
|
@@ -93,61 +79,43 @@ protected
|
|
93
79
|
|
94
80
|
# Internal: An internal cache that can be used for temporary memoization.
|
95
81
|
def memo
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
private
|
100
|
-
|
101
|
-
# Strategy: Writes an _id value to JSON using `id` as the key instead.
|
102
|
-
# NOTE: We skip the id for non-persisted documents, since it doesn't actually
|
103
|
-
# identify the document (it will change once it's persisted).
|
104
|
-
def write_value_using_id_strategy(writer, _key)
|
105
|
-
writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?
|
82
|
+
@memo ||= OjSerializers::Memo.new
|
106
83
|
end
|
107
84
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
writer.push_value(@object[key], key.to_s)
|
116
|
-
end
|
117
|
-
|
118
|
-
# Strategy: Obtains the value by calling a method in the object, and writes it.
|
119
|
-
def write_value_using_method_strategy(writer, key)
|
120
|
-
writer.push_value(@object.send(key), key)
|
121
|
-
end
|
85
|
+
class << self
|
86
|
+
# Public: Allows the user to specify `default_format :json`, as a simple
|
87
|
+
# way to ensure that `.one` and `.many` work as in Version 1.
|
88
|
+
def default_format(value)
|
89
|
+
@_default_format = value
|
90
|
+
define_serialization_shortcuts
|
91
|
+
end
|
122
92
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
93
|
+
# Public: Allows to sort fields by name instead.
|
94
|
+
def sort_attributes_by(value)
|
95
|
+
@_sort_attributes_by = case value
|
96
|
+
when :name then ->(name, options) { options[:identifier] ? "__#{name}" : name }
|
97
|
+
when Proc then value
|
98
|
+
else
|
99
|
+
raise ArgumentError, "Unknown sorting option: #{value.inspect}"
|
100
|
+
end
|
101
|
+
end
|
127
102
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
alias original_write_value_using_mongoid_strategy write_value_using_mongoid_strategy
|
138
|
-
def write_value_using_mongoid_strategy(writer, key)
|
139
|
-
original_write_value_using_mongoid_strategy(writer, key).tap do
|
140
|
-
# Apply a fake selection when 'only' is not used, so that we allow
|
141
|
-
# read_attribute to fail on typos, renamed, and removed fields.
|
142
|
-
@object.__selected_fields = @object.fields.merge(@object.relations.select { |_key, value| value.embedded? }).transform_values { 1 } unless @object.__selected_fields
|
143
|
-
@object.read_attribute(key) # Raise a missing attribute exception if it's missing.
|
103
|
+
# Public: Allows to sort fields by name instead.
|
104
|
+
def transform_keys(transformer = nil, &block)
|
105
|
+
@_transform_keys = case (transformer ||= block)
|
106
|
+
when :camelize, :camel_case then ->(key) { key.to_s.camelize(:lower) }
|
107
|
+
when Symbol then transformer.to_proc
|
108
|
+
when Proc then transformer
|
109
|
+
else
|
110
|
+
raise(ArgumentError, "Expected transform_keys to be callable, got: #{transformer.inspect}")
|
144
111
|
end
|
145
|
-
rescue StandardError => e
|
146
|
-
raise ActiveModel::MissingAttributeError, "#{e.message} in #{self.class} for #{@object.inspect}"
|
147
112
|
end
|
148
|
-
end
|
149
113
|
|
150
|
-
|
114
|
+
# Public: Creates an alias for the internal object.
|
115
|
+
def object_as(name, **)
|
116
|
+
define_method(name) { @object }
|
117
|
+
end
|
118
|
+
|
151
119
|
# Internal: We want to discourage instantiating serializers directly, as it
|
152
120
|
# prevents the possibility of reusing an instance.
|
153
121
|
#
|
@@ -156,15 +124,25 @@ private
|
|
156
124
|
|
157
125
|
# Internal: Delegates to the instance methods, the advantage is that we can
|
158
126
|
# reuse the same serializer instance to serialize different objects.
|
159
|
-
delegate :write_one, :write_many, :
|
127
|
+
delegate :write_one, :write_many, :write_to_json, to: :instance
|
160
128
|
|
161
129
|
# Internal: Keep a reference to the default `write_one` method so that we
|
162
130
|
# can use it inside cached overrides and benchmark tests.
|
163
|
-
|
131
|
+
alias_method :non_cached_write_one, :write_one
|
164
132
|
|
165
133
|
# Internal: Keep a reference to the default `write_many` method so that we
|
166
134
|
# can use it inside cached overrides and benchmark tests.
|
167
|
-
|
135
|
+
alias_method :non_cached_write_many, :write_many
|
136
|
+
|
137
|
+
# Helper: Serializes one or more items.
|
138
|
+
def render(item, options = nil)
|
139
|
+
many?(item) ? many(item, options) : one(item, options)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Helper: Serializes one or more items.
|
143
|
+
def render_as_hash(item, options = nil)
|
144
|
+
many?(item) ? many_as_hash(item, options) : one_as_hash(item, options)
|
145
|
+
end
|
168
146
|
|
169
147
|
# Helper: Serializes the item unless it's nil.
|
170
148
|
def one_if(item, options = nil)
|
@@ -177,7 +155,7 @@ private
|
|
177
155
|
# options - list of external options to pass to the sub class (available in `item.options`)
|
178
156
|
#
|
179
157
|
# Returns an Oj::StringWriter instance, which is encoded as raw json.
|
180
|
-
def
|
158
|
+
def one_as_json(item, options = nil)
|
181
159
|
writer = new_json_writer
|
182
160
|
write_one(writer, item, options)
|
183
161
|
writer
|
@@ -189,21 +167,39 @@ private
|
|
189
167
|
# options - list of external options to pass to the sub class (available in `item.options`)
|
190
168
|
#
|
191
169
|
# Returns an Oj::StringWriter instance, which is encoded as raw json.
|
192
|
-
def
|
170
|
+
def many_as_json(items, options = nil)
|
193
171
|
writer = new_json_writer
|
194
172
|
write_many(writer, items, options)
|
195
173
|
writer
|
196
174
|
end
|
197
175
|
|
198
|
-
# Public:
|
199
|
-
|
200
|
-
|
176
|
+
# Public: Renders the configured attributes for the specified object,
|
177
|
+
# without serializing to JSON.
|
178
|
+
#
|
179
|
+
# item - the item to serialize
|
180
|
+
# options - list of external options to pass to the sub class (available in `item.options`)
|
181
|
+
#
|
182
|
+
# Returns a Hash, with the attributes specified in the serializer.
|
183
|
+
def one_as_hash(item, options = nil)
|
184
|
+
instance.render_as_hash(item, options)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Public: Renders an array of items using this serializer, without
|
188
|
+
# serializing to JSON.
|
189
|
+
#
|
190
|
+
# items - Must respond to `each`.
|
191
|
+
# options - list of external options to pass to the sub class (available in `item.options`)
|
192
|
+
#
|
193
|
+
# Returns an Array of Hash, each with the attributes specified in the serializer.
|
194
|
+
def many_as_hash(items, options = nil)
|
195
|
+
serializer = instance
|
196
|
+
items.map { |item| serializer.render_as_hash(item, options) }
|
201
197
|
end
|
202
198
|
|
203
199
|
# Internal: Will alias the object according to the name of the wrapper class.
|
204
200
|
def inherited(subclass)
|
205
201
|
object_alias = subclass.name.demodulize.chomp('Serializer').underscore
|
206
|
-
subclass.object_as(object_alias) unless method_defined?(object_alias)
|
202
|
+
subclass.object_as(object_alias) unless method_defined?(object_alias) || object_alias == 'base'
|
207
203
|
super
|
208
204
|
end
|
209
205
|
|
@@ -211,15 +207,7 @@ private
|
|
211
207
|
#
|
212
208
|
# Any attributes defined in parent classes are inherited.
|
213
209
|
def _attributes
|
214
|
-
@_attributes
|
215
|
-
@_attributes
|
216
|
-
end
|
217
|
-
|
218
|
-
# Internal: List of associations to be serialized.
|
219
|
-
# Any associations defined in parent classes are inherited.
|
220
|
-
def _associations
|
221
|
-
@_associations = superclass.try(:_associations)&.dup || {} unless defined?(@_associations)
|
222
|
-
@_associations
|
210
|
+
@_attributes ||= superclass.try(:_attributes)&.dup || {}
|
223
211
|
end
|
224
212
|
|
225
213
|
protected
|
@@ -235,6 +223,36 @@ private
|
|
235
223
|
# NOTE: Benchmark it, sometimes caching is actually SLOWER.
|
236
224
|
def cached(cache_key_proc = :cache_key.to_proc)
|
237
225
|
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
|
226
|
+
cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION }.freeze
|
227
|
+
|
228
|
+
# Internal: Redefine `one_as_hash` to use the cache for the serialized hash.
|
229
|
+
define_singleton_method(:one_as_hash) do |item, options = nil|
|
230
|
+
CACHE.fetch(item_cache_key(item, cache_key_proc), cache_hash_options) do
|
231
|
+
instance.render_as_hash(item, options)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Internal: Redefine `many_as_hash` to use the cache for the serialized hash.
|
236
|
+
define_singleton_method(:many_as_hash) do |items, options = nil|
|
237
|
+
# We define a one-off method for the class to receive the entire object
|
238
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
239
|
+
# key, and we would need to build a Hash to retrieve the object.
|
240
|
+
#
|
241
|
+
# NOTE: The assignment is important, as queries would return different
|
242
|
+
# objects when expanding with the splat in fetch_multi.
|
243
|
+
items = items.entries.each do |item|
|
244
|
+
item_key = item_cache_key(item, cache_key_proc)
|
245
|
+
item.define_singleton_method(:cache_key) { item_key }
|
246
|
+
end
|
247
|
+
|
248
|
+
# Fetch all items at once by leveraging `read_multi`.
|
249
|
+
#
|
250
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
251
|
+
# store to use Redis performance would improve a lot for this case.
|
252
|
+
CACHE.fetch_multi(*items, cache_hash_options) do |item|
|
253
|
+
instance.render_as_hash(item, options)
|
254
|
+
end.values
|
255
|
+
end
|
238
256
|
|
239
257
|
# Internal: Redefine `write_one` to use the cache for the serialized JSON.
|
240
258
|
define_singleton_method(:write_one) do |external_writer, item, options = nil|
|
@@ -270,36 +288,60 @@ private
|
|
270
288
|
end.values
|
271
289
|
external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
|
272
290
|
end
|
291
|
+
|
292
|
+
define_serialization_shortcuts
|
293
|
+
end
|
294
|
+
alias_method :cached_with_key, :cached
|
295
|
+
|
296
|
+
def define_serialization_shortcuts(format = _default_format)
|
297
|
+
case format
|
298
|
+
when :json, :hash
|
299
|
+
singleton_class.alias_method :one, :"one_as_#{format}"
|
300
|
+
singleton_class.alias_method :many, :"many_as_#{format}"
|
301
|
+
else
|
302
|
+
raise ArgumentError, "Unknown serialization format: #{format.inspect}"
|
303
|
+
end
|
273
304
|
end
|
274
|
-
alias cached_with_key cached
|
275
305
|
|
276
306
|
# Internal: The writer to use to write to json
|
277
307
|
def new_json_writer
|
278
308
|
Oj::StringWriter.new(mode: :rails)
|
279
309
|
end
|
280
310
|
|
311
|
+
# Public: Identifiers are always serialized first.
|
312
|
+
#
|
313
|
+
# NOTE: We skip the id for non-persisted documents, since it doesn't
|
314
|
+
# actually identify the document (it will change once it's persisted).
|
315
|
+
def identifier(name = :id, **options)
|
316
|
+
add_attribute(name, attribute: :method, if: -> { !@object.new_record? }, **options, identifier: true)
|
317
|
+
end
|
318
|
+
|
281
319
|
# Public: Specify a collection of objects that should be serialized using
|
282
320
|
# the specified serializer.
|
283
|
-
def has_many(name, root: name,
|
284
|
-
|
321
|
+
def has_many(name, serializer:, root: name, as: root, **options, &block)
|
322
|
+
define_method(name, &block) if block
|
323
|
+
add_attribute(name, association: :many, as: as, serializer: serializer, **options)
|
285
324
|
end
|
286
325
|
|
287
326
|
# Public: Specify an object that should be serialized using the serializer.
|
288
|
-
def has_one(name, root: name,
|
289
|
-
|
327
|
+
def has_one(name, serializer:, root: name, as: root, **options, &block)
|
328
|
+
define_method(name, &block) if block
|
329
|
+
add_attribute(name, association: :one, as: as, serializer: serializer, **options)
|
290
330
|
end
|
331
|
+
# Alias: From a serializer perspective, the association type does not matter.
|
332
|
+
alias_method :belongs_to, :has_one
|
291
333
|
|
292
334
|
# Public: Specify an object that should be serialized using the serializer,
|
293
335
|
# but unlike `has_one`, this one will write the attributes directly without
|
294
336
|
# wrapping it in an object.
|
295
|
-
def flat_one(name,
|
296
|
-
|
337
|
+
def flat_one(name, serializer:, **options)
|
338
|
+
add_attribute(name, association: :flat, serializer: serializer, **options)
|
297
339
|
end
|
298
340
|
|
299
341
|
# Public: Specify which attributes are going to be obtained from indexing
|
300
342
|
# the object.
|
301
343
|
def hash_attributes(*method_names, **options)
|
302
|
-
options = { **options,
|
344
|
+
options = { **options, attribute: :hash }
|
303
345
|
method_names.each { |name| _attributes[name] = options }
|
304
346
|
end
|
305
347
|
|
@@ -310,32 +352,59 @@ private
|
|
310
352
|
#
|
311
353
|
# See ./benchmarks/document_benchmark.rb
|
312
354
|
def mongo_attributes(*method_names, **options)
|
313
|
-
|
314
|
-
|
355
|
+
identifier(:_id, as: :id, attribute: :mongoid, **options) if method_names.delete(:id)
|
356
|
+
attributes(*method_names, **options, attribute: :mongoid)
|
315
357
|
end
|
316
358
|
|
317
359
|
# Public: Specify which attributes are going to be obtained by calling a
|
318
360
|
# method in the object.
|
319
|
-
def attributes(*method_names, **
|
320
|
-
|
361
|
+
def attributes(*method_names, **methods_with_options)
|
362
|
+
attr_options = methods_with_options.extract!(:if, :as, :attribute)
|
363
|
+
attr_options[:attribute] ||= :method
|
364
|
+
|
365
|
+
method_names.each do |name|
|
366
|
+
add_attribute(name, attr_options)
|
367
|
+
end
|
368
|
+
|
369
|
+
methods_with_options.each do |name, options|
|
370
|
+
options = { as: options } if options.is_a?(Symbol)
|
371
|
+
add_attribute(name, options)
|
372
|
+
end
|
321
373
|
end
|
322
374
|
|
323
375
|
# Public: Specify which attributes are going to be obtained by calling a
|
324
376
|
# method in the serializer.
|
325
|
-
#
|
326
|
-
# NOTE: This can be one of the slowest strategies, when in doubt, measure.
|
327
377
|
def serializer_attributes(*method_names, **options)
|
328
|
-
|
378
|
+
attributes(*method_names, **options, attribute: :serializer)
|
329
379
|
end
|
330
380
|
|
331
381
|
# Syntax Sugar: Allows to use it before a method name.
|
332
382
|
#
|
333
383
|
# Example:
|
334
|
-
# attribute
|
384
|
+
# attribute
|
335
385
|
# def full_name
|
336
386
|
# "#{ first_name } #{ last_name }"
|
337
387
|
# end
|
338
|
-
|
388
|
+
def attribute(name = nil, **options, &block)
|
389
|
+
options[:attribute] = :serializer
|
390
|
+
if name
|
391
|
+
define_method(name, &block) if block
|
392
|
+
add_attribute(name, options)
|
393
|
+
else
|
394
|
+
@_current_attribute_options = options
|
395
|
+
end
|
396
|
+
end
|
397
|
+
alias_method :attr, :attribute
|
398
|
+
|
399
|
+
# Internal: Intercept a method definition, tying a type that was
|
400
|
+
# previously specified to the name of the attribute.
|
401
|
+
def method_added(name)
|
402
|
+
super(name)
|
403
|
+
if @_current_attribute_options
|
404
|
+
add_attribute(name, @_current_attribute_options)
|
405
|
+
@_current_attribute_options = nil
|
406
|
+
end
|
407
|
+
end
|
339
408
|
|
340
409
|
# Backwards Compatibility: Meant only to replace Active Model Serializers,
|
341
410
|
# calling a method in the serializer, or using `read_attribute_for_serialization`.
|
@@ -345,21 +414,48 @@ private
|
|
345
414
|
method_names.each do |method_name|
|
346
415
|
define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
|
347
416
|
end
|
348
|
-
|
417
|
+
attributes(*method_names, **options, attribute: :serializer)
|
349
418
|
end
|
350
419
|
|
351
|
-
|
420
|
+
# Internal: The default format to use for `render`, `one`, and `many`.
|
421
|
+
def _default_format
|
422
|
+
@_default_format = superclass.try(:_default_format) || :hash unless defined?(@_default_format)
|
423
|
+
@_default_format
|
424
|
+
end
|
352
425
|
|
353
|
-
|
354
|
-
|
426
|
+
# Internal: The strategy to use when sorting the fields.
|
427
|
+
#
|
428
|
+
# This setting is inherited from parent classes.
|
429
|
+
def _sort_attributes_by
|
430
|
+
@_sort_attributes_by = superclass.try(:_sort_attributes_by) unless defined?(@_sort_attributes_by)
|
431
|
+
@_sort_attributes_by
|
355
432
|
end
|
356
433
|
|
434
|
+
# Internal: The converter to use for serializer keys.
|
435
|
+
#
|
436
|
+
# This setting is inherited from parent classes.
|
437
|
+
def _transform_keys
|
438
|
+
@_transform_keys = superclass.try(:_transform_keys) unless defined?(@_transform_keys)
|
439
|
+
@_transform_keys
|
440
|
+
end
|
441
|
+
|
442
|
+
private
|
443
|
+
|
357
444
|
def add_attribute(name, options)
|
358
445
|
_attributes[name.to_s.freeze] = options
|
359
446
|
end
|
360
447
|
|
361
|
-
|
362
|
-
|
448
|
+
# Internal: Transforms the keys using the provided strategy.
|
449
|
+
def key_for(method_name, options)
|
450
|
+
key = options.fetch(:as, method_name)
|
451
|
+
_transform_keys ? _transform_keys.call(key) : key
|
452
|
+
end
|
453
|
+
|
454
|
+
# Internal: Whether the object should be serialized as a collection.
|
455
|
+
def many?(item)
|
456
|
+
item.is_a?(Array) ||
|
457
|
+
(defined?(ActiveRecord::Relation) && item.is_a?(ActiveRecord::Relation)) ||
|
458
|
+
(defined?(Mongoid::Association::Many) && item.is_a?(Mongoid::Association::Many))
|
363
459
|
end
|
364
460
|
|
365
461
|
# Internal: We generate code for the serializer to avoid the overhead of
|
@@ -368,38 +464,92 @@ private
|
|
368
464
|
#
|
369
465
|
# As a result, the performance is the same as writing the most efficient
|
370
466
|
# code by hand.
|
371
|
-
def
|
467
|
+
def code_to_write_to_json
|
372
468
|
<<~WRITE_TO_JSON
|
373
469
|
# Public: Writes this serializer content to a provided Oj::StringWriter.
|
374
|
-
def write_to_json(writer)
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
470
|
+
def write_to_json(writer, item, options = nil)
|
471
|
+
@object = item
|
472
|
+
@options = options
|
473
|
+
@memo.clear if defined?(@memo)
|
474
|
+
#{ _attributes.map { |method_name, options|
|
475
|
+
code_to_write_conditional(method_name, options) {
|
476
|
+
if options[:association]
|
477
|
+
code_to_write_association(method_name, options)
|
478
|
+
else
|
479
|
+
code_to_write_attribute(method_name, options)
|
480
|
+
end
|
380
481
|
}
|
381
|
-
}.join }
|
382
|
-
#{ _associations.map { |method_name, association_options|
|
383
|
-
write_conditional_body(method_name, association_options) {
|
384
|
-
write_association_body(method_name, association_options)
|
385
|
-
}
|
386
|
-
}.join}
|
482
|
+
}.join("\n ") }#{code_to_rescue_no_method if DEV_MODE}
|
387
483
|
end
|
388
484
|
WRITE_TO_JSON
|
389
485
|
end
|
390
486
|
|
391
|
-
# Internal:
|
392
|
-
#
|
487
|
+
# Internal: We generate code for the serializer to avoid the overhead of
|
488
|
+
# using variables for method names, having to iterate the list of attributes
|
489
|
+
# and associations, and the overhead of using `send` with dynamic methods.
|
393
490
|
#
|
394
|
-
#
|
491
|
+
# As a result, the performance is the same as writing the most efficient
|
492
|
+
# code by hand.
|
493
|
+
def code_to_render_as_hash
|
494
|
+
<<~RENDER_AS_HASH
|
495
|
+
# Public: Writes this serializer content to a Hash.
|
496
|
+
def render_as_hash(item, options = nil)
|
497
|
+
@object = item
|
498
|
+
@options = options
|
499
|
+
@memo.clear if defined?(@memo)
|
500
|
+
{
|
501
|
+
#{_attributes.map { |method_name, options|
|
502
|
+
code_to_render_conditionally(method_name, options) {
|
503
|
+
if options[:association]
|
504
|
+
code_to_render_association(method_name, options)
|
505
|
+
else
|
506
|
+
code_to_render_attribute(method_name, options)
|
507
|
+
end
|
508
|
+
}
|
509
|
+
}.join(",\n ")}
|
510
|
+
}#{code_to_rescue_no_method if DEV_MODE}
|
511
|
+
end
|
512
|
+
RENDER_AS_HASH
|
513
|
+
end
|
514
|
+
|
515
|
+
def code_to_rescue_no_method
|
516
|
+
<<~RESCUE_NO_METHOD
|
517
|
+
|
518
|
+
rescue NoMethodError => e
|
519
|
+
key = e.name.to_s.inspect
|
520
|
+
message = if respond_to?(e.name)
|
521
|
+
raise e, "Perhaps you meant to call \#{key} in \#{self.class} instead?\nTry using `serializer_attributes :\#{key}` or `attribute def \#{key}`.\n\#{e.message}"
|
522
|
+
elsif @object.respond_to?(e.name)
|
523
|
+
raise e, "Perhaps you meant to call \#{key} in \#{@object.class} instead?\nTry using `attributes :\#{key}`.\n\#{e.message}"
|
524
|
+
else
|
525
|
+
raise e
|
526
|
+
end
|
527
|
+
ensure
|
528
|
+
_check_instance_variables
|
529
|
+
RESCUE_NO_METHOD
|
530
|
+
end
|
531
|
+
|
532
|
+
# Internal: Detects any include methods defined in the serializer, or defines
|
395
533
|
# one by using the lambda passed in the `if` option, if any.
|
396
|
-
def
|
397
|
-
include_method_name = "include_#{method_name}?"
|
534
|
+
def check_conditional_method(method_name, options)
|
535
|
+
include_method_name = "include_#{method_name}#{'?' unless method_name.to_s.ends_with?('?')}"
|
398
536
|
if render_if = options[:if]
|
399
|
-
|
537
|
+
if render_if.is_a?(Symbol)
|
538
|
+
alias_method(include_method_name, render_if)
|
539
|
+
else
|
540
|
+
define_method(include_method_name, &render_if)
|
541
|
+
end
|
400
542
|
end
|
543
|
+
include_method_name if method_defined?(include_method_name)
|
544
|
+
end
|
401
545
|
|
402
|
-
|
546
|
+
# Internal: Returns the code to render an attribute or association
|
547
|
+
# conditionally.
|
548
|
+
#
|
549
|
+
# NOTE: Detects any include methods defined in the serializer, or defines
|
550
|
+
# one by using the lambda passed in the `if` option, if any.
|
551
|
+
def code_to_write_conditional(method_name, options)
|
552
|
+
if (include_method_name = check_conditional_method(method_name, options))
|
403
553
|
"if #{include_method_name};#{yield};end\n"
|
404
554
|
else
|
405
555
|
yield
|
@@ -407,31 +557,102 @@ private
|
|
407
557
|
end
|
408
558
|
|
409
559
|
# Internal: Returns the code for the association method.
|
410
|
-
def
|
560
|
+
def code_to_write_attribute(method_name, options)
|
561
|
+
key = key_for(method_name, options).to_s.inspect
|
562
|
+
|
563
|
+
case strategy = options.fetch(:attribute)
|
564
|
+
when :serializer
|
565
|
+
# Obtains the value by calling a method in the serializer.
|
566
|
+
"writer.push_value(#{method_name}, #{key})"
|
567
|
+
when :method
|
568
|
+
# Obtains the value by calling a method in the object, and writes it.
|
569
|
+
"writer.push_value(@object.#{method_name}, #{key})"
|
570
|
+
when :hash
|
571
|
+
# Writes a Hash value to JSON, works with String or Symbol keys.
|
572
|
+
"writer.push_value(@object[#{method_name.inspect}], #{key})"
|
573
|
+
when :mongoid
|
574
|
+
# Writes an Mongoid attribute to JSON, this is the fastest strategy.
|
575
|
+
"writer.push_value(@object.attributes['#{method_name}'], #{key})"
|
576
|
+
else
|
577
|
+
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
# Internal: Returns the code for the association method.
|
582
|
+
def code_to_write_association(method_name, options)
|
411
583
|
# Use a serializer method if defined, else call the association in the object.
|
412
584
|
association_method = method_defined?(method_name) ? method_name : "@object.#{method_name}"
|
413
|
-
|
414
|
-
serializer_class =
|
415
|
-
|
416
|
-
case
|
417
|
-
when :
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
585
|
+
key = key_for(method_name, options)
|
586
|
+
serializer_class = options.fetch(:serializer)
|
587
|
+
|
588
|
+
case type = options.fetch(:association)
|
589
|
+
when :one
|
590
|
+
<<~WRITE_ONE
|
591
|
+
if associated_object = #{association_method}
|
592
|
+
writer.push_key('#{key}')
|
593
|
+
#{serializer_class}.write_one(writer, associated_object)
|
594
|
+
end
|
423
595
|
WRITE_ONE
|
424
|
-
when :
|
425
|
-
|
426
|
-
|
427
|
-
|
596
|
+
when :many
|
597
|
+
<<~WRITE_MANY
|
598
|
+
writer.push_key('#{key}')
|
599
|
+
#{serializer_class}.write_many(writer, #{association_method})
|
428
600
|
WRITE_MANY
|
429
|
-
when :
|
430
|
-
|
431
|
-
|
601
|
+
when :flat
|
602
|
+
<<~WRITE_FLAT
|
603
|
+
#{serializer_class}.write_to_json(writer, #{association_method})
|
432
604
|
WRITE_FLAT
|
433
605
|
else
|
434
|
-
raise ArgumentError, "Unknown
|
606
|
+
raise ArgumentError, "Unknown association type: #{type.inspect}"
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
# Internal: Returns the code to render an attribute or association
|
611
|
+
# conditionally.
|
612
|
+
#
|
613
|
+
# NOTE: Detects any include methods defined in the serializer, or defines
|
614
|
+
# one by using the lambda passed in the `if` option, if any.
|
615
|
+
def code_to_render_conditionally(method_name, options)
|
616
|
+
if (include_method_name = check_conditional_method(method_name, options))
|
617
|
+
"**(#{include_method_name} ? {#{yield}} : {})"
|
618
|
+
else
|
619
|
+
yield
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
# Internal: Returns the code for the attribute method.
|
624
|
+
def code_to_render_attribute(method_name, options)
|
625
|
+
key = key_for(method_name, options)
|
626
|
+
case strategy = options.fetch(:attribute)
|
627
|
+
when :serializer
|
628
|
+
"#{key}: #{method_name}"
|
629
|
+
when :method
|
630
|
+
"#{key}: @object.#{method_name}"
|
631
|
+
when :hash
|
632
|
+
"#{key}: @object[#{method_name.inspect}]"
|
633
|
+
when :mongoid
|
634
|
+
"#{key}: @object.attributes['#{method_name}']"
|
635
|
+
else
|
636
|
+
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
# Internal: Returns the code for the association method.
|
641
|
+
def code_to_render_association(method_name, options)
|
642
|
+
# Use a serializer method if defined, else call the association in the object.
|
643
|
+
association = method_defined?(method_name) ? method_name : "@object.#{method_name}"
|
644
|
+
key = key_for(method_name, options)
|
645
|
+
serializer_class = options.fetch(:serializer)
|
646
|
+
|
647
|
+
case type = options.fetch(:association)
|
648
|
+
when :one
|
649
|
+
"#{key}: (one_item = #{association}) ? #{serializer_class}.one_as_hash(one_item) : nil"
|
650
|
+
when :many
|
651
|
+
"#{key}: #{serializer_class}.many_as_hash(#{association})"
|
652
|
+
when :flat
|
653
|
+
"**#{serializer_class}.one_as_hash(#{association})"
|
654
|
+
else
|
655
|
+
raise ArgumentError, "Unknown association type: #{type.inspect}"
|
435
656
|
end
|
436
657
|
end
|
437
658
|
|
@@ -447,16 +668,28 @@ private
|
|
447
668
|
|
448
669
|
# Internal: Cache key to set a thread-local instance.
|
449
670
|
def instance_key
|
450
|
-
|
451
|
-
@instance_key = "#{name.underscore}_instance_#{object_id}".to_sym
|
671
|
+
@instance_key ||= begin
|
452
672
|
# We take advantage of the fact that this method will always be called
|
453
|
-
# before instantiating a serializer to
|
454
|
-
|
455
|
-
|
673
|
+
# before instantiating a serializer, to apply last minute adjustments.
|
674
|
+
_prepare_serializer
|
675
|
+
"#{name.underscore}_instance_#{object_id}".to_sym
|
456
676
|
end
|
457
|
-
|
677
|
+
end
|
678
|
+
|
679
|
+
# Internal: Generates write_to_json and render_as_hash methods optimized for
|
680
|
+
# the specified configuration.
|
681
|
+
def _prepare_serializer
|
682
|
+
if _sort_attributes_by
|
683
|
+
@_attributes = _attributes.sort_by { |key, options|
|
684
|
+
_sort_attributes_by.call(key, options)
|
685
|
+
}.to_h
|
686
|
+
end
|
687
|
+
class_eval(code_to_write_to_json)
|
688
|
+
class_eval(code_to_render_as_hash)
|
458
689
|
end
|
459
690
|
end
|
691
|
+
|
692
|
+
define_serialization_shortcuts
|
460
693
|
end
|
461
694
|
|
462
695
|
Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer)
|