json_serializers 2.0.3

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