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.
@@ -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)