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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +61 -0
- data/README.md +699 -0
- data/lib/json_serializers/compat.rb +66 -0
- data/lib/json_serializers/controller_serialization.rb +32 -0
- data/lib/json_serializers/json_string_encoder.rb +41 -0
- data/lib/json_serializers/json_value.rb +34 -0
- data/lib/json_serializers/memo.rb +20 -0
- data/lib/json_serializers/serializer.rb +726 -0
- data/lib/json_serializers/setup.rb +31 -0
- data/lib/json_serializers/sugar.rb +17 -0
- data/lib/json_serializers/version.rb +5 -0
- data/lib/json_serializers.rb +6 -0
- metadata +61 -0
@@ -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)
|