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 +4 -4
- data/README.md +169 -86
- data/lib/oj_serializers/compat.rb +4 -19
- data/lib/oj_serializers/controller_serialization.rb +2 -2
- data/lib/oj_serializers/json_string_encoder.rb +12 -28
- data/lib/oj_serializers/serializer.rb +322 -193
- data/lib/oj_serializers/version.rb +1 -1
- metadata +8 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4ebc24ba352b46585589ae760b579009e18e9de772b1d015eb0fdfa465a39b93
|
4
|
+
data.tar.gz: 475283ebe5897716eb1812a474358ddc3775746e1e0f8ff011204fc3ff76219e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
63
|
+
attributes should be serialized.
|
62
64
|
|
63
65
|
```ruby
|
64
66
|
class AlbumSerializer < Oj::Serializer
|
65
67
|
attributes :name, :genres
|
66
68
|
|
67
|
-
|
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
|
-
```
|
81
|
+
```ruby
|
80
82
|
{
|
81
|
-
|
82
|
-
|
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
|
-
|
89
|
-
|
90
|
+
release: "September 23, 1970",
|
91
|
+
songs: [
|
90
92
|
{
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
"Michael Carabello"
|
95
|
-
]
|
93
|
+
track: 1,
|
94
|
+
name: "Sing Winds, Crying Beasts",
|
95
|
+
composers: ["Michael Carabello"],
|
96
96
|
},
|
97
97
|
{
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
"Tito Puente"
|
110
|
-
]
|
103
|
+
track: 3,
|
104
|
+
name: "Oye como va",
|
105
|
+
composers: ["Tito Puente"],
|
111
106
|
},
|
112
107
|
{
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
"José Areas"
|
125
|
-
]
|
113
|
+
track: 5,
|
114
|
+
name: "Se acabó",
|
115
|
+
composers: ["José Areas"],
|
126
116
|
},
|
127
117
|
{
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
"Gregg Rolie"
|
132
|
-
]
|
118
|
+
track: 6,
|
119
|
+
name: "Mother's Daughter",
|
120
|
+
composers: ["Gregg Rolie"],
|
133
121
|
},
|
134
122
|
{
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
"Santana"
|
139
|
-
]
|
123
|
+
track: 7,
|
124
|
+
name: "Samba pa ti",
|
125
|
+
composers: ["Santana"],
|
140
126
|
},
|
141
127
|
{
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
"Rolie"
|
146
|
-
]
|
128
|
+
track: 8,
|
129
|
+
name: "Hope You're Feeling Better",
|
130
|
+
composers: ["Rolie"],
|
147
131
|
},
|
148
132
|
{
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
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`](
|
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,
|
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
|
306
|
+
The associations DSL is essentially a shortcut for defining attributes manually:
|
325
307
|
|
326
308
|
```ruby
|
327
309
|
class SongSerializer < SongMetadataSerializer
|
328
|
-
|
310
|
+
serialize
|
329
311
|
def album
|
330
312
|
AlbumSerializer.one(song.album)
|
331
313
|
end
|
332
314
|
|
333
|
-
|
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
|
-
|
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
|
-
###
|
395
|
+
### Writing directly to JSON
|
396
|
+
|
397
|
+
In some corner cases it might be faster to serialize using a `Oj::StringWriter`.
|
400
398
|
|
401
|
-
|
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
|
405
|
-
|
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
|
-
|
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
|
-
|
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
|
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]
|
429
|
-
a `class` instead of an `instance` of a class.
|
430
|
-
|
431
|
-
|
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
|
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
|
-
|
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
|
-
|
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,
|
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, **
|
18
|
-
|
19
|
-
|
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.
|
21
|
+
each_serializer.many(object, options)
|
30
22
|
elsif object.is_a?(String)
|
31
|
-
|
32
|
-
|
33
|
-
writer.push_json(object)
|
23
|
+
OjSerializers::JsonValue.new(object)
|
34
24
|
else
|
35
|
-
|
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, **
|
47
|
-
if serializer && serializer < OjSerializers::Serializer
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
@
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
-
|
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, :
|
126
|
+
delegate :write_one, :write_many, :write_to_json, to: :instance
|
160
127
|
|
161
|
-
#
|
162
|
-
|
163
|
-
|
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
|
-
#
|
166
|
-
|
167
|
-
|
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
|
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
|
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:
|
199
|
-
|
200
|
-
|
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
|
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
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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,
|
284
|
-
|
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,
|
289
|
-
|
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,
|
296
|
-
|
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,
|
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,
|
314
|
-
add_attributes(method_names, **options,
|
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,
|
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,
|
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
|
-
#
|
280
|
+
# serialize
|
335
281
|
# def full_name
|
336
282
|
# "#{ first_name } #{ last_name }"
|
337
283
|
# end
|
338
|
-
|
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,
|
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
|
-
|
362
|
-
|
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
|
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
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
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
|
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
|
-
|
414
|
-
serializer_class =
|
415
|
-
|
416
|
-
case
|
417
|
-
when :
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
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 :
|
425
|
-
|
426
|
-
|
427
|
-
|
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 :
|
430
|
-
|
431
|
-
|
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
|
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
|
-
|
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
|
454
|
-
|
455
|
-
|
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
|
-
|
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)
|
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:
|
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-
|
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.
|
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.
|
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.
|
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:
|
69
|
+
version: 1.3.1
|
97
70
|
requirements: []
|
98
71
|
rubygems_version: 3.2.32
|
99
72
|
signing_key:
|