oj_serializers 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/README.md +465 -0
- data/lib/oj_serializers.rb +6 -0
- data/lib/oj_serializers/compat.rb +29 -0
- data/lib/oj_serializers/controller_serialization.rb +32 -0
- data/lib/oj_serializers/json_string_encoder.rb +57 -0
- data/lib/oj_serializers/json_value.rb +31 -0
- data/lib/oj_serializers/memo.rb +16 -0
- data/lib/oj_serializers/serializer.rb +465 -0
- data/lib/oj_serializers/setup.rb +28 -0
- data/lib/oj_serializers/sugar.rb +17 -0
- data/lib/oj_serializers/version.rb +5 -0
- metadata +103 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 807a53bcbe017c0279d9be8f654044a0c8834d289dd2e34646121833923ab4c1
|
4
|
+
data.tar.gz: b5f77a958e54273c9729e42a8d9270d4859f1c3aaa338efcbdff0357b3bdc8a5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0753030fff469265ce72aecab4b8f7289d626bc15355f847dd0c57dccda3c3abbaa269e4154017bf12c9e1e3f0335df02bf62e31faf105f4cd2bd2a40c34b4ea
|
7
|
+
data.tar.gz: ad791006f8d82f9a1bba0ce8179d414372d2581fd2181920e9362fdad77b0e707f271a8c9a259e115deedcea9a576dca434b2332aef060502b8b530c69f25c3d
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,465 @@
|
|
1
|
+
<h1 align="center">
|
2
|
+
Oj Serializers
|
3
|
+
<p align="center">
|
4
|
+
<!-- <a href="https://travis-ci.org/ElMassimo/oj_serializers"><img alt="Build Status" src="https://travis-ci.org/ElMassimo/oj_serializers.svg"/></a>
|
5
|
+
<a href="http://inch-ci.org/github/ElMassimo/oj_serializers"><img alt="Inline docs" src="http://inch-ci.org/github/ElMassimo/oj_serializers.svg"/></a>
|
6
|
+
<a href="https://codeclimate.com/github/ElMassimo/oj_serializers"><img alt="Maintainability" src="https://codeclimate.com/github/ElMassimo/oj_serializers/badges/gpa.svg"/></a>
|
7
|
+
<a href="https://codeclimate.com/github/ElMassimo/oj_serializers"><img alt="Test Coverage" src="https://codeclimate.com/github/ElMassimo/oj_serializers/badges/coverage.svg"/></a> -->
|
8
|
+
<a href="https://rubygems.org/gems/oj_serializers"><img alt="Gem Version" src="https://img.shields.io/gem/v/oj_serializers.svg?colorB=e9573f"/></a>
|
9
|
+
<a href="https://github.com/ElMassimo/oj_serializers/blob/master/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
|
10
|
+
</p>
|
11
|
+
</h1>
|
12
|
+
|
13
|
+
JSON serializers for Ruby, built on top of the powerful [`oj`][oj] library.
|
14
|
+
|
15
|
+
[oj]: https://github.com/ohler55/oj
|
16
|
+
[mongoid]: https://github.com/mongodb/mongoid
|
17
|
+
[ams]: https://github.com/rails-api/active_model_serializers
|
18
|
+
[jsonapi]: https://github.com/jsonapi-serializer/jsonapi-serializer
|
19
|
+
[panko]: https://github.com/panko-serializer/panko_serializer
|
20
|
+
[benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
|
21
|
+
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/master/benchmarks/document_benchmark.rb
|
22
|
+
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/master/MIGRATION_GUIDE.md
|
23
|
+
[design]: https://github.com/ElMassimo/oj_serializers#design-
|
24
|
+
[raw_json]: https://github.com/ohler55/oj/issues/542
|
25
|
+
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
|
26
|
+
|
27
|
+
## Why? 🤔
|
28
|
+
|
29
|
+
[`ActiveModel::Serializer`][ams] has a nice DSL, but it allocates many objects leading
|
30
|
+
to memory bloat, time spent on GC, and lower performance.
|
31
|
+
|
32
|
+
`Oj::Serializer` provides a similar API, with [better performance][benchmarks].
|
33
|
+
|
34
|
+
Learn more about [how this library achieves its performance][design].
|
35
|
+
|
36
|
+
## Features ⚡️
|
37
|
+
|
38
|
+
- Declaration syntax similar to Active Model Serializers
|
39
|
+
- Reduced memory allocation and [improved performance][benchmarks]
|
40
|
+
- Support for `has_one` and `has_many`, compose with `flat_one`
|
41
|
+
- Useful development checks to avoid typos and mistakes
|
42
|
+
- Integrates nicely with Rails controllers
|
43
|
+
- Caching
|
44
|
+
|
45
|
+
## Installation 💿
|
46
|
+
|
47
|
+
Add this line to your application's Gemfile:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
gem 'oj_serializers'
|
51
|
+
```
|
52
|
+
|
53
|
+
And then run:
|
54
|
+
|
55
|
+
$ bundle install
|
56
|
+
|
57
|
+
## Usage 🚀
|
58
|
+
|
59
|
+
You can define a serializer by subclassing `Oj::Serializer`, and specify which
|
60
|
+
attributes should be serialized to JSON.
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
class AlbumSerializer < Oj::Serializer
|
64
|
+
attributes :name, :genres
|
65
|
+
|
66
|
+
attribute \
|
67
|
+
def release
|
68
|
+
album.release_date.strftime('%B %d, %Y')
|
69
|
+
end
|
70
|
+
|
71
|
+
has_many :songs, serializer: SongSerializer
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
<details>
|
76
|
+
<summary>Example Output</summary>
|
77
|
+
|
78
|
+
```json
|
79
|
+
{
|
80
|
+
"name": "Abraxas",
|
81
|
+
"genres": [
|
82
|
+
"Pyschodelic Rock",
|
83
|
+
"Blues Rock",
|
84
|
+
"Jazz Fusion",
|
85
|
+
"Latin Rock"
|
86
|
+
],
|
87
|
+
"release": "September 23, 1970",
|
88
|
+
"songs": [
|
89
|
+
{
|
90
|
+
"track": 1,
|
91
|
+
"name": "Sing Winds, Crying Beasts",
|
92
|
+
"composers": [
|
93
|
+
"Michael Carabello"
|
94
|
+
]
|
95
|
+
},
|
96
|
+
{
|
97
|
+
"track": 2,
|
98
|
+
"name": "Black Magic Woman / Gypsy Queen",
|
99
|
+
"composers": [
|
100
|
+
"Peter Green",
|
101
|
+
"Gábor Szabó"
|
102
|
+
]
|
103
|
+
},
|
104
|
+
{
|
105
|
+
"track": 3,
|
106
|
+
"name": "Oye como va",
|
107
|
+
"composers": [
|
108
|
+
"Tito Puente"
|
109
|
+
]
|
110
|
+
},
|
111
|
+
{
|
112
|
+
"track": 4,
|
113
|
+
"name": "Incident at Neshabur",
|
114
|
+
"composers": [
|
115
|
+
"Alberto Gianquinto",
|
116
|
+
"Carlos Santana"
|
117
|
+
]
|
118
|
+
},
|
119
|
+
{
|
120
|
+
"track": 5,
|
121
|
+
"name": "Se acabó",
|
122
|
+
"composers": [
|
123
|
+
"José Areas"
|
124
|
+
]
|
125
|
+
},
|
126
|
+
{
|
127
|
+
"track": 6,
|
128
|
+
"name": "Mother's Daughter",
|
129
|
+
"composers": [
|
130
|
+
"Gregg Rolie"
|
131
|
+
]
|
132
|
+
},
|
133
|
+
{
|
134
|
+
"track": 7,
|
135
|
+
"name": "Samba pa ti",
|
136
|
+
"composers": [
|
137
|
+
"Santana"
|
138
|
+
]
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"track": 8,
|
142
|
+
"name": "Hope You're Feeling Better",
|
143
|
+
"composers": [
|
144
|
+
"Rolie"
|
145
|
+
]
|
146
|
+
},
|
147
|
+
{
|
148
|
+
"track": 9,
|
149
|
+
"name": "El Nicoya",
|
150
|
+
"composers": [
|
151
|
+
"Areas"
|
152
|
+
]
|
153
|
+
}
|
154
|
+
]
|
155
|
+
}
|
156
|
+
```
|
157
|
+
</details>
|
158
|
+
|
159
|
+
<br/>
|
160
|
+
|
161
|
+
To use the serializer, the recommended approach is:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class AlbumsController < ApplicationController
|
165
|
+
def show
|
166
|
+
album = Album.find(params[:id])
|
167
|
+
render json: AlbumSerializer.one(album)
|
168
|
+
end
|
169
|
+
|
170
|
+
def index
|
171
|
+
albums = Album.all
|
172
|
+
render json: { albums: AlbumSerializer.many(albums) }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
If you are using Rails you can also use something closer to Active Model Serializers by adding `sugar`:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
require 'oj_serializers/sugar'
|
181
|
+
|
182
|
+
class AlbumsController < ApplicationController
|
183
|
+
def show
|
184
|
+
album = Album.find(params[:id])
|
185
|
+
render json: album, serializer: AlbumSerializer
|
186
|
+
end
|
187
|
+
|
188
|
+
def index
|
189
|
+
albums = Album.all
|
190
|
+
render json: albums, each_serializer: AlbumSerializer, root: :albums
|
191
|
+
end
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
It's recommended to create your own `BaseSerializer` class in order to easily
|
196
|
+
add custom extensions, specially when migrating from `active_model_serializers`.
|
197
|
+
|
198
|
+
## Render DSL 🛠
|
199
|
+
|
200
|
+
In order to efficiently reuse the instances, serializers can't be instantiated directly. Use `one` and `many` to serialize objects or enumerables:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
render json: {
|
204
|
+
favorite_album: AlbumSerializer.one(album),
|
205
|
+
purchased_albums: AlbumSerializer.many(albums),
|
206
|
+
}
|
207
|
+
```
|
208
|
+
|
209
|
+
You can use these serializers inside arrays, hashes, or even inside `ActiveModel::Serializer` by using a method in the serializer.
|
210
|
+
|
211
|
+
Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
|
212
|
+
|
213
|
+
## Attributes DSL 🛠
|
214
|
+
|
215
|
+
Attributes methods can be used to define which model attributes should be serialized
|
216
|
+
to JSON. Each method provides a different strategy to obtain the values to serialize.
|
217
|
+
|
218
|
+
The internal design is simple and extensible, so creating new strategies requires very little code.
|
219
|
+
Please open an issue if you need help 😃
|
220
|
+
|
221
|
+
### `attributes`
|
222
|
+
|
223
|
+
Obtains the attribute value by calling a method in the object being serialized.
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class PlayerSerializer < Oj::Serializer
|
227
|
+
attributes :full_name
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
Have in mind that unlike Active Model Serializers, it will _not_ take into
|
232
|
+
account methods defined in the serializer. Being explicit about where the
|
233
|
+
attribute is coming from makes the serializers easier to understand and more
|
234
|
+
maintainable.
|
235
|
+
|
236
|
+
### `serializer_attributes`
|
237
|
+
|
238
|
+
Obtains the attribute value by calling a method defined in the serializer.
|
239
|
+
|
240
|
+
|
241
|
+
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:
|
242
|
+
|
243
|
+
```ruby
|
244
|
+
class PlayerSerializer < Oj::Serializer
|
245
|
+
attribute \
|
246
|
+
def full_name
|
247
|
+
"#{player.first_name} #{player.last_name}"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
Instance methods can access the object by the serializer name without the
|
253
|
+
`Serializer` suffix, `player` in the example above, or directly as `@object`.
|
254
|
+
|
255
|
+
You can customize this by using [`object_as`](https://github.com/ElMassimo/oj_serializers#using-a-different-alias-for-the-internal-object).
|
256
|
+
|
257
|
+
### `ams_attributes` 🐌
|
258
|
+
|
259
|
+
Works like `attributes` in Active Model Serializers, by calling a method in the serializer if defined, or calling `read_attribute_for_serialization` in the model.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class AlbumSerializer < Oj::Serializer
|
263
|
+
ams_attributes :name, :release
|
264
|
+
|
265
|
+
def release
|
266
|
+
album.release_date.strftime('%B %d, %Y')
|
267
|
+
end
|
268
|
+
end
|
269
|
+
```
|
270
|
+
|
271
|
+
Should only be used when migrating from Active Model Serializers, as it's slower and can create confusion.
|
272
|
+
|
273
|
+
Instead, use `attributes` for model methods, and the inline `attribute` for serializer attributes. Being explicit makes serializers easier to understand, and to maintain.
|
274
|
+
|
275
|
+
Please refer to the [migration guide] for more information.
|
276
|
+
|
277
|
+
### `hash_attributes` 🚀
|
278
|
+
|
279
|
+
Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
class PersonSerializer < Oj::Serializer
|
283
|
+
hash_attributes 'first_name', :last_name
|
284
|
+
end
|
285
|
+
|
286
|
+
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
287
|
+
# {"first_name":"Mary","last_name":"Watson"}
|
288
|
+
```
|
289
|
+
|
290
|
+
### `mongo_attributes` 🚀
|
291
|
+
|
292
|
+
Reads data directly from `attributes` in a [Mongoid] document.
|
293
|
+
|
294
|
+
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
295
|
+
|
296
|
+
Although there are some downsides, depending on how consistent your schema is,
|
297
|
+
and which kind of consumer the API has, it can be really powerful.
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
class AlbumSerializer < Oj::Serializer
|
301
|
+
mongo_attributes :id, :name
|
302
|
+
end
|
303
|
+
```
|
304
|
+
|
305
|
+
## Associations DSL 🛠
|
306
|
+
|
307
|
+
Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
|
308
|
+
|
309
|
+
The value for the association is obtained from a serializer method if defined, or by calling the method in the object being serialized.
|
310
|
+
|
311
|
+
You must specificy which serializer to use with the `serializer` option.
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
class SongSerializer < Oj::Serializer
|
315
|
+
has_one :album, serializer: AlbumSerializer
|
316
|
+
has_many :composers, serializer: ComposerSerializer
|
317
|
+
|
318
|
+
# You can also compose serializers using `flat_one`.
|
319
|
+
flat_one :song, serializer: SongMetadataSerializer
|
320
|
+
end
|
321
|
+
```
|
322
|
+
|
323
|
+
The associations DSL is more concise and achieves better performance, so prefer to use it instead of manually definining attributes:
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
class SongSerializer < SongMetadataSerializer
|
327
|
+
attribute \
|
328
|
+
def album
|
329
|
+
AlbumSerializer.one(song.album)
|
330
|
+
end
|
331
|
+
|
332
|
+
attribute \
|
333
|
+
def composers
|
334
|
+
ComposerSerializer.many(song.composers)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
```
|
338
|
+
|
339
|
+
## Other DSL 🛠
|
340
|
+
|
341
|
+
### Using a different alias for the internal object
|
342
|
+
|
343
|
+
You can use `object_as` to create an alias for the serialized object to access it from instance methods:
|
344
|
+
|
345
|
+
```ruby
|
346
|
+
class DiscographySerializer < Oj::Serializer
|
347
|
+
object_as :artist
|
348
|
+
|
349
|
+
# Now we can use `artist` instead of `object` or `discography`.
|
350
|
+
def latest_albums
|
351
|
+
artist.albums.desc(:year)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
### Rendering an attribute conditionally
|
357
|
+
|
358
|
+
All the attributes and association methods can take an `if` option to render conditionally.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
class AlbumSerializer < Oj::Serializer
|
362
|
+
mongo_attributes :release_date, if: -> { album.released? }
|
363
|
+
|
364
|
+
has_many :songs, serializer: SongSerializer, if: -> { album.songs.any? }
|
365
|
+
|
366
|
+
# You can achieve the same by manually defining a method:
|
367
|
+
def include_songs?
|
368
|
+
album.songs.any?
|
369
|
+
end
|
370
|
+
end
|
371
|
+
```
|
372
|
+
|
373
|
+
### Memoization & Local State
|
374
|
+
|
375
|
+
Serializers are designed to be stateless so that an instanced can be reused, but sometimes it's convenient to store intermediate calculations.
|
376
|
+
|
377
|
+
Use `memo` for memoization and storing temporary information.
|
378
|
+
|
379
|
+
```ruby
|
380
|
+
class DownloadSerializer < Oj::Serializer
|
381
|
+
attributes :filename, :size
|
382
|
+
|
383
|
+
attribute \
|
384
|
+
def progress
|
385
|
+
"#{ last_event&.progress || 0 }%"
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
|
390
|
+
def last_event
|
391
|
+
memo.fetch(:last_event) {
|
392
|
+
download.events.desc(:created_at).first
|
393
|
+
}
|
394
|
+
end
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
### Caching 📦
|
399
|
+
|
400
|
+
Use `cached` to leverage key-based caching, which calls `cache_key` in the object. You can also provide a lambda to `cached_with_key` to define a custom key:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
class CachedUserSerializer < UserSerializer
|
404
|
+
cached_with_key ->(user) {
|
405
|
+
"#{ user.id }/#{ user.current_sign_in_at }"
|
406
|
+
}
|
407
|
+
end
|
408
|
+
```
|
409
|
+
|
410
|
+
It will leverage `fetch_multi` when serializing a collection with `many` or `has_many`, to minimize the amount of round trips needed to read and write all items to cache. This works specially well if your cache store also supports `write_multi`.
|
411
|
+
|
412
|
+
Usually serialization happens so fast that __turning caching on can be slower__. Always benchmark to make sure it's worth it, and use caching only for time-consuming or deeply nested structures.
|
413
|
+
|
414
|
+
## Design 📐
|
415
|
+
|
416
|
+
Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
|
417
|
+
JSON, this implementation uses `Oj::StringWriter` to write JSON directly,
|
418
|
+
greatly reducing the overhead of allocating and garbage collecting the hashes.
|
419
|
+
|
420
|
+
It also allocates a single instance per serializer class, which makes it easy
|
421
|
+
to use, while keeping memory usage under control.
|
422
|
+
|
423
|
+
### Comparison with other libraries
|
424
|
+
|
425
|
+
`ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
|
426
|
+
|
427
|
+
Other libraries such as [`jsonapi-serializer`][jsonapi] evaluate serializers in the context of
|
428
|
+
a `class` instead of an `instance` of a class. Although it is efficient in terms
|
429
|
+
of memory usage, the downside is that you can't use instance methods or local
|
430
|
+
memoization, and any mixins must be applied to the class itself.
|
431
|
+
|
432
|
+
[`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.
|
433
|
+
|
434
|
+
`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.
|
435
|
+
|
436
|
+
As a result, migrating from `active_model_serializers` is relatively straightforward because instance methods, inheritance, and mixins work as usual.
|
437
|
+
|
438
|
+
## Formatting 📏
|
439
|
+
|
440
|
+
Even though most of the examples above use a single-line style to be succint, I highly recommend writing one attribute per line, sorting them alphabetically (most editors can do it for you), and [always using a trailing comma][trailing_commas].
|
441
|
+
|
442
|
+
```ruby
|
443
|
+
class AlbumSerializer < Oj::Serializer
|
444
|
+
attributes(
|
445
|
+
:genres,
|
446
|
+
:name,
|
447
|
+
:release_date,
|
448
|
+
)
|
449
|
+
end
|
450
|
+
```
|
451
|
+
|
452
|
+
It will make things clearer, minimize the amount of git conflicts, and keep the history a lot cleaner and more meaningful when using `git blame`.
|
453
|
+
|
454
|
+
## Special Thanks 🙏
|
455
|
+
|
456
|
+
This library wouldn't be possible without the wonderful and performant [`oj`](https://github.com/ohler55/oj) library. Thanks [Peter](https://github.com/ohler55)! 😃
|
457
|
+
|
458
|
+
Also, thanks to the libraries that inspired this one:
|
459
|
+
|
460
|
+
- [`active_model_serializers`][ams]: For the DSL
|
461
|
+
- [`panko-serializer`][panko]: For validating that using `Oj::StringWriter` was indeed fast
|
462
|
+
|
463
|
+
## License
|
464
|
+
|
465
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model_serializers'
|
4
|
+
|
5
|
+
# Extensions: To ensure JsonStringEncoder can process ActiveModel::Serializer
|
6
|
+
# as well.
|
7
|
+
class ActiveModel::Serializer
|
8
|
+
# JsonStringEncoder: Used internally to write a single object to JSON.
|
9
|
+
def self.write_one(writer, object, options)
|
10
|
+
writer.push_value(new(object, options))
|
11
|
+
end
|
12
|
+
|
13
|
+
# JsonStringEncoder: Used internally to write an array of objects to JSON.
|
14
|
+
def self.write_many(writer, array, options)
|
15
|
+
writer.push_array
|
16
|
+
array.each do |object|
|
17
|
+
write_one(writer, object, options)
|
18
|
+
end
|
19
|
+
writer.pop
|
20
|
+
end
|
21
|
+
|
22
|
+
# JsonStringEncoder: Used internally to instantiate an Oj::StringWriter.
|
23
|
+
def self.new_json_writer
|
24
|
+
OjSerializers::Serializer.send(:new_json_writer)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'oj_serializers'
|
29
|
+
require 'oj_serializers/sugar'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oj_serializers/json_string_encoder'
|
4
|
+
|
5
|
+
# Internal: Allows to pass Oj serializers as options in `render`.
|
6
|
+
module OjSerializers::ControllerSerialization
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include ActionController::Renderers
|
9
|
+
|
10
|
+
# Internal: Allows to use Oj::Serializer as `serializer` and `each_serializer`
|
11
|
+
# as with ActiveModelSerializers.
|
12
|
+
#
|
13
|
+
# render json: items, each_serializer: ItemSerializer
|
14
|
+
# render json: item, serializer: ItemSerializer
|
15
|
+
#
|
16
|
+
# NOTE: In practice, it should be preferable to simply do:
|
17
|
+
#
|
18
|
+
# render json: ItemSerializer.many(items)
|
19
|
+
# render json: ItemSerializer.one(item)
|
20
|
+
#
|
21
|
+
# which is more performant.
|
22
|
+
%i[_render_option_json _render_with_renderer_json].each do |renderer_method|
|
23
|
+
define_method renderer_method do |resource, **options|
|
24
|
+
serializer_class = options[:serializer] || options[:each_serializer]
|
25
|
+
if serializer_class && serializer_class < OjSerializers::Serializer
|
26
|
+
super(OjSerializers::JsonStringEncoder.encode_to_json(resource, options), options.except(:root, :serializer, :each_serializer))
|
27
|
+
else
|
28
|
+
super(resource, **options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Public: Contains utility functions to render objects to JSON.
|
4
|
+
#
|
5
|
+
# Useful to instantiate a single `JsonWriter` when rendering new serializers.
|
6
|
+
module OjSerializers::JsonStringEncoder
|
7
|
+
class << self
|
8
|
+
# Public: Allows to use Oj::Serializer in `serializer` and `each_serializer`
|
9
|
+
# as with ActiveModelSerializers.
|
10
|
+
# render json: items, each_serializer: ItemSerializer
|
11
|
+
# render json: item, serializer: ItemSerializer
|
12
|
+
#
|
13
|
+
# Returns a JSON string.
|
14
|
+
#
|
15
|
+
# NOTE: Unlike the default encoder, this one will use the `root` option
|
16
|
+
# regardless of whether a serializer is specified or not.
|
17
|
+
def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **extras)
|
18
|
+
# NOTE: Serializers may override `new_json_writer` to modify the behavior.
|
19
|
+
writer = (serializer || each_serializer || OjSerializers::Serializer).send(:new_json_writer)
|
20
|
+
|
21
|
+
if root
|
22
|
+
writer.push_object
|
23
|
+
writer.push_key(root.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
if serializer
|
27
|
+
serializer.write_one(writer, object, extras)
|
28
|
+
elsif each_serializer
|
29
|
+
each_serializer.write_many(writer, object, extras)
|
30
|
+
elsif object.is_a?(String)
|
31
|
+
return object unless root
|
32
|
+
|
33
|
+
writer.push_json(object)
|
34
|
+
else
|
35
|
+
writer.push_value(object)
|
36
|
+
end
|
37
|
+
|
38
|
+
writer.pop if root
|
39
|
+
|
40
|
+
writer.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
# Allows to detect misusage of the options during development.
|
44
|
+
if OjSerializers::Serializer::DEV_MODE
|
45
|
+
alias actual_encode_to_json encode_to_json
|
46
|
+
def encode_to_json(object, root: nil, serializer: nil, each_serializer: nil, **extras)
|
47
|
+
if serializer && serializer < OjSerializers::Serializer
|
48
|
+
raise ArgumentError, 'You must use `each_serializer` when serializing collections' if object.respond_to?(:each)
|
49
|
+
end
|
50
|
+
if each_serializer && each_serializer < OjSerializers::Serializer
|
51
|
+
raise ArgumentError, 'You must use `serializer` when serializing a single object' unless object.respond_to?(:each)
|
52
|
+
end
|
53
|
+
actual_encode_to_json(object, root: root, serializer: serializer, each_serializer: each_serializer, **extras)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Public: Allows to prevent double encoding an existing JSON string.
|
4
|
+
#
|
5
|
+
# NOTE: Oj's raw_json option means there's no performance overhead, as it would
|
6
|
+
# occur with the previous alternative of parsing the JSON string.
|
7
|
+
class OjSerializers::JsonValue
|
8
|
+
# Helper: Expects an Array of JSON-encoded strings and wraps them in a JSON array.
|
9
|
+
def self.array(json_rows)
|
10
|
+
new("[#{json_rows.join(',')}]")
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(json)
|
14
|
+
@json = json
|
15
|
+
end
|
16
|
+
|
17
|
+
# Public: Return the internal json when using string interpolation.
|
18
|
+
def to_s
|
19
|
+
@json
|
20
|
+
end
|
21
|
+
|
22
|
+
# Internal: Used by Oj::Rails::Encoder because we use the `raw_json` option.
|
23
|
+
def raw_json(*)
|
24
|
+
@json
|
25
|
+
end
|
26
|
+
|
27
|
+
# Internal: Used by Oj::Rails::Encoder when found inside a Hash or Array.
|
28
|
+
def as_json(_options = nil)
|
29
|
+
self
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Internal: Provides a simple API on top of Hash for memoization purposes.
|
4
|
+
class OjSerializers::Memo
|
5
|
+
def initialize
|
6
|
+
@cache = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear
|
10
|
+
@cache.clear
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch(key)
|
14
|
+
@cache.fetch(key) { @cache[key] = yield }
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,465 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'active_support/core_ext/object/try'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
|
7
|
+
require 'oj'
|
8
|
+
require 'oj_serializers/memo'
|
9
|
+
require 'oj_serializers/json_value'
|
10
|
+
|
11
|
+
# Public: Implementation of an "ActiveModelSerializer"-like DSL, but with a
|
12
|
+
# design that allows replacing the internal object, which greatly reduces object
|
13
|
+
# allocation.
|
14
|
+
#
|
15
|
+
# Unlike ActiveModelSerializer, which builds a Hash which then gets encoded to
|
16
|
+
# JSON, this implementation allows to use Oj::StringWriter to write directly to
|
17
|
+
# JSON, greatly reducing the overhead of allocating and garbage collecting the
|
18
|
+
# hashes.
|
19
|
+
class OjSerializers::Serializer
|
20
|
+
# Public: Used to validate incorrect memoization during development. Users of
|
21
|
+
# this library might add additional options as needed.
|
22
|
+
ALLOWED_INSTANCE_VARIABLES = %w[memo object].freeze
|
23
|
+
|
24
|
+
CACHE = (defined?(Rails) && Rails.cache) ||
|
25
|
+
(defined?(ActiveSupport::Cache::MemoryStore) ? ActiveSupport::Cache::MemoryStore.new : OjSerializers::Memo.new)
|
26
|
+
|
27
|
+
# Internal: The environment the app is currently running on.
|
28
|
+
environment = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'production'
|
29
|
+
|
30
|
+
# Internal: Used to display warnings or detect misusage during development.
|
31
|
+
DEV_MODE = %w[test development].include?(environment) && !ENV['BENCHMARK']
|
32
|
+
|
33
|
+
DEFAULT_OPTIONS = {}.freeze
|
34
|
+
|
35
|
+
# Backwards Compatibility: Allows to access options passed through `render json`,
|
36
|
+
# in the same way than ActiveModel::Serializers.
|
37
|
+
def options
|
38
|
+
@object.try(:options) || DEFAULT_OPTIONS
|
39
|
+
end
|
40
|
+
|
41
|
+
# Internal: Used internally to write attributes and associations to JSON.
|
42
|
+
#
|
43
|
+
# NOTE: Binds this instance to the specified object and options and writes
|
44
|
+
# to json using the provided writer.
|
45
|
+
def write_flat(writer, item)
|
46
|
+
@memo.clear if defined?(@memo)
|
47
|
+
@object = item
|
48
|
+
write_to_json(writer)
|
49
|
+
end
|
50
|
+
|
51
|
+
# NOTE: Helps developers to remember to keep serializers stateless.
|
52
|
+
if DEV_MODE
|
53
|
+
prepend(Module.new do
|
54
|
+
def write_flat(writer, item)
|
55
|
+
if instance_values.keys.any? { |key| !ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
56
|
+
bad_keys = instance_values.keys.reject { |key| ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
57
|
+
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')}"
|
58
|
+
end
|
59
|
+
super
|
60
|
+
end
|
61
|
+
end)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Internal: Used internally to write a single object to JSON.
|
65
|
+
#
|
66
|
+
# writer - writer used to serialize results
|
67
|
+
# item - item to serialize results for
|
68
|
+
# options - list of external options to pass to the serializer (available as `options`)
|
69
|
+
#
|
70
|
+
# NOTE: Binds this instance to the specified object and options and writes
|
71
|
+
# to json using the provided writer.
|
72
|
+
def write_one(writer, item, options = nil)
|
73
|
+
item.define_singleton_method(:options) { options } if options
|
74
|
+
writer.push_object
|
75
|
+
write_flat(writer, item)
|
76
|
+
writer.pop
|
77
|
+
end
|
78
|
+
|
79
|
+
# Internal: Used internally to write an array of objects to JSON.
|
80
|
+
#
|
81
|
+
# writer - writer used to serialize results
|
82
|
+
# items - items to serialize results for
|
83
|
+
# options - list of external options to pass to the serializer (available as `options`)
|
84
|
+
def write_many(writer, items, options = nil)
|
85
|
+
writer.push_array
|
86
|
+
items.each do |item|
|
87
|
+
write_one(writer, item, options)
|
88
|
+
end
|
89
|
+
writer.pop
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
# Internal: An internal cache that can be used for temporary memoization.
|
95
|
+
def memo
|
96
|
+
defined?(@memo) ? @memo : @memo = OjSerializers::Memo.new
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Strategy: Writes an _id value to JSON using `id` as the key instead.
|
102
|
+
# NOTE: We skip the id for non-persisted documents, since it doesn't actually
|
103
|
+
# identify the document (it will change once it's persisted).
|
104
|
+
def write_value_using_id_strategy(writer, _key)
|
105
|
+
writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?
|
106
|
+
end
|
107
|
+
|
108
|
+
# Strategy: Writes an Mongoid attribute to JSON, this is the fastest strategy.
|
109
|
+
def write_value_using_mongoid_strategy(writer, key)
|
110
|
+
writer.push_value(@object.attributes[key], key)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Strategy: Writes a Hash value to JSON, works with String or Symbol keys.
|
114
|
+
def write_value_using_hash_strategy(writer, key)
|
115
|
+
writer.push_value(@object[key], key.to_s)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Strategy: Obtains the value by calling a method in the object, and writes it.
|
119
|
+
def write_value_using_method_strategy(writer, key)
|
120
|
+
writer.push_value(@object.send(key), key)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Strategy: Obtains the value by calling a method in the serializer.
|
124
|
+
def write_value_using_serializer_strategy(writer, key)
|
125
|
+
writer.push_value(send(key), key)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Override to detect missing attribute errors locally.
|
129
|
+
if DEV_MODE
|
130
|
+
alias original_write_value_using_method_strategy write_value_using_method_strategy
|
131
|
+
def write_value_using_method_strategy(writer, key)
|
132
|
+
original_write_value_using_method_strategy(writer, key)
|
133
|
+
rescue NoMethodError => e
|
134
|
+
raise e, "Perhaps you meant to call #{key.inspect} in #{self.class.name} instead?\nTry using `serializer_attributes :#{key}` or `attribute def #{key}`.\n#{e.message}"
|
135
|
+
end
|
136
|
+
|
137
|
+
alias original_write_value_using_mongoid_strategy write_value_using_mongoid_strategy
|
138
|
+
def write_value_using_mongoid_strategy(writer, key)
|
139
|
+
original_write_value_using_mongoid_strategy(writer, key).tap do
|
140
|
+
# Apply a fake selection when 'only' is not used, so that we allow
|
141
|
+
# read_attribute to fail on typos, renamed, and removed fields.
|
142
|
+
@object.__selected_fields = @object.fields.merge(@object.relations.select { |_key, value| value.embedded? }).transform_values { 1 } unless @object.__selected_fields
|
143
|
+
@object.read_attribute(key) # Raise a missing attribute exception if it's missing.
|
144
|
+
end
|
145
|
+
rescue StandardError => e
|
146
|
+
raise ActiveModel::MissingAttributeError, "#{e.message} in #{self.class} for #{@object.inspect}"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class << self
|
151
|
+
# Internal: We want to discourage instantiating serializers directly, as it
|
152
|
+
# prevents the possibility of reusing an instance.
|
153
|
+
#
|
154
|
+
# NOTE: `one` serves as a replacement for `new` in these serializers.
|
155
|
+
private :new
|
156
|
+
|
157
|
+
# Internal: Delegates to the instance methods, the advantage is that we can
|
158
|
+
# reuse the same serializer instance to serialize different objects.
|
159
|
+
delegate :write_one, :write_many, :write_flat, to: :instance
|
160
|
+
|
161
|
+
# Internal: Keep a reference to the default `write_one` method so that we
|
162
|
+
# can use it inside cached overrides and benchmark tests.
|
163
|
+
alias non_cached_write_one write_one
|
164
|
+
|
165
|
+
# Internal: Keep a reference to the default `write_many` method so that we
|
166
|
+
# can use it inside cached overrides and benchmark tests.
|
167
|
+
alias non_cached_write_many write_many
|
168
|
+
|
169
|
+
# Helper: Serializes the item unless it's nil.
|
170
|
+
def one_if(item, options = nil)
|
171
|
+
one(item, options) if item
|
172
|
+
end
|
173
|
+
|
174
|
+
# Public: Serializes the configured attributes for the specified object.
|
175
|
+
#
|
176
|
+
# item - the item to serialize
|
177
|
+
# options - list of external options to pass to the sub class (available in `item.options`)
|
178
|
+
#
|
179
|
+
# Returns an Oj::StringWriter instance, which is encoded as raw json.
|
180
|
+
def one(item, options = nil)
|
181
|
+
writer = new_json_writer
|
182
|
+
write_one(writer, item, options)
|
183
|
+
writer
|
184
|
+
end
|
185
|
+
|
186
|
+
# Public: Serializes an array of items using this serializer.
|
187
|
+
#
|
188
|
+
# items - Must respond to `each`.
|
189
|
+
# options - list of external options to pass to the sub class (available in `item.options`)
|
190
|
+
#
|
191
|
+
# Returns an Oj::StringWriter instance, which is encoded as raw json.
|
192
|
+
def many(items, options = nil)
|
193
|
+
writer = new_json_writer
|
194
|
+
write_many(writer, items, options)
|
195
|
+
writer
|
196
|
+
end
|
197
|
+
|
198
|
+
# Public: Creates an alias for the internal object.
|
199
|
+
def object_as(name)
|
200
|
+
define_method(name) { @object }
|
201
|
+
end
|
202
|
+
|
203
|
+
# Internal: Will alias the object according to the name of the wrapper class.
|
204
|
+
def inherited(subclass)
|
205
|
+
object_alias = subclass.name.demodulize.chomp('Serializer').underscore
|
206
|
+
subclass.object_as(object_alias) unless method_defined?(object_alias)
|
207
|
+
super
|
208
|
+
end
|
209
|
+
|
210
|
+
# Internal: List of attributes to be serialized.
|
211
|
+
#
|
212
|
+
# Any attributes defined in parent classes are inherited.
|
213
|
+
def _attributes
|
214
|
+
@_attributes = superclass.try(:_attributes)&.dup || {} unless defined?(@_attributes)
|
215
|
+
@_attributes
|
216
|
+
end
|
217
|
+
|
218
|
+
# Internal: List of associations to be serialized.
|
219
|
+
# Any associations defined in parent classes are inherited.
|
220
|
+
def _associations
|
221
|
+
@_associations = superclass.try(:_associations)&.dup || {} unless defined?(@_associations)
|
222
|
+
@_associations
|
223
|
+
end
|
224
|
+
|
225
|
+
# Internal: Iterating arrays is faster than iterating hashes.
|
226
|
+
attr_reader :_attributes_entries, :_associations_entries
|
227
|
+
|
228
|
+
protected
|
229
|
+
|
230
|
+
# Internal: Calculates the cache_key used to cache one serialized item.
|
231
|
+
def item_cache_key(item, cache_key_proc)
|
232
|
+
ActiveSupport::Cache.expand_cache_key(cache_key_proc.call(item))
|
233
|
+
end
|
234
|
+
|
235
|
+
# Public: Allows to define a cache key strategy for the serializer.
|
236
|
+
# Defaults to calling cache_key in the object if no key is provided.
|
237
|
+
#
|
238
|
+
# NOTE: Benchmark it, sometimes caching is actually SLOWER.
|
239
|
+
def cached(cache_key_proc = :cache_key.to_proc)
|
240
|
+
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
|
241
|
+
|
242
|
+
# Internal: Redefine `write_one` to use the cache for the serialized JSON.
|
243
|
+
define_singleton_method(:write_one) do |external_writer, item, options = nil|
|
244
|
+
cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
|
245
|
+
writer = new_json_writer
|
246
|
+
non_cached_write_one(writer, item, options)
|
247
|
+
writer.to_json
|
248
|
+
end
|
249
|
+
external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
|
250
|
+
end
|
251
|
+
|
252
|
+
# Internal: Redefine `write_many` to use fetch_multi from cache.
|
253
|
+
define_singleton_method(:write_many) do |external_writer, items, options = nil|
|
254
|
+
# We define a one-off method for the class to receive the entire object
|
255
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
256
|
+
# key, and we would need to build a Hash to retrieve the object.
|
257
|
+
#
|
258
|
+
# NOTE: The assignment is important, as queries would return different
|
259
|
+
# objects when expanding with the splat in fetch_multi.
|
260
|
+
items = items.entries.each do |item|
|
261
|
+
item_key = item_cache_key(item, cache_key_proc)
|
262
|
+
item.define_singleton_method(:cache_key) { item_key }
|
263
|
+
end
|
264
|
+
|
265
|
+
# Fetch all items at once by leveraging `read_multi`.
|
266
|
+
#
|
267
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
268
|
+
# store to use Redis performance would improve a lot for this case.
|
269
|
+
cached_items = CACHE.fetch_multi(*items, cache_options) do |item|
|
270
|
+
writer = new_json_writer
|
271
|
+
non_cached_write_one(writer, item, options)
|
272
|
+
writer.to_json
|
273
|
+
end.values
|
274
|
+
external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
|
275
|
+
end
|
276
|
+
end
|
277
|
+
alias cached_with_key cached
|
278
|
+
|
279
|
+
# Internal: The writer to use to write to json
|
280
|
+
def new_json_writer
|
281
|
+
Oj::StringWriter.new(mode: :rails)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Public: Specify a collection of objects that should be serialized using
|
285
|
+
# the specified serializer.
|
286
|
+
def has_many(name, root: name, serializer:, **options)
|
287
|
+
add_association(name, write_method: :write_many, root: root, serializer: serializer, **options)
|
288
|
+
end
|
289
|
+
|
290
|
+
# Public: Specify an object that should be serialized using the serializer.
|
291
|
+
def has_one(name, root: name, serializer:, **options)
|
292
|
+
add_association(name, write_method: :write_one, root: root, serializer: serializer, **options)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Public: Specify an object that should be serialized using the serializer,
|
296
|
+
# but unlike `has_one`, this one will write the attributes directly without
|
297
|
+
# wrapping it in an object.
|
298
|
+
def flat_one(name, root: false, serializer:, **options)
|
299
|
+
add_association(name, write_method: :write_flat, root: root, serializer: serializer, **options)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Public: Specify which attributes are going to be obtained from indexing
|
303
|
+
# the object.
|
304
|
+
def hash_attributes(*method_names, **options)
|
305
|
+
options = { **options, strategy: :write_value_using_hash_strategy }
|
306
|
+
method_names.each { |name| _attributes[name] = options }
|
307
|
+
end
|
308
|
+
|
309
|
+
# Public: Specify which attributes are going to be obtained from indexing
|
310
|
+
# a Mongoid model's `attributes` hash directly, for performance.
|
311
|
+
#
|
312
|
+
# Automatically renames `_id` to `id` for Mongoid models.
|
313
|
+
#
|
314
|
+
# See ./benchmarks/document_benchmark.rb
|
315
|
+
def mongo_attributes(*method_names, **options)
|
316
|
+
add_attribute('id', **options, strategy: :write_value_using_id_strategy) if method_names.delete(:id)
|
317
|
+
add_attributes(method_names, **options, strategy: :write_value_using_mongoid_strategy)
|
318
|
+
end
|
319
|
+
|
320
|
+
# Public: Specify which attributes are going to be obtained by calling a
|
321
|
+
# method in the object.
|
322
|
+
def attributes(*method_names, **options)
|
323
|
+
add_attributes(method_names, **options, strategy: :write_value_using_method_strategy)
|
324
|
+
end
|
325
|
+
|
326
|
+
# Public: Specify which attributes are going to be obtained by calling a
|
327
|
+
# method in the serializer.
|
328
|
+
#
|
329
|
+
# NOTE: This can be one of the slowest strategies, when in doubt, measure.
|
330
|
+
def serializer_attributes(*method_names, **options)
|
331
|
+
add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
|
332
|
+
end
|
333
|
+
|
334
|
+
# Syntax Sugar: Allows to use it before a method name.
|
335
|
+
#
|
336
|
+
# Example:
|
337
|
+
# attribute \
|
338
|
+
# def full_name
|
339
|
+
# "#{ first_name } #{ last_name }"
|
340
|
+
# end
|
341
|
+
alias attribute serializer_attributes
|
342
|
+
|
343
|
+
# Backwards Compatibility: Meant only to replace Active Model Serializers,
|
344
|
+
# calling a method in the serializer, or using `read_attribute_for_serialization`.
|
345
|
+
#
|
346
|
+
# NOTE: Prefer to use `attributes` or `serializer_attributes` explicitly.
|
347
|
+
def ams_attributes(*method_names, **options)
|
348
|
+
method_names.each do |method_name|
|
349
|
+
define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
|
350
|
+
end
|
351
|
+
add_attributes(method_names, **options, strategy: :write_value_using_serializer_strategy)
|
352
|
+
end
|
353
|
+
|
354
|
+
private
|
355
|
+
|
356
|
+
def add_attributes(names, options)
|
357
|
+
names.each { |name| add_attribute(name, options) }
|
358
|
+
end
|
359
|
+
|
360
|
+
def add_attribute(name, options)
|
361
|
+
_attributes[name.to_s.freeze] = options
|
362
|
+
end
|
363
|
+
|
364
|
+
def add_association(name, options)
|
365
|
+
_associations[name.to_s.freeze] = options
|
366
|
+
end
|
367
|
+
|
368
|
+
# Internal: We generate code for the serializer to avoid the overhead of
|
369
|
+
# using variables for method names, having to iterate the list of attributes
|
370
|
+
# and associations, and the overhead of using `send` with dynamic methods.
|
371
|
+
#
|
372
|
+
# As a result, the performance is the same as writing the most efficient
|
373
|
+
# code by hand.
|
374
|
+
def write_to_json_body
|
375
|
+
<<~WRITE_TO_JSON
|
376
|
+
# Public: Writes this serializer content to a provided Oj::StringWriter.
|
377
|
+
def write_to_json(writer)
|
378
|
+
#{ _attributes.map { |method_name, attribute_options|
|
379
|
+
write_conditional_body(method_name, attribute_options) {
|
380
|
+
<<-WRITE_ATTRIBUTE
|
381
|
+
#{attribute_options.fetch(:strategy)}(writer, #{method_name.inspect})
|
382
|
+
WRITE_ATTRIBUTE
|
383
|
+
}
|
384
|
+
}.join }
|
385
|
+
#{ _associations.map { |method_name, association_options|
|
386
|
+
write_conditional_body(method_name, association_options) {
|
387
|
+
write_association_body(method_name, association_options)
|
388
|
+
}
|
389
|
+
}.join}
|
390
|
+
end
|
391
|
+
WRITE_TO_JSON
|
392
|
+
end
|
393
|
+
|
394
|
+
# Internal: Returns the code to render an attribute or association
|
395
|
+
# conditionally.
|
396
|
+
#
|
397
|
+
# NOTE: Detects any include methods defined in the serializer, or defines
|
398
|
+
# one by using the lambda passed in the `if` option, if any.
|
399
|
+
def write_conditional_body(method_name, options)
|
400
|
+
include_method_name = "include_#{method_name}?"
|
401
|
+
if render_if = options[:if]
|
402
|
+
define_method(include_method_name, &render_if)
|
403
|
+
end
|
404
|
+
|
405
|
+
if method_defined?(include_method_name)
|
406
|
+
"if #{include_method_name};#{yield};end\n"
|
407
|
+
else
|
408
|
+
yield
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
# Internal: Returns the code for the association method.
|
413
|
+
def write_association_body(method_name, association_options)
|
414
|
+
# Use a serializer method if defined, else call the association in the object.
|
415
|
+
association_method = method_defined?(method_name) ? method_name : "@object.#{method_name}"
|
416
|
+
association_root = association_options[:root]
|
417
|
+
serializer_class = association_options.fetch(:serializer)
|
418
|
+
|
419
|
+
case write_method = association_options.fetch(:write_method)
|
420
|
+
when :write_one
|
421
|
+
<<-WRITE_ONE
|
422
|
+
if associated_object = #{association_method}
|
423
|
+
writer.push_key(#{association_root.to_s.inspect})
|
424
|
+
#{serializer_class}.write_one(writer, associated_object)
|
425
|
+
end
|
426
|
+
WRITE_ONE
|
427
|
+
when :write_many
|
428
|
+
<<-WRITE_MANY
|
429
|
+
writer.push_key(#{association_root.to_s.inspect})
|
430
|
+
#{serializer_class}.write_many(writer, #{association_method})
|
431
|
+
WRITE_MANY
|
432
|
+
when :write_flat
|
433
|
+
<<-WRITE_FLAT
|
434
|
+
#{serializer_class}.write_flat(writer, #{association_method})
|
435
|
+
WRITE_FLAT
|
436
|
+
else
|
437
|
+
raise ArgumentError, "Unknown write_method #{write_method}"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Internal: Allows to obtain a pre-existing instance and binds it to the
|
442
|
+
# specified object.
|
443
|
+
#
|
444
|
+
# NOTE: Each class is only instantiated once to reduce object allocation.
|
445
|
+
# For that reason, serializers must be completely stateless (or use global
|
446
|
+
# state).
|
447
|
+
def instance
|
448
|
+
Thread.current[instance_key] ||= new
|
449
|
+
end
|
450
|
+
|
451
|
+
# Internal: Cache key to set a thread-local instance.
|
452
|
+
def instance_key
|
453
|
+
unless defined?(@instance_key)
|
454
|
+
@instance_key = "#{name.underscore}_instance".to_sym
|
455
|
+
# We take advantage of the fact that this method will always be called
|
456
|
+
# before instantiating a serializer to define the write_to_json method.
|
457
|
+
class_eval(write_to_json_body)
|
458
|
+
raise ArgumentError, "You must use `cached ->(object) { ... }` in order to specify a different cache key when subclassing #{name}." if method_defined?(:cache_key) || respond_to?(:cache_key)
|
459
|
+
end
|
460
|
+
@instance_key
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'oj'
|
4
|
+
|
5
|
+
# NOTE: We automatically set the necessary configuration unless it had been
|
6
|
+
# explicitly set beforehand.
|
7
|
+
unless Oj.default_options[:use_raw_json]
|
8
|
+
require 'rails'
|
9
|
+
Oj.optimize_rails
|
10
|
+
Oj.default_options = { mode: :rails, use_raw_json: true }
|
11
|
+
end
|
12
|
+
|
13
|
+
# NOTE: Add an optimization to make it easier to work with a StringWriter
|
14
|
+
# transparently in different scenarios.
|
15
|
+
class Oj::StringWriter
|
16
|
+
# Patch: ActiveSupport can pass an options argument to `as_json` when
|
17
|
+
# serializing a Hash or Array.
|
18
|
+
alias_method :original_as_json, :as_json
|
19
|
+
def as_json(_options = nil)
|
20
|
+
original_as_json
|
21
|
+
end
|
22
|
+
|
23
|
+
# Optimization: We can use `to_s` directly, this is not important but gives a
|
24
|
+
# slight boost to a few use cases that use it for caching in Memcached.
|
25
|
+
def to_json(_options = nil)
|
26
|
+
to_s.delete_suffix("\n")
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/railtie'
|
4
|
+
require 'action_controller'
|
5
|
+
require 'action_controller/railtie'
|
6
|
+
|
7
|
+
require 'oj_serializers'
|
8
|
+
require 'oj_serializers/controller_serialization'
|
9
|
+
|
10
|
+
# Internal: Allows to pass Oj serializers as options in `render`.
|
11
|
+
class OjSerializers::Railtie < Rails::Railtie
|
12
|
+
initializer 'oj_serializers.action_controller' do
|
13
|
+
ActiveSupport.on_load(:action_controller) do
|
14
|
+
include(OjSerializers::ControllerSerialization)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: oj_serializers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maximo Mussini
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: oj
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.8.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.8.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: actionpack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
55
|
+
description: oj_serializers leverages the performance of the oj JSON serialization
|
56
|
+
library, and minimizes object allocations, all while provding a similar API to Active
|
57
|
+
Model Serializers.
|
58
|
+
email:
|
59
|
+
- maximomussini@gmail.com
|
60
|
+
executables: []
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- CHANGELOG.md
|
65
|
+
- README.md
|
66
|
+
- lib/oj_serializers.rb
|
67
|
+
- lib/oj_serializers/compat.rb
|
68
|
+
- lib/oj_serializers/controller_serialization.rb
|
69
|
+
- lib/oj_serializers/json_string_encoder.rb
|
70
|
+
- lib/oj_serializers/json_value.rb
|
71
|
+
- lib/oj_serializers/memo.rb
|
72
|
+
- lib/oj_serializers/serializer.rb
|
73
|
+
- lib/oj_serializers/setup.rb
|
74
|
+
- lib/oj_serializers/sugar.rb
|
75
|
+
- lib/oj_serializers/version.rb
|
76
|
+
homepage: https://github.com/ElMassimo/oj_serializers
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
homepage_uri: https://github.com/ElMassimo/oj_serializers
|
81
|
+
source_code_uri: https://github.com/ElMassimo/oj_serializers
|
82
|
+
changelog_uri: https://github.com/ElMassimo/oj_serializers/blob/master/CHANGELOG.md
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 2.3.0
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubygems_version: 3.1.2
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: A lighter JSON serializer for Ruby Objects in Rails. Easily migrate away
|
102
|
+
from Active Model Serializers.
|
103
|
+
test_files: []
|