oj_serializers 1.0.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|