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.
@@ -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
- @object.try(:options) || DEFAULT_OPTIONS
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
- prepend(Module.new do
54
- def write_flat(writer, item)
55
- if instance_values.keys.any? { |key| !ALLOWED_INSTANCE_VARIABLES.include?(key) }
56
- bad_keys = instance_values.keys.reject { |key| ALLOWED_INSTANCE_VARIABLES.include?(key) }
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
- write_flat(writer, item)
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
- defined?(@memo) ? @memo : @memo = OjSerializers::Memo.new
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
- # Strategy: Writes an Mongoid attribute to JSON, this is the fastest strategy.
109
- def write_value_using_mongoid_strategy(writer, key)
110
- writer.push_value(@object.attributes[key], key)
111
- end
112
-
113
- # Strategy: Writes a Hash value to JSON, works with String or Symbol keys.
114
- def write_value_using_hash_strategy(writer, key)
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
- # Strategy: Obtains the value by calling a method in the serializer.
124
- def write_value_using_serializer_strategy(writer, key)
125
- writer.push_value(send(key), key)
126
- end
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
- # Override to detect missing attribute errors locally.
129
- if DEV_MODE
130
- alias original_write_value_using_method_strategy write_value_using_method_strategy
131
- def write_value_using_method_strategy(writer, key)
132
- original_write_value_using_method_strategy(writer, key)
133
- rescue NoMethodError => e
134
- raise e, "Perhaps you meant to call #{key.inspect} in #{self.class.name} instead?\nTry using `serializer_attributes :#{key}` or `attribute def #{key}`.\n#{e.message}"
135
- end
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
- class << self
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, :write_flat, to: :instance
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
- alias non_cached_write_one write_one
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
- alias non_cached_write_many write_many
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 one(item, options = nil)
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 many(items, options = nil)
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: Creates an alias for the internal object.
199
- def object_as(name)
200
- define_method(name) { @object }
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 = superclass.try(:_attributes)&.dup || {} unless defined?(@_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, serializer:, **options)
284
- add_association(name, write_method: :write_many, root: root, serializer: serializer, **options)
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, serializer:, **options)
289
- add_association(name, write_method: :write_one, root: root, serializer: serializer, **options)
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, root: false, serializer:, **options)
296
- add_association(name, write_method: :write_flat, root: root, serializer: serializer, **options)
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, strategy: :write_value_using_hash_strategy }
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
- add_attribute('id', **options, strategy: :write_value_using_id_strategy) if method_names.delete(:id)
314
- add_attributes(method_names, **options, strategy: :write_value_using_mongoid_strategy)
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, **options)
320
- add_attributes(method_names, **options, strategy: :write_value_using_method_strategy)
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
- add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
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
- alias attribute serializer_attributes
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
- add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
417
+ attributes(*method_names, **options, attribute: :serializer)
349
418
  end
350
419
 
351
- private
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
- def add_attributes(names, options)
354
- names.each { |name| add_attribute(name, options) }
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
- def add_association(name, options)
362
- _associations[name.to_s.freeze] = options
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 write_to_json_body
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
- #{ _attributes.map { |method_name, attribute_options|
376
- write_conditional_body(method_name, attribute_options) {
377
- <<-WRITE_ATTRIBUTE
378
- #{attribute_options.fetch(:strategy)}(writer, #{method_name.inspect})
379
- WRITE_ATTRIBUTE
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: Returns the code to render an attribute or association
392
- # conditionally.
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
- # NOTE: Detects any include methods defined in the serializer, or defines
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 write_conditional_body(method_name, options)
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
- define_method(include_method_name, &render_if)
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
- if method_defined?(include_method_name)
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 write_association_body(method_name, association_options)
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
- association_root = association_options[:root]
414
- serializer_class = association_options.fetch(:serializer)
415
-
416
- case write_method = association_options.fetch(:write_method)
417
- when :write_one
418
- <<-WRITE_ONE
419
- if associated_object = #{association_method}
420
- writer.push_key(#{association_root.to_s.inspect})
421
- #{serializer_class}.write_one(writer, associated_object)
422
- end
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 :write_many
425
- <<-WRITE_MANY
426
- writer.push_key(#{association_root.to_s.inspect})
427
- #{serializer_class}.write_many(writer, #{association_method})
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 :write_flat
430
- <<-WRITE_FLAT
431
- #{serializer_class}.write_flat(writer, #{association_method})
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 write_method #{write_method}"
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
- unless defined?(@instance_key)
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 define the write_to_json method.
454
- class_eval(write_to_json_body)
455
- raise ArgumentError, "You must use `cached ->(object) { ... }` in order to specify a different cache key when subclassing #{name}." if method_defined?(:cache_key) || respond_to?(:cache_key)
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
- @instance_key
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)