oj_serializers 1.0.2 → 2.0.0.pre.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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: