oj_serializers 1.0.2 → 2.0.0.pre.beta.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a03657fa651d7b3309e4d182dc2a8446733190cd92833ebcfd20a3bc24efeb9b
4
- data.tar.gz: fa3169a06d80fbc73324eda88866a3f549f59ccf5d75085ac988a6e571d7d8a2
3
+ metadata.gz: 4ebc24ba352b46585589ae760b579009e18e9de772b1d015eb0fdfa465a39b93
4
+ data.tar.gz: 475283ebe5897716eb1812a474358ddc3775746e1e0f8ff011204fc3ff76219e
5
5
  SHA512:
6
- metadata.gz: 3b1fe6d00cec3052d51bb4f508731a458d52b23645e936e83992f144d638d381abaa95cbb0adbb8280bf41f2f13e2fcdbe6916556a9d5ecbac8135ceffc37f00
7
- data.tar.gz: 77372418c201970662770f7ab1bbfeeca7a9260bf155ff2431cae7003dbd6dbed5d1dafaf9e149347ec452015d1656b2430089688316e41b1d837d0b5e971cfc
6
+ metadata.gz: e4a0c4e060fbac9cc04ef2e6ce31406fa5aa83052989a272e2bba6c1b7c30767e82bd22aa51b016716adfff8bb039442a7725f25ddf7093ca4b33de646360181
7
+ data.tar.gz: d0d41a7a283e26831222a99b96669498eb01b14dac383da008db053c769052ca0e91b1078f1c0d9c797faadd082b3ecaeabce37fa267d617bc2bc039a05dcaec
data/README.md CHANGED
@@ -10,13 +10,14 @@ Oj Serializers
10
10
  </p>
11
11
  </h1>
12
12
 
13
- JSON serializers for Ruby, built on top of the powerful [`oj`][oj] library.
13
+ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] library.
14
14
 
15
15
  [oj]: https://github.com/ohler55/oj
16
16
  [mongoid]: https://github.com/mongodb/mongoid
17
17
  [ams]: https://github.com/rails-api/active_model_serializers
18
18
  [jsonapi]: https://github.com/jsonapi-serializer/jsonapi-serializer
19
19
  [panko]: https://github.com/panko-serializer/panko_serializer
20
+ [blueprinter]: https://github.com/procore/blueprinter
20
21
  [benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
21
22
  [raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/master/benchmarks/document_benchmark.rb
22
23
  [sugar]: https://github.com/ElMassimo/oj_serializers/blob/master/lib/oj_serializers/sugar.rb#L14
@@ -24,11 +25,13 @@ JSON serializers for Ruby, built on top of the powerful [`oj`][oj] library.
24
25
  [design]: https://github.com/ElMassimo/oj_serializers#design-
25
26
  [raw_json]: https://github.com/ohler55/oj/issues/542
26
27
  [trailing_commas]: https://maximomussini.com/posts/trailing-commas/
28
+ [render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
29
+ [sorbet]: https://sorbet.org/
27
30
 
28
31
  ## Why? 🤔
29
32
 
30
- [`ActiveModel::Serializer`][ams] has a nice DSL, but it allocates many objects leading
31
- to memory bloat, time spent on GC, and lower performance.
33
+ [`ActiveModel::Serializer`][ams] has a nice DSL, but it allocates many objects
34
+ leading to memory bloat, time spent on GC, and lower performance.
32
35
 
33
36
  `Oj::Serializer` provides a similar API, with [better performance][benchmarks].
34
37
 
@@ -41,7 +44,6 @@ Learn more about [how this library achieves its performance][design].
41
44
  - Support for `has_one` and `has_many`, compose with `flat_one`
42
45
  - Useful development checks to avoid typos and mistakes
43
46
  - Integrates nicely with Rails controllers
44
- - Caching
45
47
 
46
48
  ## Installation 💿
47
49
 
@@ -58,13 +60,13 @@ And then run:
58
60
  ## Usage 🚀
59
61
 
60
62
  You can define a serializer by subclassing `Oj::Serializer`, and specify which
61
- attributes should be serialized to JSON.
63
+ attributes should be serialized.
62
64
 
63
65
  ```ruby
64
66
  class AlbumSerializer < Oj::Serializer
65
67
  attributes :name, :genres
66
68
 
67
- attribute \
69
+ serialize
68
70
  def release
69
71
  album.release_date.strftime('%B %d, %Y')
70
72
  end
@@ -76,83 +78,63 @@ end
76
78
  <details>
77
79
  <summary>Example Output</summary>
78
80
 
79
- ```json
81
+ ```ruby
80
82
  {
81
- "name": "Abraxas",
82
- "genres": [
83
+ name: "Abraxas",
84
+ genres: [
83
85
  "Pyschodelic Rock",
84
86
  "Blues Rock",
85
87
  "Jazz Fusion",
86
- "Latin Rock"
88
+ "Latin Rock",
87
89
  ],
88
- "release": "September 23, 1970",
89
- "songs": [
90
+ release: "September 23, 1970",
91
+ songs: [
90
92
  {
91
- "track": 1,
92
- "name": "Sing Winds, Crying Beasts",
93
- "composers": [
94
- "Michael Carabello"
95
- ]
93
+ track: 1,
94
+ name: "Sing Winds, Crying Beasts",
95
+ composers: ["Michael Carabello"],
96
96
  },
97
97
  {
98
- "track": 2,
99
- "name": "Black Magic Woman / Gypsy Queen",
100
- "composers": [
101
- "Peter Green",
102
- "Gábor Szabó"
103
- ]
98
+ track: 2,
99
+ name: "Black Magic Woman / Gypsy Queen",
100
+ composers: ["Peter Green", "Gábor Szabó"],
104
101
  },
105
102
  {
106
- "track": 3,
107
- "name": "Oye como va",
108
- "composers": [
109
- "Tito Puente"
110
- ]
103
+ track: 3,
104
+ name: "Oye como va",
105
+ composers: ["Tito Puente"],
111
106
  },
112
107
  {
113
- "track": 4,
114
- "name": "Incident at Neshabur",
115
- "composers": [
116
- "Alberto Gianquinto",
117
- "Carlos Santana"
118
- ]
108
+ track: 4,
109
+ name: "Incident at Neshabur",
110
+ composers: ["Alberto Gianquinto", "Carlos Santana"],
119
111
  },
120
112
  {
121
- "track": 5,
122
- "name": "Se acabó",
123
- "composers": [
124
- "José Areas"
125
- ]
113
+ track: 5,
114
+ name: "Se acabó",
115
+ composers: ["José Areas"],
126
116
  },
127
117
  {
128
- "track": 6,
129
- "name": "Mother's Daughter",
130
- "composers": [
131
- "Gregg Rolie"
132
- ]
118
+ track: 6,
119
+ name: "Mother's Daughter",
120
+ composers: ["Gregg Rolie"],
133
121
  },
134
122
  {
135
- "track": 7,
136
- "name": "Samba pa ti",
137
- "composers": [
138
- "Santana"
139
- ]
123
+ track: 7,
124
+ name: "Samba pa ti",
125
+ composers: ["Santana"],
140
126
  },
141
127
  {
142
- "track": 8,
143
- "name": "Hope You're Feeling Better",
144
- "composers": [
145
- "Rolie"
146
- ]
128
+ track: 8,
129
+ name: "Hope You're Feeling Better",
130
+ composers: ["Rolie"],
147
131
  },
148
132
  {
149
- "track": 9,
150
- "name": "El Nicoya",
151
- "composers": [
152
- "Areas"
153
- ]
154
- }
155
- ]
133
+ track: 9,
134
+ name: "El Nicoya",
135
+ composers: ["Areas"],
136
+ },
137
+ ],
156
138
  }
157
139
  ```
158
140
  </details>
@@ -209,8 +191,6 @@ render json: {
209
191
 
210
192
  You can use these serializers inside arrays, hashes, or even inside `ActiveModel::Serializer` by using a method in the serializer.
211
193
 
212
- Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
213
-
214
194
  ## Attributes DSL 🛠
215
195
 
216
196
  Attributes methods can be used to define which model attributes should be serialized
@@ -238,22 +218,23 @@ maintainable.
238
218
 
239
219
  Obtains the attribute value by calling a method defined in the serializer.
240
220
 
241
-
242
- You may call [`serializer_attributes`](https://github.com/ElMassimo/oj_serializers/blob/master/spec/support/serializers/song_serializer.rb#L13-L15) or use the `attribute` inline syntax:
221
+ Simply call `serialize` right before defining the method, and it will be serialized:
243
222
 
244
223
  ```ruby
245
224
  class PlayerSerializer < Oj::Serializer
246
- attribute \
225
+ serialize
247
226
  def full_name
248
227
  "#{player.first_name} #{player.last_name}"
249
228
  end
250
229
  end
251
230
  ```
252
231
 
232
+ > This inline syntax was inspired by how types are defined in [`sorbet`][sorbet].
233
+
253
234
  Instance methods can access the object by the serializer name without the
254
235
  `Serializer` suffix, `player` in the example above, or directly as `@object`.
255
236
 
256
- You can customize this by using [`object_as`](https://github.com/ElMassimo/oj_serializers#using-a-different-alias-for-the-internal-object).
237
+ You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
257
238
 
258
239
  ### `ams_attributes` 🐌
259
240
 
@@ -307,7 +288,8 @@ end
307
288
 
308
289
  Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
309
290
 
310
- The value for the association is obtained from a serializer method if defined, or by calling the method in the object being serialized.
291
+ The value for the association is obtained from a serializer method if defined,
292
+ or by calling the method in the object being serialized.
311
293
 
312
294
  You must specificy which serializer to use with the `serializer` option.
313
295
 
@@ -321,16 +303,16 @@ class SongSerializer < Oj::Serializer
321
303
  end
322
304
  ```
323
305
 
324
- The associations DSL is more concise and achieves better performance, so prefer to use it instead of manually definining attributes:
306
+ The associations DSL is essentially a shortcut for defining attributes manually:
325
307
 
326
308
  ```ruby
327
309
  class SongSerializer < SongMetadataSerializer
328
- attribute \
310
+ serialize
329
311
  def album
330
312
  AlbumSerializer.one(song.album)
331
313
  end
332
314
 
333
- attribute \
315
+ serialize
334
316
  def composers
335
317
  ComposerSerializer.many(song.composers)
336
318
  end
@@ -348,12 +330,26 @@ class DiscographySerializer < Oj::Serializer
348
330
  object_as :artist
349
331
 
350
332
  # Now we can use `artist` instead of `object` or `discography`.
333
+ serialize
351
334
  def latest_albums
352
335
  artist.albums.desc(:year)
353
336
  end
354
337
  end
355
338
  ```
356
339
 
340
+ ### Aliasing or renaming attributes
341
+
342
+ You can pass `as` when defining an attribute or association to serialize it
343
+ using a different key:
344
+
345
+ ```ruby
346
+ class SongSerializer < Oj::Serializer
347
+ has_one :album, as: :latest_album, serializer: AlbumSerializer
348
+
349
+ attribute :title, as: :name
350
+ end
351
+ ```
352
+
357
353
  ### Rendering an attribute conditionally
358
354
 
359
355
  All the attributes and association methods can take an `if` option to render conditionally.
@@ -381,7 +377,7 @@ Use `memo` for memoization and storing temporary information.
381
377
  class DownloadSerializer < Oj::Serializer
382
378
  attributes :filename, :size
383
379
 
384
- attribute \
380
+ serialize
385
381
  def progress
386
382
  "#{ last_event&.progress || 0 }%"
387
383
  end
@@ -396,26 +392,112 @@ private
396
392
  end
397
393
  ```
398
394
 
399
- ### Caching 📦
395
+ ### Writing directly to JSON
396
+
397
+ In some corner cases it might be faster to serialize using a `Oj::StringWriter`.
400
398
 
401
- Use `cached` to leverage key-based caching, which calls `cache_key` in the object. You can also provide a lambda to `cached_with_key` to define a custom key:
399
+ You can toggle this mode at a serializer level by using `default_format :json`,
400
+ or configure it globally from your base serializer.
402
401
 
403
402
  ```ruby
404
- class CachedUserSerializer < UserSerializer
405
- cached_with_key ->(user) {
406
- "#{ user.id }/#{ user.current_sign_in_at }"
407
- }
403
+ class BaseSerializer < Oj::Serializer
404
+ default_format :json # :hash is the default
408
405
  end
409
406
  ```
410
407
 
411
- It will leverage `fetch_multi` when serializing a collection with `many` or `has_many`, to minimize the amount of round trips needed to read and write all items to cache. This works specially well if your cache store also supports `write_multi`.
408
+ This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
409
+ so that the serializer writes directly to JSON instead of returning a Hash.
412
410
 
413
- Usually serialization happens so fast that __turning caching on can be slower__. Always benchmark to make sure it's worth it, and use caching only for time-consuming or deeply nested structures.
411
+ Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
412
+
413
+ <details>
414
+ <summary>Example Output</summary>
415
+
416
+ ```json
417
+ {
418
+ "name": "Abraxas",
419
+ "genres": [
420
+ "Pyschodelic Rock",
421
+ "Blues Rock",
422
+ "Jazz Fusion",
423
+ "Latin Rock"
424
+ ],
425
+ "release": "September 23, 1970",
426
+ "songs": [
427
+ {
428
+ "track": 1,
429
+ "name": "Sing Winds, Crying Beasts",
430
+ "composers": [
431
+ "Michael Carabello"
432
+ ]
433
+ },
434
+ {
435
+ "track": 2,
436
+ "name": "Black Magic Woman / Gypsy Queen",
437
+ "composers": [
438
+ "Peter Green",
439
+ "Gábor Szabó"
440
+ ]
441
+ },
442
+ {
443
+ "track": 3,
444
+ "name": "Oye como va",
445
+ "composers": [
446
+ "Tito Puente"
447
+ ]
448
+ },
449
+ {
450
+ "track": 4,
451
+ "name": "Incident at Neshabur",
452
+ "composers": [
453
+ "Alberto Gianquinto",
454
+ "Carlos Santana"
455
+ ]
456
+ },
457
+ {
458
+ "track": 5,
459
+ "name": "Se acabó",
460
+ "composers": [
461
+ "José Areas"
462
+ ]
463
+ },
464
+ {
465
+ "track": 6,
466
+ "name": "Mother's Daughter",
467
+ "composers": [
468
+ "Gregg Rolie"
469
+ ]
470
+ },
471
+ {
472
+ "track": 7,
473
+ "name": "Samba pa ti",
474
+ "composers": [
475
+ "Santana"
476
+ ]
477
+ },
478
+ {
479
+ "track": 8,
480
+ "name": "Hope You're Feeling Better",
481
+ "composers": [
482
+ "Rolie"
483
+ ]
484
+ },
485
+ {
486
+ "track": 9,
487
+ "name": "El Nicoya",
488
+ "composers": [
489
+ "Areas"
490
+ ]
491
+ }
492
+ ]
493
+ }
494
+ ```
495
+ </details>
414
496
 
415
497
  ## Design 📐
416
498
 
417
499
  Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
418
- JSON, this implementation uses `Oj::StringWriter` to write JSON directly,
500
+ JSON, this implementation can use `Oj::StringWriter` to write JSON directly,
419
501
  greatly reducing the overhead of allocating and garbage collecting the hashes.
420
502
 
421
503
  It also allocates a single instance per serializer class, which makes it easy
@@ -425,16 +507,17 @@ to use, while keeping memory usage under control.
425
507
 
426
508
  `ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
427
509
 
428
- Other libraries such as [`jsonapi-serializer`][jsonapi] evaluate serializers in the context of
429
- a `class` instead of an `instance` of a class. Although it is efficient in terms
430
- of memory usage, the downside is that you can't use instance methods or local
431
- memoization, and any mixins must be applied to the class itself.
510
+ Other libraries such as [`blueprinter`][blueprinter] [`jsonapi-serializer`][jsonapi]
511
+ evaluate serializers in the context of a `class` instead of an `instance` of a class.
512
+ The downside is that you can't use instance methods or local memoization, and any
513
+ mixins must be applied to the class itself.
432
514
 
433
515
  [`panko-serializer`][panko] also uses `Oj::StringWriter`, but it has the big downside of having to own the entire render tree. Putting a serializer inside a Hash or an Active Model Serializer and serializing that to JSON doesn't work, making a gradual migration harder to achieve. Also, it's optimized for Active Record but I needed good Mongoid support.
434
516
 
435
517
  `Oj::Serializer` combines some of these ideas, by using instances, but reusing them to avoid object allocations. Serializing 10,000 items instantiates a single serializer. Unlike `panko-serializer`, it doesn't suffer from [double encoding problems](https://panko.dev/docs/response-bag) so it's easier to use.
436
518
 
437
- As a result, migrating from `active_model_serializers` is relatively straightforward because instance methods, inheritance, and mixins work as usual.
519
+ As a result, migrating from `active_model_serializers` is relatively
520
+ straightforward because instance methods, inheritance, and mixins work as usual.
438
521
 
439
522
  ## Formatting 📏
440
523
 
@@ -6,28 +6,13 @@ require 'active_model_serializers'
6
6
  # as well.
7
7
  class ActiveModel::Serializer
8
8
  # JsonStringEncoder: Used internally to write a single object to JSON.
9
- #
10
- # Returns nothing.
11
- def self.write_one(writer, object, options)
12
- writer.push_value(new(object, options))
9
+ def self.one(object, options = nil)
10
+ new(object, options)
13
11
  end
14
12
 
15
13
  # JsonStringEncoder: Used internally to write an array of objects to JSON.
16
- #
17
- # Returns nothing.
18
- def self.write_many(writer, array, options)
19
- writer.push_array
20
- array.each do |object|
21
- write_one(writer, object, options)
22
- end
23
- writer.pop
24
- end
25
-
26
- # JsonStringEncoder: Used internally to instantiate an Oj::StringWriter.
27
- #
28
- # Returns an Oj::StringWriter.
29
- def self.new_json_writer
30
- OjSerializers::Serializer.send(:new_json_writer)
14
+ def self.many(array, options = nil)
15
+ array.map { |object| new(object, options) }
31
16
  end
32
17
  end
33
18
 
@@ -20,10 +20,10 @@ module OjSerializers::ControllerSerialization
20
20
  #
21
21
  # which is more performant.
22
22
  %i[_render_option_json _render_with_renderer_json].each do |renderer_method|
23
- define_method renderer_method do |resource, **options|
23
+ define_method renderer_method do |resource, options = {}|
24
24
  serializer_class = options[:serializer] || options[:each_serializer]
25
25
  if serializer_class && serializer_class < OjSerializers::Serializer
26
- super(OjSerializers::JsonStringEncoder.encode_to_json(resource, options), options.except(:root, :serializer, :each_serializer))
26
+ super(OjSerializers::JsonStringEncoder.encode_to_json(resource, **options), options.except(:root, :serializer, :each_serializer))
27
27
  else
28
28
  super(resource, **options)
29
29
  end
@@ -14,43 +14,27 @@ module OjSerializers::JsonStringEncoder
14
14
  # regardless of whether a serializer is specified or not.
15
15
  #
16
16
  # Returns a JSON string.
17
- def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **extras)
18
- # NOTE: Serializers may override `new_json_writer` to modify the behavior.
19
- writer = (serializer || each_serializer || OjSerializers::Serializer).send(:new_json_writer)
20
-
21
- if root
22
- writer.push_object
23
- writer.push_key(root.to_s)
24
- end
25
-
26
- if serializer
27
- serializer.write_one(writer, object, extras)
17
+ def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **options)
18
+ result = if serializer
19
+ serializer.one(object, options)
28
20
  elsif each_serializer
29
- each_serializer.write_many(writer, object, extras)
21
+ each_serializer.many(object, options)
30
22
  elsif object.is_a?(String)
31
- return object unless root
32
-
33
- writer.push_json(object)
23
+ OjSerializers::JsonValue.new(object)
34
24
  else
35
- writer.push_value(object)
25
+ object
36
26
  end
37
-
38
- writer.pop if root
39
-
40
- writer.to_json
27
+ Oj.dump(root ? { root => result } : result)
41
28
  end
42
29
 
43
30
  if OjSerializers::Serializer::DEV_MODE
44
31
  alias actual_encode_to_json encode_to_json
45
32
  # Internal: Allows to detect misusage of the options during development.
46
- def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **extras)
47
- if serializer && serializer < OjSerializers::Serializer
48
- raise ArgumentError, 'You must use `each_serializer` when serializing collections' if object.respond_to?(:each)
49
- end
50
- if each_serializer && each_serializer < OjSerializers::Serializer
51
- raise ArgumentError, 'You must use `serializer` when serializing a single object' unless object.respond_to?(:each)
52
- end
53
- actual_encode_to_json(object, root: root, serializer: serializer, each_serializer: each_serializer, **extras)
33
+ def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **options)
34
+ raise ArgumentError, 'You must use `each_serializer` when serializing collections' if serializer && serializer < OjSerializers::Serializer && object.respond_to?(:map)
35
+ raise ArgumentError, 'You must use `serializer` when serializing a single object' if each_serializer && each_serializer < OjSerializers::Serializer && !object.respond_to?(:map)
36
+
37
+ actual_encode_to_json(object, root: root, serializer: serializer, each_serializer: each_serializer, **options)
54
38
  end
55
39
  end
56
40
  end
@@ -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(',')}"
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,42 @@ 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?
106
- end
107
-
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)
82
+ @memo ||= OjSerializers::Memo.new
111
83
  end
112
84
 
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
+ define_serialization_shortcuts(value)
90
+ end
122
91
 
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
92
+ # Public: Allows to sort fields by name instead.
93
+ def sort_attributes_by(value)
94
+ @_sort_attributes_by = case value
95
+ when :name then ->(name, options) { options[:identifier] ? "__#{name}" : name }
96
+ when Proc then value
97
+ else
98
+ raise ArgumentError, "Unknown sorting option: #{value.inspect}"
99
+ end
100
+ end
127
101
 
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.
102
+ # Public: Allows to sort fields by name instead.
103
+ def transform_keys(transformer = nil, &block)
104
+ @_transform_keys = case (transformer ||= block)
105
+ when :camelize, :camel_case then ->(key) { key.to_s.camelize(:lower) }
106
+ when Symbol then transformer.to_proc
107
+ when Proc then transformer
108
+ else
109
+ raise(ArgumentError, "Expected transform_keys to be callable, got: #{transformer.inspect}")
144
110
  end
145
- rescue StandardError => e
146
- raise ActiveModel::MissingAttributeError, "#{e.message} in #{self.class} for #{@object.inspect}"
147
111
  end
148
- end
149
112
 
150
- class << self
113
+ # Public: Creates an alias for the internal object.
114
+ def object_as(name, **)
115
+ define_method(name) { @object }
116
+ end
117
+
151
118
  # Internal: We want to discourage instantiating serializers directly, as it
152
119
  # prevents the possibility of reusing an instance.
153
120
  #
@@ -156,15 +123,17 @@ private
156
123
 
157
124
  # Internal: Delegates to the instance methods, the advantage is that we can
158
125
  # reuse the same serializer instance to serialize different objects.
159
- delegate :write_one, :write_many, :write_flat, to: :instance
126
+ delegate :write_one, :write_many, :write_to_json, to: :instance
160
127
 
161
- # Internal: Keep a reference to the default `write_one` method so that we
162
- # can use it inside cached overrides and benchmark tests.
163
- alias non_cached_write_one write_one
128
+ # Helper: Serializes one or more items.
129
+ def render(item, options = nil)
130
+ many?(item) ? many(item, options) : one(item, options)
131
+ end
164
132
 
165
- # Internal: Keep a reference to the default `write_many` method so that we
166
- # can use it inside cached overrides and benchmark tests.
167
- alias non_cached_write_many write_many
133
+ # Helper: Serializes one or more items.
134
+ def render_as_hash(item, options = nil)
135
+ many?(item) ? many_as_hash(item, options) : one_as_hash(item, options)
136
+ end
168
137
 
169
138
  # Helper: Serializes the item unless it's nil.
170
139
  def one_if(item, options = nil)
@@ -177,7 +146,7 @@ private
177
146
  # options - list of external options to pass to the sub class (available in `item.options`)
178
147
  #
179
148
  # Returns an Oj::StringWriter instance, which is encoded as raw json.
180
- def one(item, options = nil)
149
+ def one_as_json(item, options = nil)
181
150
  writer = new_json_writer
182
151
  write_one(writer, item, options)
183
152
  writer
@@ -189,15 +158,33 @@ private
189
158
  # options - list of external options to pass to the sub class (available in `item.options`)
190
159
  #
191
160
  # Returns an Oj::StringWriter instance, which is encoded as raw json.
192
- def many(items, options = nil)
161
+ def many_as_json(items, options = nil)
193
162
  writer = new_json_writer
194
163
  write_many(writer, items, options)
195
164
  writer
196
165
  end
197
166
 
198
- # Public: Creates an alias for the internal object.
199
- def object_as(name)
200
- define_method(name) { @object }
167
+ # Public: Renders the configured attributes for the specified object,
168
+ # without serializing to JSON.
169
+ #
170
+ # item - the item to serialize
171
+ # options - list of external options to pass to the sub class (available in `item.options`)
172
+ #
173
+ # Returns a Hash, with the attributes specified in the serializer.
174
+ def one_as_hash(item, options = nil)
175
+ instance.render_as_hash(item, options)
176
+ end
177
+
178
+ # Public: Renders an array of items using this serializer, without
179
+ # serializing to JSON.
180
+ #
181
+ # items - Must respond to `each`.
182
+ # options - list of external options to pass to the sub class (available in `item.options`)
183
+ #
184
+ # Returns an Array of Hash, each with the attributes specified in the serializer.
185
+ def many_as_hash(items, options = nil)
186
+ serializer = instance
187
+ items.map { |item| serializer.render_as_hash(item, options) }
201
188
  end
202
189
 
203
190
  # Internal: Will alias the object according to the name of the wrapper class.
@@ -211,95 +198,53 @@ private
211
198
  #
212
199
  # Any attributes defined in parent classes are inherited.
213
200
  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
201
+ @_attributes ||= superclass.try(:_attributes)&.dup || {}
223
202
  end
224
203
 
225
204
  protected
226
205
 
227
- # Internal: Calculates the cache_key used to cache one serialized item.
228
- def item_cache_key(item, cache_key_proc)
229
- ActiveSupport::Cache.expand_cache_key(cache_key_proc.call(item))
230
- end
231
-
232
- # Public: Allows to define a cache key strategy for the serializer.
233
- # Defaults to calling cache_key in the object if no key is provided.
234
- #
235
- # NOTE: Benchmark it, sometimes caching is actually SLOWER.
236
- def cached(cache_key_proc = :cache_key.to_proc)
237
- cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
238
-
239
- # Internal: Redefine `write_one` to use the cache for the serialized JSON.
240
- define_singleton_method(:write_one) do |external_writer, item, options = nil|
241
- cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
242
- writer = new_json_writer
243
- non_cached_write_one(writer, item, options)
244
- writer.to_json
245
- end
246
- external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
247
- end
248
-
249
- # Internal: Redefine `write_many` to use fetch_multi from cache.
250
- define_singleton_method(:write_many) do |external_writer, items, options = nil|
251
- # We define a one-off method for the class to receive the entire object
252
- # inside the `fetch_multi` block. Otherwise we would only get the cache
253
- # key, and we would need to build a Hash to retrieve the object.
254
- #
255
- # NOTE: The assignment is important, as queries would return different
256
- # objects when expanding with the splat in fetch_multi.
257
- items = items.entries.each do |item|
258
- item_key = item_cache_key(item, cache_key_proc)
259
- item.define_singleton_method(:cache_key) { item_key }
260
- end
261
-
262
- # Fetch all items at once by leveraging `read_multi`.
263
- #
264
- # NOTE: Memcached does not support `write_multi`, if we switch the cache
265
- # store to use Redis performance would improve a lot for this case.
266
- cached_items = CACHE.fetch_multi(*items, cache_options) do |item|
267
- writer = new_json_writer
268
- non_cached_write_one(writer, item, options)
269
- writer.to_json
270
- end.values
271
- external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
206
+ def define_serialization_shortcuts(format)
207
+ case format
208
+ when :json, :hash
209
+ singleton_class.alias_method :one, :"one_as_#{format}"
210
+ singleton_class.alias_method :many, :"many_as_#{format}"
211
+ else
212
+ raise ArgumentError, "Unknown serialization format: #{format.inspect}"
272
213
  end
273
214
  end
274
- alias cached_with_key cached
275
215
 
276
216
  # Internal: The writer to use to write to json
277
217
  def new_json_writer
278
218
  Oj::StringWriter.new(mode: :rails)
279
219
  end
280
220
 
221
+ # Public: Identifiers are always serialized first.
222
+ def identifier(name = :id, **options)
223
+ add_attribute(name, **options, attribute: :method, identifier: true, if: -> { !@object.new_record? })
224
+ end
225
+
281
226
  # Public: Specify a collection of objects that should be serialized using
282
227
  # 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)
228
+ def has_many(name, serializer:, root: name, as: root, **options)
229
+ add_attribute(name, association: :many, as: as, serializer: serializer, **options)
285
230
  end
286
231
 
287
232
  # 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)
233
+ def has_one(name, serializer:, root: name, as: root, **options)
234
+ add_attribute(name, association: :one, as: as, serializer: serializer, **options)
290
235
  end
291
236
 
292
237
  # Public: Specify an object that should be serialized using the serializer,
293
238
  # but unlike `has_one`, this one will write the attributes directly without
294
239
  # 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)
240
+ def flat_one(name, serializer:, **options)
241
+ add_attribute(name, association: :flat, serializer: serializer, **options)
297
242
  end
298
243
 
299
244
  # Public: Specify which attributes are going to be obtained from indexing
300
245
  # the object.
301
246
  def hash_attributes(*method_names, **options)
302
- options = { **options, strategy: :write_value_using_hash_strategy }
247
+ options = { **options, attribute: :hash }
303
248
  method_names.each { |name| _attributes[name] = options }
304
249
  end
305
250
 
@@ -310,32 +255,49 @@ private
310
255
  #
311
256
  # See ./benchmarks/document_benchmark.rb
312
257
  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)
258
+ add_attribute('id', **options, attribute: :id, identifier: true) if method_names.delete(:id)
259
+ add_attributes(method_names, **options, attribute: :mongoid)
315
260
  end
316
261
 
317
262
  # Public: Specify which attributes are going to be obtained by calling a
318
263
  # method in the object.
319
264
  def attributes(*method_names, **options)
320
- add_attributes(method_names, **options, strategy: :write_value_using_method_strategy)
265
+ add_attributes(method_names, **options, attribute: :method)
321
266
  end
267
+ alias_method :attribute, :attributes
322
268
 
323
269
  # Public: Specify which attributes are going to be obtained by calling a
324
270
  # method in the serializer.
325
271
  #
326
272
  # NOTE: This can be one of the slowest strategies, when in doubt, measure.
327
273
  def serializer_attributes(*method_names, **options)
328
- add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
274
+ add_attributes(method_names, **options, attribute: :serializer)
329
275
  end
330
276
 
331
277
  # Syntax Sugar: Allows to use it before a method name.
332
278
  #
333
279
  # Example:
334
- # attribute \
280
+ # serialize
335
281
  # def full_name
336
282
  # "#{ first_name } #{ last_name }"
337
283
  # end
338
- alias attribute serializer_attributes
284
+ def serialize(name = nil, **options)
285
+ if name
286
+ serializer_attributes(name, **options)
287
+ else
288
+ @_current_attribute = options
289
+ end
290
+ end
291
+
292
+ # Internal: Intercept a method definition, tying a type that was
293
+ # previously specified to the name of the attribute.
294
+ def method_added(name)
295
+ super(name)
296
+ if @_current_attribute
297
+ serializer_attributes(name, **@_current_attribute)
298
+ @_current_attribute = nil
299
+ end
300
+ end
339
301
 
340
302
  # Backwards Compatibility: Meant only to replace Active Model Serializers,
341
303
  # calling a method in the serializer, or using `read_attribute_for_serialization`.
@@ -345,7 +307,23 @@ private
345
307
  method_names.each do |method_name|
346
308
  define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
347
309
  end
348
- add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
310
+ add_attributes(method_names, **options, attribute: :serializer)
311
+ end
312
+
313
+ # Internal: The strategy to use when sorting the fields.
314
+ #
315
+ # This setting is inherited from parent classes.
316
+ def _sort_attributes_by
317
+ @_sort_attributes_by = superclass.try(:_sort_attributes_by) unless defined?(@_sort_attributes_by)
318
+ @_sort_attributes_by
319
+ end
320
+
321
+ # Internal: The converter to use for serializer keys.
322
+ #
323
+ # This setting is inherited from parent classes.
324
+ def _transform_keys
325
+ @_transform_keys = superclass.try(:_transform_keys) unless defined?(@_transform_keys)
326
+ @_transform_keys
349
327
  end
350
328
 
351
329
  private
@@ -358,8 +336,17 @@ private
358
336
  _attributes[name.to_s.freeze] = options
359
337
  end
360
338
 
361
- def add_association(name, options)
362
- _associations[name.to_s.freeze] = options
339
+ # Internal: Transforms the keys using the provided strategy.
340
+ def key_for(method_name, options)
341
+ key = options.fetch(:as, method_name)
342
+ _transform_keys ? _transform_keys.call(key) : key
343
+ end
344
+
345
+ # Internal: Whether the object should be serialized as a collection.
346
+ def many?(item)
347
+ item.is_a?(Array) ||
348
+ (defined?(ActiveRecord::Relation) && item.is_a?(ActiveRecord::Relation)) ||
349
+ (defined?(Mongoid::Association::Many) && item.is_a?(Mongoid::Association::Many))
363
350
  end
364
351
 
365
352
  # Internal: We generate code for the serializer to avoid the overhead of
@@ -368,33 +355,78 @@ private
368
355
  #
369
356
  # As a result, the performance is the same as writing the most efficient
370
357
  # code by hand.
371
- def write_to_json_body
358
+ def code_to_write_to_json
372
359
  <<~WRITE_TO_JSON
373
360
  # 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
380
- }
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)
361
+ def write_to_json(writer, item, options = nil)
362
+ @object = item
363
+ @options = options
364
+ @memo.clear if defined?(@memo)
365
+ #{ _attributes.map { |method_name, options|
366
+ code_to_write_conditional(method_name, options) {
367
+ if options[:association]
368
+ code_to_write_association(method_name, options)
369
+ else
370
+ code_to_write_attribute(method_name, options)
371
+ end
385
372
  }
386
- }.join}
373
+ }.join("\n ") }#{code_to_rescue_no_method if DEV_MODE}
387
374
  end
388
375
  WRITE_TO_JSON
389
376
  end
390
377
 
378
+ # Internal: We generate code for the serializer to avoid the overhead of
379
+ # using variables for method names, having to iterate the list of attributes
380
+ # and associations, and the overhead of using `send` with dynamic methods.
381
+ #
382
+ # As a result, the performance is the same as writing the most efficient
383
+ # code by hand.
384
+ def code_to_render_as_hash
385
+ <<~RENDER_AS_HASH
386
+ # Public: Writes this serializer content to a Hash.
387
+ def render_as_hash(item, options = nil)
388
+ @object = item
389
+ @options = options
390
+ @memo.clear if defined?(@memo)
391
+ {
392
+ #{_attributes.map { |method_name, options|
393
+ code_to_render_conditionally(method_name, options) {
394
+ if options[:association]
395
+ code_to_render_association(method_name, options)
396
+ else
397
+ code_to_render_attribute(method_name, options)
398
+ end
399
+ }
400
+ }.join(",\n ")}
401
+ }#{code_to_rescue_no_method if DEV_MODE}
402
+ end
403
+ RENDER_AS_HASH
404
+ end
405
+
406
+ def code_to_rescue_no_method
407
+ <<~RESCUE_NO_METHOD
408
+
409
+ rescue NoMethodError => e
410
+ key = e.name.to_s.inspect
411
+ message = if respond_to?(e.name)
412
+ raise e, "Perhaps you meant to call \#{key} in \#{self.class} instead?\nTry using `serializer_attributes :\#{key}` or `attribute def \#{key}`.\n\#{e.message}"
413
+ elsif @object.respond_to?(e.name)
414
+ raise e, "Perhaps you meant to call \#{key} in \#{@object.class} instead?\nTry using `attributes :\#{key}`.\n\#{e.message}"
415
+ else
416
+ raise e
417
+ end
418
+ ensure
419
+ _check_instance_variables
420
+ RESCUE_NO_METHOD
421
+ end
422
+
391
423
  # Internal: Returns the code to render an attribute or association
392
424
  # conditionally.
393
425
  #
394
426
  # NOTE: Detects any include methods defined in the serializer, or defines
395
427
  # 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}?"
428
+ def code_to_write_conditional(method_name, options)
429
+ include_method_name = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
398
430
  if render_if = options[:if]
399
431
  define_method(include_method_name, &render_if)
400
432
  end
@@ -407,31 +439,116 @@ private
407
439
  end
408
440
 
409
441
  # Internal: Returns the code for the association method.
410
- def write_association_body(method_name, association_options)
442
+ def code_to_write_attribute(method_name, options)
443
+ key = key_for(method_name, options).to_s.inspect
444
+
445
+ case strategy = options.fetch(:attribute)
446
+ when :serializer
447
+ # Obtains the value by calling a method in the serializer.
448
+ "writer.push_value(#{method_name}, #{key})"
449
+ when :method
450
+ # Obtains the value by calling a method in the object, and writes it.
451
+ "writer.push_value(@object.#{method_name}, #{key})"
452
+ when :hash
453
+ # Writes a Hash value to JSON, works with String or Symbol keys.
454
+ "writer.push_value(@object[#{method_name.inspect}], #{key})"
455
+ when :mongoid
456
+ # Writes an Mongoid attribute to JSON, this is the fastest strategy.
457
+ "writer.push_value(@object.attributes['#{method_name}'], #{key})"
458
+ when :id
459
+ # Writes an _id value to JSON using `id` as the key instead.
460
+ #
461
+ # NOTE: We skip the id for non-persisted documents, since it doesn't actually
462
+ # identify the document (it will change once it's persisted).
463
+ "writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?"
464
+ else
465
+ raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
466
+ end
467
+ end
468
+
469
+ # Internal: Returns the code for the association method.
470
+ def code_to_write_association(method_name, options)
411
471
  # Use a serializer method if defined, else call the association in the object.
412
472
  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
473
+ key = key_for(method_name, options)
474
+ serializer_class = options.fetch(:serializer)
475
+
476
+ case type = options.fetch(:association)
477
+ when :one
478
+ <<~WRITE_ONE
479
+ if associated_object = #{association_method}
480
+ writer.push_key('#{key}')
481
+ #{serializer_class}.write_one(writer, associated_object)
482
+ end
423
483
  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})
484
+ when :many
485
+ <<~WRITE_MANY
486
+ writer.push_key('#{key}')
487
+ #{serializer_class}.write_many(writer, #{association_method})
428
488
  WRITE_MANY
429
- when :write_flat
430
- <<-WRITE_FLAT
431
- #{serializer_class}.write_flat(writer, #{association_method})
489
+ when :flat
490
+ <<~WRITE_FLAT
491
+ #{serializer_class}.write_to_json(writer, #{association_method})
432
492
  WRITE_FLAT
433
493
  else
434
- raise ArgumentError, "Unknown write_method #{write_method}"
494
+ raise ArgumentError, "Unknown association type: #{type.inspect}"
495
+ end
496
+ end
497
+
498
+ # Internal: Returns the code to render an attribute or association
499
+ # conditionally.
500
+ #
501
+ # NOTE: Detects any include methods defined in the serializer, or defines
502
+ # one by using the lambda passed in the `if` option, if any.
503
+ def code_to_render_conditionally(method_name, options)
504
+ include_method_name = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
505
+
506
+ if render_if = options[:if]
507
+ define_method(include_method_name, &render_if)
508
+ end
509
+
510
+ if method_defined?(include_method_name)
511
+ "**(#{include_method_name} ? {#{yield}} : {})"
512
+ else
513
+ yield
514
+ end
515
+ end
516
+
517
+ # Internal: Returns the code for the attribute method.
518
+ def code_to_render_attribute(method_name, options)
519
+ key = key_for(method_name, options)
520
+ case strategy = options.fetch(:attribute)
521
+ when :serializer
522
+ "#{key}: #{method_name}"
523
+ when :method
524
+ "#{key}: @object.#{method_name}"
525
+ when :hash
526
+ "#{key}: @object[#{method_name.inspect}]"
527
+ when :mongoid
528
+ "#{key}: @object.attributes['#{method_name}']"
529
+ when :id
530
+ "**(@object.new_record? ? {} : {id: @object.attributes['_id']})"
531
+ else
532
+ raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
533
+ end
534
+ end
535
+
536
+ # Internal: Returns the code for the association method.
537
+ def code_to_render_association(method_name, options)
538
+ # Use a serializer method if defined, else call the association in the object.
539
+ association = method_defined?(method_name) ? method_name : "@object.#{method_name}"
540
+ key = key_for(method_name, options)
541
+ serializer_class = options.fetch(:serializer)
542
+
543
+ case type = options.fetch(:association)
544
+ when :one
545
+ "#{key}: (one_item = #{association}) ? #{serializer_class}.one_as_hash(one_item) : nil"
546
+ when :many
547
+ "#{key}: #{serializer_class}.many_as_hash(#{association})"
548
+ when :flat
549
+ "**#{serializer_class}.one_as_hash(#{association})"
550
+ else
551
+ raise ArgumentError, "Unknown association type: #{type.inspect}"
435
552
  end
436
553
  end
437
554
 
@@ -447,16 +564,28 @@ private
447
564
 
448
565
  # Internal: Cache key to set a thread-local instance.
449
566
  def instance_key
450
- unless defined?(@instance_key)
451
- @instance_key = "#{name.underscore}_instance_#{object_id}".to_sym
567
+ @instance_key ||= begin
452
568
  # 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)
569
+ # before instantiating a serializer, to apply last minute adjustments.
570
+ _prepare_serializer
571
+ "#{name.underscore}_instance_#{object_id}".to_sym
456
572
  end
457
- @instance_key
573
+ end
574
+
575
+ # Internal: Generates write_to_json and render_as_hash methods optimized for
576
+ # the specified configuration.
577
+ def _prepare_serializer
578
+ if _sort_attributes_by
579
+ @_attributes = _attributes.sort_by { |key, options|
580
+ _sort_attributes_by.call(key, options)
581
+ }.to_h
582
+ end
583
+ class_eval(code_to_write_to_json)
584
+ class_eval(code_to_render_as_hash)
458
585
  end
459
586
  end
587
+
588
+ define_serialization_shortcuts(:hash)
460
589
  end
461
590
 
462
591
  Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OjSerializers
4
- VERSION = '1.0.2'
4
+ VERSION = '2.0.0-beta.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oj_serializers
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.0.0.pre.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maximo Mussini
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-01 00:00:00.000000000 Z
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj
@@ -16,42 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 3.8.0
19
+ version: 3.14.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 3.8.0
27
- - !ruby/object:Gem::Dependency
28
- name: actionpack
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '4.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '4.0'
41
- - !ruby/object:Gem::Dependency
42
- name: railties
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '4.0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '4.0'
26
+ version: 3.14.0
55
27
  description: oj_serializers leverages the performance of the oj JSON serialization
56
28
  library, and minimizes object allocations, all while provding a similar API to Active
57
29
  Model Serializers.
@@ -80,6 +52,7 @@ metadata:
80
52
  homepage_uri: https://github.com/ElMassimo/oj_serializers
81
53
  source_code_uri: https://github.com/ElMassimo/oj_serializers
82
54
  changelog_uri: https://github.com/ElMassimo/oj_serializers/blob/master/CHANGELOG.md
55
+ rubygems_mfa_required: 'true'
83
56
  post_install_message:
84
57
  rdoc_options: []
85
58
  require_paths:
@@ -88,12 +61,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
61
  requirements:
89
62
  - - ">="
90
63
  - !ruby/object:Gem::Version
91
- version: 2.3.0
64
+ version: 2.7.0
92
65
  required_rubygems_version: !ruby/object:Gem::Requirement
93
66
  requirements:
94
- - - ">="
67
+ - - ">"
95
68
  - !ruby/object:Gem::Version
96
- version: '0'
69
+ version: 1.3.1
97
70
  requirements: []
98
71
  rubygems_version: 3.2.32
99
72
  signing_key: