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 +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:
|