oj_serializers 2.0.0.pre.beta.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +226 -140
- data/lib/oj_serializers/serializer.rb +153 -49
- data/lib/oj_serializers/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 281e961cd6a9da8c3723dd56ce7e1aa8ec112c57d13ab15e763ecef98eb1f659
|
4
|
+
data.tar.gz: cd9a42db1ea64aac9c75755550ce0bb3123646ac85020056a4d3bc5dae7e9251
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3816ef19190cf4dbddfa942d205b21cb0d639ed33cf9ff34a9972038179d677cfabe659fc36f1780e6fb64fb8c7145a756012bfc1a8de3bc4cb0256e419917a
|
7
|
+
data.tar.gz: 364da01e189d7c00b4abcbaa490c2e8a0432acf9f126a0db67ccab3d1a2c7a1b1724267ec084980ba706ad43fb89c1964749a4684736d72888f6ddb5fe970f3f
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## [Oj Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9)
|
2
|
+
|
3
|
+
### Features ✨
|
4
|
+
|
5
|
+
- Improved performance (20% to 40% faster than v1)
|
6
|
+
- Added `render_as_hash` to efficiently build a Hash from the serializer
|
7
|
+
- `transform_keys :camelize`: a built-in setting to convert keys, in a way that does not affect runtime performance
|
8
|
+
- `sort_keys_by :name`: allows to sort the response alphabetically, without affecting runtime performance
|
9
|
+
- `render` shortcut, unifying `one` and `many`
|
10
|
+
- `attribute` as an easier approach to define serializer attributes
|
11
|
+
|
12
|
+
### Breaking Changes
|
13
|
+
|
14
|
+
Since returning a `Hash` is more convenient than returning a `Oj::StringWriter`, and performance is comparable, `default_format :hash` is now the default.
|
15
|
+
|
16
|
+
The previous APIs will still be available as `one_as_json` and `many_as_json`, as well as `default_format :json` to make the library work like in version 1.
|
17
|
+
|
1
18
|
## Oj Serializers 1.0.2 (2023-03-01) ##
|
2
19
|
|
3
20
|
* [fix: avoid freezing `ALLOWED_INSTANCE_VARIABLES`](https://github.com/ElMassimo/oj_serializers/commit/ade0302)
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Oj Serializers
|
|
6
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
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
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/
|
9
|
+
<a href="https://github.com/ElMassimo/oj_serializers/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
|
10
10
|
</p>
|
11
11
|
</h1>
|
12
12
|
|
@@ -19,14 +19,15 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar
|
|
19
19
|
[panko]: https://github.com/panko-serializer/panko_serializer
|
20
20
|
[blueprinter]: https://github.com/procore/blueprinter
|
21
21
|
[benchmarks]: https://github.com/ElMassimo/oj_serializers/tree/master/benchmarks
|
22
|
-
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/
|
23
|
-
[sugar]: https://github.com/ElMassimo/oj_serializers/blob/
|
24
|
-
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/
|
22
|
+
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb
|
23
|
+
[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14
|
24
|
+
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
|
25
25
|
[design]: https://github.com/ElMassimo/oj_serializers#design-
|
26
26
|
[raw_json]: https://github.com/ohler55/oj/issues/542
|
27
27
|
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
|
28
28
|
[render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
|
29
29
|
[sorbet]: https://sorbet.org/
|
30
|
+
[Discussion]: https://github.com/ElMassimo/oj_serializers/discussions
|
30
31
|
|
31
32
|
## Why? 🤔
|
32
33
|
|
@@ -39,8 +40,8 @@ Learn more about [how this library achieves its performance][design].
|
|
39
40
|
|
40
41
|
## Features ⚡️
|
41
42
|
|
42
|
-
- Declaration syntax similar to Active Model Serializers
|
43
|
-
- Reduced memory allocation and [improved performance][benchmarks]
|
43
|
+
- Declaration syntax [similar to Active Model Serializers][migration guide]
|
44
|
+
- Reduced [memory allocation][benchmarks] and [improved performance][benchmarks]
|
44
45
|
- Support for `has_one` and `has_many`, compose with `flat_one`
|
45
46
|
- Useful development checks to avoid typos and mistakes
|
46
47
|
- Integrates nicely with Rails controllers
|
@@ -66,7 +67,7 @@ attributes should be serialized.
|
|
66
67
|
class AlbumSerializer < Oj::Serializer
|
67
68
|
attributes :name, :genres
|
68
69
|
|
69
|
-
|
70
|
+
attr
|
70
71
|
def release
|
71
72
|
album.release_date.strftime('%B %d, %Y')
|
72
73
|
end
|
@@ -139,48 +140,23 @@ end
|
|
139
140
|
```
|
140
141
|
</details>
|
141
142
|
|
142
|
-
|
143
|
-
|
144
|
-
To use the serializer, the recommended approach is:
|
143
|
+
You can then use your new serializer to render an object or collection:
|
145
144
|
|
146
145
|
```ruby
|
147
146
|
class AlbumsController < ApplicationController
|
148
147
|
def show
|
149
|
-
album = Album.find(params[:id])
|
150
148
|
render json: AlbumSerializer.one(album)
|
151
149
|
end
|
152
150
|
|
153
151
|
def index
|
154
|
-
albums = Album.all
|
155
152
|
render json: { albums: AlbumSerializer.many(albums) }
|
156
153
|
end
|
157
154
|
end
|
158
155
|
```
|
159
156
|
|
160
|
-
|
161
|
-
|
162
|
-
```ruby
|
163
|
-
require 'oj_serializers/sugar'
|
164
|
-
|
165
|
-
class AlbumsController < ApplicationController
|
166
|
-
def show
|
167
|
-
album = Album.find(params[:id])
|
168
|
-
render json: album, serializer: AlbumSerializer
|
169
|
-
end
|
170
|
-
|
171
|
-
def index
|
172
|
-
albums = Album.all
|
173
|
-
render json: albums, each_serializer: AlbumSerializer, root: :albums
|
174
|
-
end
|
175
|
-
end
|
176
|
-
```
|
177
|
-
|
178
|
-
It's recommended to create your own `BaseSerializer` class in order to easily
|
179
|
-
add custom extensions, specially when migrating from `active_model_serializers`.
|
157
|
+
## Rendering 🖨
|
180
158
|
|
181
|
-
|
182
|
-
|
183
|
-
In order to efficiently reuse the instances, serializers can't be instantiated directly. Use `one` and `many` to serialize objects or enumerables:
|
159
|
+
Use `one` to serialize objects, and `many` to serialize enumerables:
|
184
160
|
|
185
161
|
```ruby
|
186
162
|
render json: {
|
@@ -189,187 +165,212 @@ render json: {
|
|
189
165
|
}
|
190
166
|
```
|
191
167
|
|
192
|
-
|
193
|
-
|
194
|
-
|
168
|
+
Serializers can be rendered arrays, hashes, or even inside `ActiveModel::Serializer`
|
169
|
+
by using a method in the serializer, making it very easy to combine with other
|
170
|
+
libraries and migrate incrementally.
|
195
171
|
|
196
|
-
|
197
|
-
to JSON. Each method provides a different strategy to obtain the values to serialize.
|
172
|
+
You can use `render` as a shortcut for `one` and `many`, but it might be less readable:
|
198
173
|
|
199
|
-
|
200
|
-
|
174
|
+
```ruby
|
175
|
+
render json: {
|
176
|
+
favorite_album: AlbumSerializer.render(album),
|
177
|
+
purchased_albums: AlbumSerializer.render(albums),
|
178
|
+
}
|
179
|
+
```
|
201
180
|
|
202
|
-
|
181
|
+
## Attributes DSL 🪄
|
203
182
|
|
204
|
-
|
183
|
+
Specify which attributes should be rendered by calling a method in the object to serialize.
|
205
184
|
|
206
185
|
```ruby
|
207
186
|
class PlayerSerializer < Oj::Serializer
|
208
|
-
attributes :full_name
|
187
|
+
attributes :first_name, :last_name, :full_name
|
209
188
|
end
|
210
189
|
```
|
211
190
|
|
212
|
-
|
213
|
-
account methods defined in the serializer. Being explicit about where the
|
214
|
-
attribute is coming from makes the serializers easier to understand and more
|
215
|
-
maintainable.
|
216
|
-
|
217
|
-
### `serializer_attributes`
|
218
|
-
|
219
|
-
Obtains the attribute value by calling a method defined in the serializer.
|
220
|
-
|
221
|
-
Simply call `serialize` right before defining the method, and it will be serialized:
|
191
|
+
You can serialize custom values by specifying that a method is an `attribute`:
|
222
192
|
|
223
193
|
```ruby
|
224
194
|
class PlayerSerializer < Oj::Serializer
|
225
|
-
|
226
|
-
def full_name
|
195
|
+
attribute :name do
|
227
196
|
"#{player.first_name} #{player.last_name}"
|
228
197
|
end
|
229
|
-
end
|
230
|
-
```
|
231
|
-
|
232
|
-
> This inline syntax was inspired by how types are defined in [`sorbet`][sorbet].
|
233
198
|
|
234
|
-
|
235
|
-
`Serializer` suffix, `player` in the example above, or directly as `@object`.
|
199
|
+
# or
|
236
200
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
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.
|
242
|
-
|
243
|
-
```ruby
|
244
|
-
class AlbumSerializer < Oj::Serializer
|
245
|
-
ams_attributes :name, :release
|
246
|
-
|
247
|
-
def release
|
248
|
-
album.release_date.strftime('%B %d, %Y')
|
201
|
+
attribute
|
202
|
+
def name
|
203
|
+
"#{player.first_name} #{player.last_name}"
|
249
204
|
end
|
250
205
|
end
|
251
206
|
```
|
252
207
|
|
253
|
-
|
208
|
+
> **Note**
|
209
|
+
>
|
210
|
+
> In this example, `player` was inferred from `PlayerSerializer`.
|
211
|
+
>
|
212
|
+
> You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
|
254
213
|
|
255
|
-
Instead, use `attributes` for model methods, and the inline `attribute` for serializer attributes. Being explicit makes serializers easier to understand, and to maintain.
|
256
214
|
|
257
|
-
|
215
|
+
### Associations 🔗
|
258
216
|
|
259
|
-
|
217
|
+
Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
|
260
218
|
|
261
|
-
|
219
|
+
You must specificy which serializer to use with the `serializer` option.
|
262
220
|
|
263
221
|
```ruby
|
264
|
-
class
|
265
|
-
|
222
|
+
class SongSerializer < Oj::Serializer
|
223
|
+
has_one :album, serializer: AlbumSerializer
|
224
|
+
has_many :composers, serializer: ComposerSerializer
|
266
225
|
end
|
267
|
-
|
268
|
-
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
269
|
-
# {"first_name":"Mary","last_name":"Watson"}
|
270
226
|
```
|
271
227
|
|
272
|
-
|
273
|
-
|
274
|
-
Reads data directly from `attributes` in a [Mongoid] document.
|
275
|
-
|
276
|
-
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
277
|
-
|
278
|
-
Although there are some downsides, depending on how consistent your schema is,
|
279
|
-
and which kind of consumer the API has, it can be really powerful.
|
228
|
+
Specify a different value for the association by providing a block:
|
280
229
|
|
281
230
|
```ruby
|
282
|
-
class
|
283
|
-
|
231
|
+
class SongSerializer < Oj::Serializer
|
232
|
+
has_one :album, serializer: AlbumSerializer do
|
233
|
+
Album.find_by(song_ids: song.id)
|
234
|
+
end
|
284
235
|
end
|
285
236
|
```
|
286
237
|
|
287
|
-
|
238
|
+
In case you need to pass options, you can call the serializer manually:
|
288
239
|
|
289
|
-
|
240
|
+
```ruby
|
241
|
+
class SongSerializer < Oj::Serializer
|
242
|
+
attribute :album do
|
243
|
+
AlbumSerializer.one(song.album, for_song: song)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
```
|
290
247
|
|
291
|
-
|
292
|
-
or by calling the method in the object being serialized.
|
248
|
+
### Aliasing or renaming attributes ↔️
|
293
249
|
|
294
|
-
You
|
250
|
+
You can pass `as` when defining an attribute or association to serialize it
|
251
|
+
using a different key:
|
295
252
|
|
296
253
|
```ruby
|
297
254
|
class SongSerializer < Oj::Serializer
|
298
|
-
has_one :album, serializer: AlbumSerializer
|
299
|
-
has_many :composers, serializer: ComposerSerializer
|
255
|
+
has_one :album, as: :first_release, serializer: AlbumSerializer
|
300
256
|
|
301
|
-
|
302
|
-
|
257
|
+
attributes title: {as: :name}
|
258
|
+
|
259
|
+
# or as a shortcut
|
260
|
+
attributes title: :name
|
303
261
|
end
|
304
262
|
```
|
305
263
|
|
306
|
-
|
264
|
+
### Conditional attributes ❔
|
265
|
+
|
266
|
+
You can render attributes and associations conditionally by using `:if`.
|
307
267
|
|
308
268
|
```ruby
|
309
|
-
class
|
310
|
-
|
311
|
-
def album
|
312
|
-
AlbumSerializer.one(song.album)
|
313
|
-
end
|
269
|
+
class PlayerSerializer < Oj::Serializer
|
270
|
+
attributes :first_name, :last_name, if: -> { player.display_name? }
|
314
271
|
|
315
|
-
|
316
|
-
def composers
|
317
|
-
ComposerSerializer.many(song.composers)
|
318
|
-
end
|
272
|
+
has_one :album, serializer: AlbumSerializer, if: -> { player.album }
|
319
273
|
end
|
320
274
|
```
|
321
275
|
|
322
|
-
|
276
|
+
This is useful in cases where you don't want to `null` values to be in the response.
|
277
|
+
|
278
|
+
## Advanced Usage 🧙♂️
|
323
279
|
|
324
280
|
### Using a different alias for the internal object
|
325
281
|
|
326
|
-
|
282
|
+
In most cases, the default alias for the `object` will be convenient enough.
|
283
|
+
|
284
|
+
However, if you would like to specify it manually, use `object_as`:
|
327
285
|
|
328
286
|
```ruby
|
329
287
|
class DiscographySerializer < Oj::Serializer
|
330
288
|
object_as :artist
|
331
289
|
|
332
290
|
# Now we can use `artist` instead of `object` or `discography`.
|
333
|
-
|
291
|
+
attribute
|
334
292
|
def latest_albums
|
335
293
|
artist.albums.desc(:year)
|
336
294
|
end
|
337
295
|
end
|
338
296
|
```
|
339
297
|
|
340
|
-
###
|
298
|
+
### Identifier attributes
|
341
299
|
|
342
|
-
|
343
|
-
|
300
|
+
The `identifier` method allows you to only include an identifier if the record
|
301
|
+
or document has been persisted.
|
344
302
|
|
345
303
|
```ruby
|
346
|
-
class
|
347
|
-
|
304
|
+
class AlbumSerializer < Oj::Serializer
|
305
|
+
identifier
|
348
306
|
|
349
|
-
|
307
|
+
# or if it's a different field
|
308
|
+
identifier :uuid
|
350
309
|
end
|
351
310
|
```
|
352
311
|
|
353
|
-
|
312
|
+
Additionally, identifier fields are always rendered first, even when sorting
|
313
|
+
fields alphabetically.
|
314
|
+
|
315
|
+
### Transforming attribute keys 🗝
|
316
|
+
|
317
|
+
When serialized data will be consumed from a client language that has different
|
318
|
+
naming conventions, it can be convenient to transform keys accordingly.
|
319
|
+
|
320
|
+
For example, when rendering an API to be consumed from the browser via JavaScript,
|
321
|
+
where properties are traditionally named using camel case.
|
354
322
|
|
355
|
-
|
323
|
+
Use `transform_keys` to handle that conversion.
|
356
324
|
|
357
325
|
```ruby
|
358
|
-
class
|
359
|
-
|
326
|
+
class BaseSerializer < Oj::Serializer
|
327
|
+
transform_keys :camelize
|
328
|
+
|
329
|
+
# shortcut for
|
330
|
+
transform_keys -> (key) { key.to_s.camelize(:lower) }
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
This has no performance impact, as keys will be transformed at load time.
|
335
|
+
|
336
|
+
### Sorting attributes 📶
|
337
|
+
|
338
|
+
By default attributes are rendered in the order they are defined.
|
339
|
+
|
340
|
+
If you would like to sort attributes alphabetically, you can specify it at a
|
341
|
+
serializer level:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
class BaseSerializer < Oj::Serializer
|
345
|
+
sort_attributes_by :name # or a Proc
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
This has no performance impact, as attributes will be sorted at load time.
|
360
350
|
|
361
|
-
|
351
|
+
### Path helpers 🛣
|
362
352
|
|
363
|
-
|
364
|
-
|
365
|
-
|
353
|
+
In case you need to access path helpers in your serializers, you can use the
|
354
|
+
following:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
class BaseSerializer < Oj::Serializer
|
358
|
+
include Rails.application.routes.url_helpers
|
359
|
+
|
360
|
+
def default_url_options
|
361
|
+
Rails.application.routes.default_url_options
|
366
362
|
end
|
367
363
|
end
|
368
364
|
```
|
369
365
|
|
370
|
-
|
366
|
+
One slight variation that might make it easier to maintain in the long term is
|
367
|
+
to use a separate singleton service to provide the url helpers and options, and
|
368
|
+
make it available as `urls`.
|
369
|
+
|
370
|
+
### Memoization & local state
|
371
371
|
|
372
|
-
Serializers are designed to be stateless so that an instanced can be reused, but
|
372
|
+
Serializers are designed to be stateless so that an instanced can be reused, but
|
373
|
+
sometimes it's convenient to store intermediate calculations.
|
373
374
|
|
374
375
|
Use `memo` for memoization and storing temporary information.
|
375
376
|
|
@@ -377,7 +378,7 @@ Use `memo` for memoization and storing temporary information.
|
|
377
378
|
class DownloadSerializer < Oj::Serializer
|
378
379
|
attributes :filename, :size
|
379
380
|
|
380
|
-
|
381
|
+
attribute
|
381
382
|
def progress
|
382
383
|
"#{ last_event&.progress || 0 }%"
|
383
384
|
end
|
@@ -392,23 +393,86 @@ private
|
|
392
393
|
end
|
393
394
|
```
|
394
395
|
|
395
|
-
###
|
396
|
+
### `hash_attributes` 🚀
|
396
397
|
|
397
|
-
|
398
|
+
Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
|
398
399
|
|
399
|
-
|
400
|
-
|
400
|
+
```ruby
|
401
|
+
class PersonSerializer < Oj::Serializer
|
402
|
+
hash_attributes 'first_name', :last_name
|
403
|
+
end
|
404
|
+
|
405
|
+
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
406
|
+
# {first_name: "Mary", last_name: "Watson"}
|
407
|
+
```
|
408
|
+
|
409
|
+
### `mongo_attributes` 🚀
|
410
|
+
|
411
|
+
Reads data directly from `attributes` in a [Mongoid] document.
|
412
|
+
|
413
|
+
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
414
|
+
|
415
|
+
Although there are some downsides, depending on how consistent your schema is,
|
416
|
+
and which kind of consumer the API has, it can be really powerful.
|
417
|
+
|
418
|
+
```ruby
|
419
|
+
class AlbumSerializer < Oj::Serializer
|
420
|
+
mongo_attributes :id, :name
|
421
|
+
end
|
422
|
+
```
|
423
|
+
|
424
|
+
### Caching 📦
|
425
|
+
|
426
|
+
Usually rendering is so fast that __turning caching on can be slower__.
|
427
|
+
|
428
|
+
However, in cases of deeply nested structures, unpredictable query patterns, or
|
429
|
+
methods that take a long time to run, caching can improve performance.
|
430
|
+
|
431
|
+
To enable caching, use `cached`, which calls `cache_key` in the object:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
class CachedUserSerializer < UserSerializer
|
435
|
+
cached
|
436
|
+
end
|
437
|
+
```
|
438
|
+
|
439
|
+
You can also provide a lambda to `cached_with_key` to define a custom key:
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
class CachedUserSerializer < UserSerializer
|
443
|
+
cached_with_key ->(user) {
|
444
|
+
"#{ user.id }/#{ user.current_sign_in_at }"
|
445
|
+
}
|
446
|
+
end
|
447
|
+
```
|
448
|
+
|
449
|
+
It will leverage `fetch_multi` when serializing a collection with `many` or
|
450
|
+
`has_many`, to minimize the amount of round trips needed to read and write all
|
451
|
+
items to cache.
|
452
|
+
|
453
|
+
This works specially well if your cache store also supports `write_multi`.
|
454
|
+
|
455
|
+
### Writing to JSON
|
456
|
+
|
457
|
+
In some corner cases it might be faster to serialize using a `Oj::StringWriter`,
|
458
|
+
which you can access by using `one_as_json` and `many_as_json`.
|
459
|
+
|
460
|
+
Alternatively, you can toggle this mode at a serializer level by using
|
461
|
+
`default_format :json`, or configure it globally from your base serializer:
|
401
462
|
|
402
463
|
```ruby
|
403
464
|
class BaseSerializer < Oj::Serializer
|
404
|
-
default_format :json
|
465
|
+
default_format :json
|
405
466
|
end
|
406
467
|
```
|
407
468
|
|
408
469
|
This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
|
409
470
|
so that the serializer writes directly to JSON instead of returning a Hash.
|
410
471
|
|
411
|
-
|
472
|
+
> **Note**
|
473
|
+
>
|
474
|
+
> This was the default behavior in `oj_serializers` v1, but was replaced with
|
475
|
+
`default_format :hash` in v2.
|
412
476
|
|
413
477
|
<details>
|
414
478
|
<summary>Example Output</summary>
|
@@ -494,6 +558,9 @@ Follow [this discussion][raw_json] to find out more about [the `raw_json` extens
|
|
494
558
|
```
|
495
559
|
</details>
|
496
560
|
|
561
|
+
Even when using this mode, you can still use rendered values inside arrays,
|
562
|
+
hashes, and other serializers, thanks to [the `raw_json` extensions][raw_json].
|
563
|
+
|
497
564
|
## Design 📐
|
498
565
|
|
499
566
|
Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
|
@@ -503,6 +570,10 @@ greatly reducing the overhead of allocating and garbage collecting the hashes.
|
|
503
570
|
It also allocates a single instance per serializer class, which makes it easy
|
504
571
|
to use, while keeping memory usage under control.
|
505
572
|
|
573
|
+
The internal design is simple and extensible, and because the library is written
|
574
|
+
in Ruby, creating new serialization strategies requires very little code.
|
575
|
+
Please open a [Discussion] if you need help 😃
|
576
|
+
|
506
577
|
### Comparison with other libraries
|
507
578
|
|
508
579
|
`ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
|
@@ -516,9 +587,24 @@ mixins must be applied to the class itself.
|
|
516
587
|
|
517
588
|
`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.
|
518
589
|
|
590
|
+
Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
|
591
|
+
|
519
592
|
As a result, migrating from `active_model_serializers` is relatively
|
520
593
|
straightforward because instance methods, inheritance, and mixins work as usual.
|
521
594
|
|
595
|
+
### Benchmarks 📊
|
596
|
+
|
597
|
+
This library includes some [benchmarks] to compare performance with similar libraries.
|
598
|
+
|
599
|
+
See [this pull request](https://github.com/ElMassimo/oj_serializers/pull/9) for a quick comparison,
|
600
|
+
or check the CI to see the latest results.
|
601
|
+
|
602
|
+
### Migrating from other libraries
|
603
|
+
|
604
|
+
Please refer to the [migration guide] for a full discussion of the compatibility
|
605
|
+
modes available to make it easier to migrate from `active_model_serializers` and
|
606
|
+
similar libraries.
|
607
|
+
|
522
608
|
## Formatting 📏
|
523
609
|
|
524
610
|
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].
|
@@ -43,7 +43,7 @@ class OjSerializers::Serializer
|
|
43
43
|
def _check_instance_variables
|
44
44
|
if instance_values.keys.any? { |key| !ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
45
45
|
bad_keys = instance_values.keys.reject { |key| ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
46
|
-
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')}"
|
46
|
+
raise ArgumentError, "Serializer instances are reused so they must be stateless. Use `memo.fetch` for memoization purposes instead. Bad keys: #{bad_keys.join(',')} in #{self.class}"
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
@@ -86,7 +86,8 @@ protected
|
|
86
86
|
# Public: Allows the user to specify `default_format :json`, as a simple
|
87
87
|
# way to ensure that `.one` and `.many` work as in Version 1.
|
88
88
|
def default_format(value)
|
89
|
-
|
89
|
+
@_default_format = value
|
90
|
+
define_serialization_shortcuts
|
90
91
|
end
|
91
92
|
|
92
93
|
# Public: Allows to sort fields by name instead.
|
@@ -125,6 +126,14 @@ protected
|
|
125
126
|
# reuse the same serializer instance to serialize different objects.
|
126
127
|
delegate :write_one, :write_many, :write_to_json, to: :instance
|
127
128
|
|
129
|
+
# Internal: Keep a reference to the default `write_one` method so that we
|
130
|
+
# can use it inside cached overrides and benchmark tests.
|
131
|
+
alias_method :non_cached_write_one, :write_one
|
132
|
+
|
133
|
+
# Internal: Keep a reference to the default `write_many` method so that we
|
134
|
+
# can use it inside cached overrides and benchmark tests.
|
135
|
+
alias_method :non_cached_write_many, :write_many
|
136
|
+
|
128
137
|
# Helper: Serializes one or more items.
|
129
138
|
def render(item, options = nil)
|
130
139
|
many?(item) ? many(item, options) : one(item, options)
|
@@ -190,7 +199,7 @@ protected
|
|
190
199
|
# Internal: Will alias the object according to the name of the wrapper class.
|
191
200
|
def inherited(subclass)
|
192
201
|
object_alias = subclass.name.demodulize.chomp('Serializer').underscore
|
193
|
-
subclass.object_as(object_alias) unless method_defined?(object_alias)
|
202
|
+
subclass.object_as(object_alias) unless method_defined?(object_alias) || object_alias == 'base'
|
194
203
|
super
|
195
204
|
end
|
196
205
|
|
@@ -203,7 +212,88 @@ protected
|
|
203
212
|
|
204
213
|
protected
|
205
214
|
|
206
|
-
|
215
|
+
# Internal: Calculates the cache_key used to cache one serialized item.
|
216
|
+
def item_cache_key(item, cache_key_proc)
|
217
|
+
ActiveSupport::Cache.expand_cache_key(cache_key_proc.call(item))
|
218
|
+
end
|
219
|
+
|
220
|
+
# Public: Allows to define a cache key strategy for the serializer.
|
221
|
+
# Defaults to calling cache_key in the object if no key is provided.
|
222
|
+
#
|
223
|
+
# NOTE: Benchmark it, sometimes caching is actually SLOWER.
|
224
|
+
def cached(cache_key_proc = :cache_key.to_proc)
|
225
|
+
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
|
226
|
+
cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION }.freeze
|
227
|
+
|
228
|
+
# Internal: Redefine `one_as_hash` to use the cache for the serialized hash.
|
229
|
+
define_singleton_method(:one_as_hash) do |item, options = nil|
|
230
|
+
CACHE.fetch(item_cache_key(item, cache_key_proc), cache_hash_options) do
|
231
|
+
instance.render_as_hash(item, options)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Internal: Redefine `many_as_hash` to use the cache for the serialized hash.
|
236
|
+
define_singleton_method(:many_as_hash) do |items, options = nil|
|
237
|
+
# We define a one-off method for the class to receive the entire object
|
238
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
239
|
+
# key, and we would need to build a Hash to retrieve the object.
|
240
|
+
#
|
241
|
+
# NOTE: The assignment is important, as queries would return different
|
242
|
+
# objects when expanding with the splat in fetch_multi.
|
243
|
+
items = items.entries.each do |item|
|
244
|
+
item_key = item_cache_key(item, cache_key_proc)
|
245
|
+
item.define_singleton_method(:cache_key) { item_key }
|
246
|
+
end
|
247
|
+
|
248
|
+
# Fetch all items at once by leveraging `read_multi`.
|
249
|
+
#
|
250
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
251
|
+
# store to use Redis performance would improve a lot for this case.
|
252
|
+
CACHE.fetch_multi(*items, cache_hash_options) do |item|
|
253
|
+
instance.render_as_hash(item, options)
|
254
|
+
end.values
|
255
|
+
end
|
256
|
+
|
257
|
+
# Internal: Redefine `write_one` to use the cache for the serialized JSON.
|
258
|
+
define_singleton_method(:write_one) do |external_writer, item, options = nil|
|
259
|
+
cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
|
260
|
+
writer = new_json_writer
|
261
|
+
non_cached_write_one(writer, item, options)
|
262
|
+
writer.to_json
|
263
|
+
end
|
264
|
+
external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
|
265
|
+
end
|
266
|
+
|
267
|
+
# Internal: Redefine `write_many` to use fetch_multi from cache.
|
268
|
+
define_singleton_method(:write_many) do |external_writer, items, options = nil|
|
269
|
+
# We define a one-off method for the class to receive the entire object
|
270
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
271
|
+
# key, and we would need to build a Hash to retrieve the object.
|
272
|
+
#
|
273
|
+
# NOTE: The assignment is important, as queries would return different
|
274
|
+
# objects when expanding with the splat in fetch_multi.
|
275
|
+
items = items.entries.each do |item|
|
276
|
+
item_key = item_cache_key(item, cache_key_proc)
|
277
|
+
item.define_singleton_method(:cache_key) { item_key }
|
278
|
+
end
|
279
|
+
|
280
|
+
# Fetch all items at once by leveraging `read_multi`.
|
281
|
+
#
|
282
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
283
|
+
# store to use Redis performance would improve a lot for this case.
|
284
|
+
cached_items = CACHE.fetch_multi(*items, cache_options) do |item|
|
285
|
+
writer = new_json_writer
|
286
|
+
non_cached_write_one(writer, item, options)
|
287
|
+
writer.to_json
|
288
|
+
end.values
|
289
|
+
external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
|
290
|
+
end
|
291
|
+
|
292
|
+
define_serialization_shortcuts
|
293
|
+
end
|
294
|
+
alias_method :cached_with_key, :cached
|
295
|
+
|
296
|
+
def define_serialization_shortcuts(format = _default_format)
|
207
297
|
case format
|
208
298
|
when :json, :hash
|
209
299
|
singleton_class.alias_method :one, :"one_as_#{format}"
|
@@ -219,20 +309,27 @@ protected
|
|
219
309
|
end
|
220
310
|
|
221
311
|
# Public: Identifiers are always serialized first.
|
312
|
+
#
|
313
|
+
# NOTE: We skip the id for non-persisted documents, since it doesn't
|
314
|
+
# actually identify the document (it will change once it's persisted).
|
222
315
|
def identifier(name = :id, **options)
|
223
|
-
add_attribute(name,
|
316
|
+
add_attribute(name, attribute: :method, if: -> { !@object.new_record? }, **options, identifier: true)
|
224
317
|
end
|
225
318
|
|
226
319
|
# Public: Specify a collection of objects that should be serialized using
|
227
320
|
# the specified serializer.
|
228
|
-
def has_many(name, serializer:, root: name, as: root, **options)
|
321
|
+
def has_many(name, serializer:, root: name, as: root, **options, &block)
|
322
|
+
define_method(name, &block) if block
|
229
323
|
add_attribute(name, association: :many, as: as, serializer: serializer, **options)
|
230
324
|
end
|
231
325
|
|
232
326
|
# Public: Specify an object that should be serialized using the serializer.
|
233
|
-
def has_one(name, serializer:, root: name, as: root, **options)
|
327
|
+
def has_one(name, serializer:, root: name, as: root, **options, &block)
|
328
|
+
define_method(name, &block) if block
|
234
329
|
add_attribute(name, association: :one, as: as, serializer: serializer, **options)
|
235
330
|
end
|
331
|
+
# Alias: From a serializer perspective, the association type does not matter.
|
332
|
+
alias_method :belongs_to, :has_one
|
236
333
|
|
237
334
|
# Public: Specify an object that should be serialized using the serializer,
|
238
335
|
# but unlike `has_one`, this one will write the attributes directly without
|
@@ -255,47 +352,57 @@ protected
|
|
255
352
|
#
|
256
353
|
# See ./benchmarks/document_benchmark.rb
|
257
354
|
def mongo_attributes(*method_names, **options)
|
258
|
-
|
259
|
-
|
355
|
+
identifier(:_id, as: :id, attribute: :mongoid, **options) if method_names.delete(:id)
|
356
|
+
attributes(*method_names, **options, attribute: :mongoid)
|
260
357
|
end
|
261
358
|
|
262
359
|
# Public: Specify which attributes are going to be obtained by calling a
|
263
360
|
# method in the object.
|
264
|
-
def attributes(*method_names, **
|
265
|
-
|
361
|
+
def attributes(*method_names, **methods_with_options)
|
362
|
+
attr_options = methods_with_options.extract!(:if, :as, :attribute)
|
363
|
+
attr_options[:attribute] ||= :method
|
364
|
+
|
365
|
+
method_names.each do |name|
|
366
|
+
add_attribute(name, attr_options)
|
367
|
+
end
|
368
|
+
|
369
|
+
methods_with_options.each do |name, options|
|
370
|
+
options = { as: options } if options.is_a?(Symbol)
|
371
|
+
add_attribute(name, options)
|
372
|
+
end
|
266
373
|
end
|
267
|
-
alias_method :attribute, :attributes
|
268
374
|
|
269
375
|
# Public: Specify which attributes are going to be obtained by calling a
|
270
376
|
# method in the serializer.
|
271
|
-
#
|
272
|
-
# NOTE: This can be one of the slowest strategies, when in doubt, measure.
|
273
377
|
def serializer_attributes(*method_names, **options)
|
274
|
-
|
378
|
+
attributes(*method_names, **options, attribute: :serializer)
|
275
379
|
end
|
276
380
|
|
277
381
|
# Syntax Sugar: Allows to use it before a method name.
|
278
382
|
#
|
279
383
|
# Example:
|
280
|
-
#
|
384
|
+
# attribute
|
281
385
|
# def full_name
|
282
386
|
# "#{ first_name } #{ last_name }"
|
283
387
|
# end
|
284
|
-
def
|
388
|
+
def attribute(name = nil, **options, &block)
|
389
|
+
options[:attribute] = :serializer
|
285
390
|
if name
|
286
|
-
|
391
|
+
define_method(name, &block) if block
|
392
|
+
add_attribute(name, options)
|
287
393
|
else
|
288
|
-
@
|
394
|
+
@_current_attribute_options = options
|
289
395
|
end
|
290
396
|
end
|
397
|
+
alias_method :attr, :attribute
|
291
398
|
|
292
399
|
# Internal: Intercept a method definition, tying a type that was
|
293
400
|
# previously specified to the name of the attribute.
|
294
401
|
def method_added(name)
|
295
402
|
super(name)
|
296
|
-
if @
|
297
|
-
|
298
|
-
@
|
403
|
+
if @_current_attribute_options
|
404
|
+
add_attribute(name, @_current_attribute_options)
|
405
|
+
@_current_attribute_options = nil
|
299
406
|
end
|
300
407
|
end
|
301
408
|
|
@@ -307,7 +414,13 @@ protected
|
|
307
414
|
method_names.each do |method_name|
|
308
415
|
define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
|
309
416
|
end
|
310
|
-
|
417
|
+
attributes(*method_names, **options, attribute: :serializer)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Internal: The default format to use for `render`, `one`, and `many`.
|
421
|
+
def _default_format
|
422
|
+
@_default_format = superclass.try(:_default_format) || :hash unless defined?(@_default_format)
|
423
|
+
@_default_format
|
311
424
|
end
|
312
425
|
|
313
426
|
# Internal: The strategy to use when sorting the fields.
|
@@ -328,10 +441,6 @@ protected
|
|
328
441
|
|
329
442
|
private
|
330
443
|
|
331
|
-
def add_attributes(names, options)
|
332
|
-
names.each { |name| add_attribute(name, options) }
|
333
|
-
end
|
334
|
-
|
335
444
|
def add_attribute(name, options)
|
336
445
|
_attributes[name.to_s.freeze] = options
|
337
446
|
end
|
@@ -420,18 +529,27 @@ protected
|
|
420
529
|
RESCUE_NO_METHOD
|
421
530
|
end
|
422
531
|
|
532
|
+
# Internal: Detects any include methods defined in the serializer, or defines
|
533
|
+
# one by using the lambda passed in the `if` option, if any.
|
534
|
+
def check_conditional_method(method_name, options)
|
535
|
+
include_method_name = "include_#{method_name}#{'?' unless method_name.to_s.ends_with?('?')}"
|
536
|
+
if render_if = options[:if]
|
537
|
+
if render_if.is_a?(Symbol)
|
538
|
+
alias_method(include_method_name, render_if)
|
539
|
+
else
|
540
|
+
define_method(include_method_name, &render_if)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
include_method_name if method_defined?(include_method_name)
|
544
|
+
end
|
545
|
+
|
423
546
|
# Internal: Returns the code to render an attribute or association
|
424
547
|
# conditionally.
|
425
548
|
#
|
426
549
|
# NOTE: Detects any include methods defined in the serializer, or defines
|
427
550
|
# one by using the lambda passed in the `if` option, if any.
|
428
551
|
def code_to_write_conditional(method_name, options)
|
429
|
-
include_method_name =
|
430
|
-
if render_if = options[:if]
|
431
|
-
define_method(include_method_name, &render_if)
|
432
|
-
end
|
433
|
-
|
434
|
-
if method_defined?(include_method_name)
|
552
|
+
if (include_method_name = check_conditional_method(method_name, options))
|
435
553
|
"if #{include_method_name};#{yield};end\n"
|
436
554
|
else
|
437
555
|
yield
|
@@ -455,12 +573,6 @@ protected
|
|
455
573
|
when :mongoid
|
456
574
|
# Writes an Mongoid attribute to JSON, this is the fastest strategy.
|
457
575
|
"writer.push_value(@object.attributes['#{method_name}'], #{key})"
|
458
|
-
when :id
|
459
|
-
# Writes an _id value to JSON using `id` as the key instead.
|
460
|
-
#
|
461
|
-
# NOTE: We skip the id for non-persisted documents, since it doesn't actually
|
462
|
-
# identify the document (it will change once it's persisted).
|
463
|
-
"writer.push_value(@object.attributes['_id'], 'id') unless @object.new_record?"
|
464
576
|
else
|
465
577
|
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
466
578
|
end
|
@@ -501,13 +613,7 @@ protected
|
|
501
613
|
# NOTE: Detects any include methods defined in the serializer, or defines
|
502
614
|
# one by using the lambda passed in the `if` option, if any.
|
503
615
|
def code_to_render_conditionally(method_name, options)
|
504
|
-
include_method_name =
|
505
|
-
|
506
|
-
if render_if = options[:if]
|
507
|
-
define_method(include_method_name, &render_if)
|
508
|
-
end
|
509
|
-
|
510
|
-
if method_defined?(include_method_name)
|
616
|
+
if (include_method_name = check_conditional_method(method_name, options))
|
511
617
|
"**(#{include_method_name} ? {#{yield}} : {})"
|
512
618
|
else
|
513
619
|
yield
|
@@ -526,8 +632,6 @@ protected
|
|
526
632
|
"#{key}: @object[#{method_name.inspect}]"
|
527
633
|
when :mongoid
|
528
634
|
"#{key}: @object.attributes['#{method_name}']"
|
529
|
-
when :id
|
530
|
-
"**(@object.new_record? ? {} : {id: @object.attributes['_id']})"
|
531
635
|
else
|
532
636
|
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
533
637
|
end
|
@@ -585,7 +689,7 @@ protected
|
|
585
689
|
end
|
586
690
|
end
|
587
691
|
|
588
|
-
define_serialization_shortcuts
|
692
|
+
define_serialization_shortcuts
|
589
693
|
end
|
590
694
|
|
591
695
|
Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oj_serializers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maximo Mussini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|
@@ -64,9 +64,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
64
|
version: 2.7.0
|
65
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - ">="
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version:
|
69
|
+
version: '0'
|
70
70
|
requirements: []
|
71
71
|
rubygems_version: 3.2.32
|
72
72
|
signing_key:
|