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