oj_serializers 2.0.0.pre.beta.1 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +239 -142
- data/lib/oj_serializers/serializer.rb +277 -143
- 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: e8fd87262444d89cfbbe11a1b6ae6201de6d71ed996edcd8a4a9ae224320ad7c
|
4
|
+
data.tar.gz: bfe507b6e02c01087d99381ab8ef971547e0062085eb4bfb9c9551ceb2796c18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 060b791a7d8c2f204adab9313873b950b89eb1a80400b26355b1382ab6e08d0efa3e48efeef1ececc96cb8f42729f8f4cea50b37d579ab1acc333b393b060c0d
|
7
|
+
data.tar.gz: a7ea8370d3155f8bfd99f9d1cffe67fe05f896930277a0e69a81b3e4eda2b0088a8901730ec2fb910e564dcf3f29ea3200f5cc70d8321dd1892b72c9f9b75bf4
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
## Oj Serializers 2.0.1 (2023-04-02)
|
2
|
+
|
3
|
+
### Features ✨
|
4
|
+
|
5
|
+
- [Automatically mark `id` as an identifier (rendered first)](https://github.com/ElMassimo/oj_serializers/commit/c4c6de7)
|
6
|
+
- [Fail on typos in attribute and association options](https://github.com/ElMassimo/oj_serializers/commit/afd80ac)
|
7
|
+
|
8
|
+
### Fixes 🐞
|
9
|
+
|
10
|
+
- [Aliased attributes should be sorted by the output key](https://github.com/ElMassimo/oj_serializers/commit/fc6f4c1)
|
11
|
+
|
12
|
+
## [Oj Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9)
|
13
|
+
|
14
|
+
### Features ✨
|
15
|
+
|
16
|
+
- Improved performance (20% to 40% faster than v1)
|
17
|
+
- Added `render_as_hash` to efficiently build a Hash from the serializer
|
18
|
+
- `transform_keys :camelize`: a built-in setting to convert keys, in a way that does not affect runtime performance
|
19
|
+
- `sort_keys_by :name`: allows to sort the response alphabetically, without affecting runtime performance
|
20
|
+
- `render` shortcut, unifying `one` and `many`
|
21
|
+
- `attribute` as an easier approach to define serializer attributes
|
22
|
+
|
23
|
+
### Breaking Changes
|
24
|
+
|
25
|
+
Since returning a `Hash` is more convenient than returning a `Oj::StringWriter`, and performance is comparable, `default_format :hash` is now the default.
|
26
|
+
|
27
|
+
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.
|
28
|
+
|
1
29
|
## Oj Serializers 1.0.2 (2023-03-01) ##
|
2
30
|
|
3
31
|
* [fix: avoid freezing `ALLOWED_INSTANCE_VARIABLES`](https://github.com/ElMassimo/oj_serializers/commit/ade0302)
|
data/README.md
CHANGED
@@ -2,11 +2,10 @@
|
|
2
2
|
Oj Serializers
|
3
3
|
<p align="center">
|
4
4
|
<a href="https://github.com/ElMassimo/oj_serializers/actions"><img alt="Build Status" src="https://github.com/ElMassimo/oj_serializers/workflows/build/badge.svg"/></a>
|
5
|
-
<a href="https://inch-ci.org/github/ElMassimo/oj_serializers"><img alt="Inline docs" src="https://inch-ci.org/github/ElMassimo/oj_serializers.svg"/></a>
|
6
5
|
<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
6
|
<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
7
|
<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/
|
8
|
+
<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
9
|
</p>
|
11
10
|
</h1>
|
12
11
|
|
@@ -19,14 +18,17 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar
|
|
19
18
|
[panko]: https://github.com/panko-serializer/panko_serializer
|
20
19
|
[blueprinter]: https://github.com/procore/blueprinter
|
21
20
|
[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/
|
21
|
+
[raw_benchmarks]: https://github.com/ElMassimo/oj_serializers/blob/main/benchmarks/document_benchmark.rb
|
22
|
+
[sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14
|
23
|
+
[migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
|
25
24
|
[design]: https://github.com/ElMassimo/oj_serializers#design-
|
26
25
|
[raw_json]: https://github.com/ohler55/oj/issues/542
|
27
26
|
[trailing_commas]: https://maximomussini.com/posts/trailing-commas/
|
28
27
|
[render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
|
29
28
|
[sorbet]: https://sorbet.org/
|
29
|
+
[Discussion]: https://github.com/ElMassimo/oj_serializers/discussions
|
30
|
+
[TypeScript]: https://www.typescriptlang.org/
|
31
|
+
[types_from_serializers]: https://github.com/ElMassimo/types_from_serializers
|
30
32
|
|
31
33
|
## Why? 🤔
|
32
34
|
|
@@ -39,11 +41,12 @@ Learn more about [how this library achieves its performance][design].
|
|
39
41
|
|
40
42
|
## Features ⚡️
|
41
43
|
|
42
|
-
- Declaration syntax similar to Active Model Serializers
|
43
|
-
- Reduced memory allocation and [improved performance][benchmarks]
|
44
|
+
- Declaration syntax [similar to Active Model Serializers][migration guide]
|
45
|
+
- Reduced [memory allocation][benchmarks] and [improved performance][benchmarks]
|
44
46
|
- Support for `has_one` and `has_many`, compose with `flat_one`
|
45
47
|
- Useful development checks to avoid typos and mistakes
|
46
48
|
- Integrates nicely with Rails controllers
|
49
|
+
- [Generate TypeScript interfaces automatically][types_from_serializers]
|
47
50
|
|
48
51
|
## Installation 💿
|
49
52
|
|
@@ -66,8 +69,7 @@ attributes should be serialized.
|
|
66
69
|
class AlbumSerializer < Oj::Serializer
|
67
70
|
attributes :name, :genres
|
68
71
|
|
69
|
-
|
70
|
-
def release
|
72
|
+
attribute :release do
|
71
73
|
album.release_date.strftime('%B %d, %Y')
|
72
74
|
end
|
73
75
|
|
@@ -139,48 +141,23 @@ end
|
|
139
141
|
```
|
140
142
|
</details>
|
141
143
|
|
142
|
-
|
143
|
-
|
144
|
-
To use the serializer, the recommended approach is:
|
144
|
+
You can then use your new serializer to render an object or collection:
|
145
145
|
|
146
146
|
```ruby
|
147
147
|
class AlbumsController < ApplicationController
|
148
148
|
def show
|
149
|
-
album = Album.find(params[:id])
|
150
149
|
render json: AlbumSerializer.one(album)
|
151
150
|
end
|
152
151
|
|
153
152
|
def index
|
154
|
-
albums = Album.all
|
155
153
|
render json: { albums: AlbumSerializer.many(albums) }
|
156
154
|
end
|
157
155
|
end
|
158
156
|
```
|
159
157
|
|
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`.
|
158
|
+
## Rendering 🖨
|
180
159
|
|
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:
|
160
|
+
Use `one` to serialize objects, and `many` to serialize enumerables:
|
184
161
|
|
185
162
|
```ruby
|
186
163
|
render json: {
|
@@ -189,187 +166,222 @@ render json: {
|
|
189
166
|
}
|
190
167
|
```
|
191
168
|
|
192
|
-
|
193
|
-
|
194
|
-
|
169
|
+
Serializers can be rendered arrays, hashes, or even inside `ActiveModel::Serializer`
|
170
|
+
by using a method in the serializer, making it very easy to combine with other
|
171
|
+
libraries and migrate incrementally.
|
195
172
|
|
196
|
-
|
197
|
-
to JSON. Each method provides a different strategy to obtain the values to serialize.
|
173
|
+
You can use `render` as a shortcut for `one` and `many`, but it might be less readable:
|
198
174
|
|
199
|
-
|
200
|
-
|
175
|
+
```ruby
|
176
|
+
render json: {
|
177
|
+
favorite_album: AlbumSerializer.render(album),
|
178
|
+
purchased_albums: AlbumSerializer.render(albums),
|
179
|
+
}
|
180
|
+
```
|
201
181
|
|
202
|
-
|
182
|
+
## Attributes DSL 🪄
|
203
183
|
|
204
|
-
|
184
|
+
Specify which attributes should be rendered by calling a method in the object to serialize.
|
205
185
|
|
206
186
|
```ruby
|
207
187
|
class PlayerSerializer < Oj::Serializer
|
208
|
-
attributes :full_name
|
188
|
+
attributes :first_name, :last_name, :full_name
|
209
189
|
end
|
210
190
|
```
|
211
191
|
|
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:
|
192
|
+
You can serialize custom values by specifying that a method is an `attribute`:
|
222
193
|
|
223
194
|
```ruby
|
224
195
|
class PlayerSerializer < Oj::Serializer
|
225
|
-
|
226
|
-
def full_name
|
196
|
+
attribute :name do
|
227
197
|
"#{player.first_name} #{player.last_name}"
|
228
198
|
end
|
229
|
-
end
|
230
|
-
```
|
231
|
-
|
232
|
-
> This inline syntax was inspired by how types are defined in [`sorbet`][sorbet].
|
233
199
|
|
234
|
-
|
235
|
-
`Serializer` suffix, `player` in the example above, or directly as `@object`.
|
236
|
-
|
237
|
-
You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
|
238
|
-
|
239
|
-
### `ams_attributes` 🐌
|
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
|
200
|
+
# or
|
246
201
|
|
247
|
-
|
248
|
-
|
202
|
+
attribute
|
203
|
+
def name
|
204
|
+
"#{player.first_name} #{player.last_name}"
|
249
205
|
end
|
250
206
|
end
|
251
207
|
```
|
252
208
|
|
253
|
-
|
209
|
+
> **Note**
|
210
|
+
>
|
211
|
+
> In this example, `player` was inferred from `PlayerSerializer`.
|
212
|
+
>
|
213
|
+
> You can customize this by using [`object_as`](#using-a-different-alias-for-the-internal-object).
|
254
214
|
|
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
215
|
|
257
|
-
|
216
|
+
### Associations 🔗
|
258
217
|
|
259
|
-
|
218
|
+
Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
|
260
219
|
|
261
|
-
|
220
|
+
You must specificy which serializer to use with the `serializer` option.
|
262
221
|
|
263
222
|
```ruby
|
264
|
-
class
|
265
|
-
|
223
|
+
class SongSerializer < Oj::Serializer
|
224
|
+
has_one :album, serializer: AlbumSerializer
|
225
|
+
has_many :composers, serializer: ComposerSerializer
|
266
226
|
end
|
267
|
-
|
268
|
-
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
269
|
-
# {"first_name":"Mary","last_name":"Watson"}
|
270
227
|
```
|
271
228
|
|
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.
|
229
|
+
Specify a different value for the association by providing a block:
|
280
230
|
|
281
231
|
```ruby
|
282
|
-
class
|
283
|
-
|
232
|
+
class SongSerializer < Oj::Serializer
|
233
|
+
has_one :album, serializer: AlbumSerializer do
|
234
|
+
Album.find_by(song_ids: song.id)
|
235
|
+
end
|
284
236
|
end
|
285
237
|
```
|
286
238
|
|
287
|
-
|
239
|
+
In case you need to pass options, you can call the serializer manually:
|
288
240
|
|
289
|
-
|
241
|
+
```ruby
|
242
|
+
class SongSerializer < Oj::Serializer
|
243
|
+
attribute :album do
|
244
|
+
AlbumSerializer.one(song.album, for_song: song)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
```
|
290
248
|
|
291
|
-
|
292
|
-
or by calling the method in the object being serialized.
|
249
|
+
### Aliasing or renaming attributes ↔️
|
293
250
|
|
294
|
-
You
|
251
|
+
You can pass `as` when defining an attribute or association to serialize it
|
252
|
+
using a different key:
|
295
253
|
|
296
254
|
```ruby
|
297
255
|
class SongSerializer < Oj::Serializer
|
298
|
-
has_one :album, serializer: AlbumSerializer
|
299
|
-
|
256
|
+
has_one :album, as: :first_release, serializer: AlbumSerializer
|
257
|
+
|
258
|
+
attributes title: {as: :name}
|
300
259
|
|
301
|
-
#
|
302
|
-
|
260
|
+
# or as a shortcut
|
261
|
+
attributes title: :name
|
303
262
|
end
|
304
263
|
```
|
305
264
|
|
306
|
-
|
265
|
+
### Conditional attributes ❔
|
266
|
+
|
267
|
+
You can render attributes and associations conditionally by using `:if`.
|
307
268
|
|
308
269
|
```ruby
|
309
|
-
class
|
310
|
-
|
311
|
-
def album
|
312
|
-
AlbumSerializer.one(song.album)
|
313
|
-
end
|
270
|
+
class PlayerSerializer < Oj::Serializer
|
271
|
+
attributes :first_name, :last_name, if: -> { player.display_name? }
|
314
272
|
|
315
|
-
|
316
|
-
def composers
|
317
|
-
ComposerSerializer.many(song.composers)
|
318
|
-
end
|
273
|
+
has_one :album, serializer: AlbumSerializer, if: -> { player.album }
|
319
274
|
end
|
320
275
|
```
|
321
276
|
|
322
|
-
|
277
|
+
This is useful in cases where you don't want to `null` values to be in the response.
|
278
|
+
|
279
|
+
## Advanced Usage 🧙♂️
|
323
280
|
|
324
281
|
### Using a different alias for the internal object
|
325
282
|
|
326
|
-
|
283
|
+
In most cases, the default alias for the `object` will be convenient enough.
|
284
|
+
|
285
|
+
However, if you would like to specify it manually, use `object_as`:
|
327
286
|
|
328
287
|
```ruby
|
329
288
|
class DiscographySerializer < Oj::Serializer
|
330
289
|
object_as :artist
|
331
290
|
|
332
291
|
# Now we can use `artist` instead of `object` or `discography`.
|
333
|
-
|
292
|
+
attribute
|
334
293
|
def latest_albums
|
335
294
|
artist.albums.desc(:year)
|
336
295
|
end
|
337
296
|
end
|
338
297
|
```
|
339
298
|
|
340
|
-
###
|
299
|
+
### Identifier attributes
|
341
300
|
|
342
|
-
|
343
|
-
|
301
|
+
The `identifier` method allows you to only include an identifier if the record
|
302
|
+
or document has been persisted.
|
344
303
|
|
345
304
|
```ruby
|
346
|
-
class
|
347
|
-
|
305
|
+
class AlbumSerializer < Oj::Serializer
|
306
|
+
identifier
|
307
|
+
|
308
|
+
# or if it's a different field
|
309
|
+
identifier :uuid
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
Additionally, identifier fields are always rendered first, even when sorting
|
314
|
+
fields alphabetically.
|
315
|
+
|
316
|
+
### Transforming attribute keys 🗝
|
317
|
+
|
318
|
+
When serialized data will be consumed from a client language that has different
|
319
|
+
naming conventions, it can be convenient to transform keys accordingly.
|
320
|
+
|
321
|
+
For example, when rendering an API to be consumed from the browser via JavaScript,
|
322
|
+
where properties are traditionally named using camel case.
|
323
|
+
|
324
|
+
Use `transform_keys` to handle that conversion.
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
class BaseSerializer < Oj::Serializer
|
328
|
+
transform_keys :camelize
|
348
329
|
|
349
|
-
|
330
|
+
# shortcut for
|
331
|
+
transform_keys -> (key) { key.to_s.camelize(:lower) }
|
350
332
|
end
|
351
333
|
```
|
352
334
|
|
353
|
-
|
335
|
+
This has no performance impact, as keys will be transformed at load time.
|
354
336
|
|
355
|
-
|
337
|
+
### Sorting attributes 📶
|
338
|
+
|
339
|
+
By default attributes are rendered in the order they are defined.
|
340
|
+
|
341
|
+
If you would like to sort attributes alphabetically, you can specify it at a
|
342
|
+
serializer level:
|
356
343
|
|
357
344
|
```ruby
|
358
|
-
class
|
359
|
-
|
345
|
+
class BaseSerializer < Oj::Serializer
|
346
|
+
sort_attributes_by :name # or a Proc
|
347
|
+
end
|
348
|
+
```
|
349
|
+
|
350
|
+
This has no performance impact, as attributes will be sorted at load time.
|
351
|
+
|
352
|
+
### Path helpers 🛣
|
360
353
|
|
361
|
-
|
354
|
+
In case you need to access path helpers in your serializers, you can use the
|
355
|
+
following:
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
class BaseSerializer < Oj::Serializer
|
359
|
+
include Rails.application.routes.url_helpers
|
362
360
|
|
363
|
-
|
364
|
-
|
365
|
-
album.songs.any?
|
361
|
+
def default_url_options
|
362
|
+
Rails.application.routes.default_url_options
|
366
363
|
end
|
367
364
|
end
|
368
365
|
```
|
369
366
|
|
370
|
-
|
367
|
+
One slight variation that might make it easier to maintain in the long term is
|
368
|
+
to use a separate singleton service to provide the url helpers and options, and
|
369
|
+
make it available as `urls`.
|
370
|
+
|
371
|
+
### Generating TypeScript automatically 🤖
|
372
|
+
|
373
|
+
It's easy for the backend and the frontend to become out of sync. Traditionally, preventing bugs requires writing extensive integration tests.
|
374
|
+
|
375
|
+
[TypeScript] is a great tool to catch this kind of bugs and mistakes, as it can detect incorrect usages and missing fields, but writing types manually is cumbersome, and they can become stale over time, giving a false sense of confidence.
|
376
|
+
|
377
|
+
[`types_from_serializers`][types_from_serializers] extends this library to allow embedding type information, as well as inferring types from the SQL schema when available, and uses this information to automatically generate TypeScript interfaces from your serializers.
|
371
378
|
|
372
|
-
|
379
|
+
As a result, it's posible to easily detect mismatches between the backend and the frontend, as well as make the fields more discoverable and provide great autocompletion in the frontend, without having to manually write the types.
|
380
|
+
|
381
|
+
### Memoization & local state
|
382
|
+
|
383
|
+
Serializers are designed to be stateless so that an instanced can be reused, but
|
384
|
+
sometimes it's convenient to store intermediate calculations.
|
373
385
|
|
374
386
|
Use `memo` for memoization and storing temporary information.
|
375
387
|
|
@@ -377,7 +389,7 @@ Use `memo` for memoization and storing temporary information.
|
|
377
389
|
class DownloadSerializer < Oj::Serializer
|
378
390
|
attributes :filename, :size
|
379
391
|
|
380
|
-
|
392
|
+
attribute
|
381
393
|
def progress
|
382
394
|
"#{ last_event&.progress || 0 }%"
|
383
395
|
end
|
@@ -392,23 +404,86 @@ private
|
|
392
404
|
end
|
393
405
|
```
|
394
406
|
|
395
|
-
###
|
407
|
+
### `hash_attributes` 🚀
|
408
|
+
|
409
|
+
Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
class PersonSerializer < Oj::Serializer
|
413
|
+
hash_attributes 'first_name', :last_name
|
414
|
+
end
|
415
|
+
|
416
|
+
PersonSerializer.one('first_name' => 'Mary', :middle_name => 'Jane', :last_name => 'Watson')
|
417
|
+
# {first_name: "Mary", last_name: "Watson"}
|
418
|
+
```
|
396
419
|
|
397
|
-
|
420
|
+
### `mongo_attributes` 🚀
|
398
421
|
|
399
|
-
|
400
|
-
|
422
|
+
Reads data directly from `attributes` in a [Mongoid] document.
|
423
|
+
|
424
|
+
By skipping type casting, coercion, and defaults, it [achieves the best performance][raw_benchmarks].
|
425
|
+
|
426
|
+
Although there are some downsides, depending on how consistent your schema is,
|
427
|
+
and which kind of consumer the API has, it can be really powerful.
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
class AlbumSerializer < Oj::Serializer
|
431
|
+
mongo_attributes :id, :name
|
432
|
+
end
|
433
|
+
```
|
434
|
+
|
435
|
+
### Caching 📦
|
436
|
+
|
437
|
+
Usually rendering is so fast that __turning caching on can be slower__.
|
438
|
+
|
439
|
+
However, in cases of deeply nested structures, unpredictable query patterns, or
|
440
|
+
methods that take a long time to run, caching can improve performance.
|
441
|
+
|
442
|
+
To enable caching, use `cached`, which calls `cache_key` in the object:
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
class CachedUserSerializer < UserSerializer
|
446
|
+
cached
|
447
|
+
end
|
448
|
+
```
|
449
|
+
|
450
|
+
You can also provide a lambda to `cached_with_key` to define a custom key:
|
451
|
+
|
452
|
+
```ruby
|
453
|
+
class CachedUserSerializer < UserSerializer
|
454
|
+
cached_with_key ->(user) {
|
455
|
+
"#{ user.id }/#{ user.current_sign_in_at }"
|
456
|
+
}
|
457
|
+
end
|
458
|
+
```
|
459
|
+
|
460
|
+
It will leverage `fetch_multi` when serializing a collection with `many` or
|
461
|
+
`has_many`, to minimize the amount of round trips needed to read and write all
|
462
|
+
items to cache.
|
463
|
+
|
464
|
+
This works specially well if your cache store also supports `write_multi`.
|
465
|
+
|
466
|
+
### Writing to JSON
|
467
|
+
|
468
|
+
In some corner cases it might be faster to serialize using a `Oj::StringWriter`,
|
469
|
+
which you can access by using `one_as_json` and `many_as_json`.
|
470
|
+
|
471
|
+
Alternatively, you can toggle this mode at a serializer level by using
|
472
|
+
`default_format :json`, or configure it globally from your base serializer:
|
401
473
|
|
402
474
|
```ruby
|
403
475
|
class BaseSerializer < Oj::Serializer
|
404
|
-
default_format :json
|
476
|
+
default_format :json
|
405
477
|
end
|
406
478
|
```
|
407
479
|
|
408
480
|
This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
|
409
481
|
so that the serializer writes directly to JSON instead of returning a Hash.
|
410
482
|
|
411
|
-
|
483
|
+
> **Note**
|
484
|
+
>
|
485
|
+
> This was the default behavior in `oj_serializers` v1, but was replaced with
|
486
|
+
`default_format :hash` in v2.
|
412
487
|
|
413
488
|
<details>
|
414
489
|
<summary>Example Output</summary>
|
@@ -494,6 +569,9 @@ Follow [this discussion][raw_json] to find out more about [the `raw_json` extens
|
|
494
569
|
```
|
495
570
|
</details>
|
496
571
|
|
572
|
+
Even when using this mode, you can still use rendered values inside arrays,
|
573
|
+
hashes, and other serializers, thanks to [the `raw_json` extensions][raw_json].
|
574
|
+
|
497
575
|
## Design 📐
|
498
576
|
|
499
577
|
Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
|
@@ -503,6 +581,10 @@ greatly reducing the overhead of allocating and garbage collecting the hashes.
|
|
503
581
|
It also allocates a single instance per serializer class, which makes it easy
|
504
582
|
to use, while keeping memory usage under control.
|
505
583
|
|
584
|
+
The internal design is simple and extensible, and because the library is written
|
585
|
+
in Ruby, creating new serialization strategies requires very little code.
|
586
|
+
Please open a [Discussion] if you need help 😃
|
587
|
+
|
506
588
|
### Comparison with other libraries
|
507
589
|
|
508
590
|
`ActiveModel::Serializer` instantiates one serializer object per item to be serialized.
|
@@ -516,9 +598,24 @@ mixins must be applied to the class itself.
|
|
516
598
|
|
517
599
|
`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
600
|
|
601
|
+
Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
|
602
|
+
|
519
603
|
As a result, migrating from `active_model_serializers` is relatively
|
520
604
|
straightforward because instance methods, inheritance, and mixins work as usual.
|
521
605
|
|
606
|
+
### Benchmarks 📊
|
607
|
+
|
608
|
+
This library includes some [benchmarks] to compare performance with similar libraries.
|
609
|
+
|
610
|
+
See [this pull request](https://github.com/ElMassimo/oj_serializers/pull/9) for a quick comparison,
|
611
|
+
or check the CI to see the latest results.
|
612
|
+
|
613
|
+
### Migrating from other libraries
|
614
|
+
|
615
|
+
Please refer to the [migration guide] for a full discussion of the compatibility
|
616
|
+
modes available to make it easier to migrate from `active_model_serializers` and
|
617
|
+
similar libraries.
|
618
|
+
|
522
619
|
## Formatting 📏
|
523
620
|
|
524
621
|
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].
|
@@ -19,7 +19,22 @@ require 'oj_serializers/json_value'
|
|
19
19
|
class OjSerializers::Serializer
|
20
20
|
# Public: Used to validate incorrect memoization during development. Users of
|
21
21
|
# this library might add additional options as needed.
|
22
|
-
ALLOWED_INSTANCE_VARIABLES = %w[
|
22
|
+
ALLOWED_INSTANCE_VARIABLES = %w[
|
23
|
+
memo
|
24
|
+
object
|
25
|
+
options
|
26
|
+
_routes
|
27
|
+
]
|
28
|
+
|
29
|
+
KNOWN_ATTRIBUTE_OPTIONS = %i[
|
30
|
+
attribute
|
31
|
+
association
|
32
|
+
identifier
|
33
|
+
if
|
34
|
+
optional
|
35
|
+
type
|
36
|
+
serializer
|
37
|
+
].to_set
|
23
38
|
|
24
39
|
CACHE = (defined?(Rails) && Rails.cache) ||
|
25
40
|
(defined?(ActiveSupport::Cache::MemoryStore) ? ActiveSupport::Cache::MemoryStore.new : OjSerializers::Memo.new)
|
@@ -43,7 +58,7 @@ class OjSerializers::Serializer
|
|
43
58
|
def _check_instance_variables
|
44
59
|
if instance_values.keys.any? { |key| !ALLOWED_INSTANCE_VARIABLES.include?(key) }
|
45
60
|
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(',')}"
|
61
|
+
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
62
|
end
|
48
63
|
end
|
49
64
|
end
|
@@ -85,29 +100,40 @@ protected
|
|
85
100
|
class << self
|
86
101
|
# Public: Allows the user to specify `default_format :json`, as a simple
|
87
102
|
# way to ensure that `.one` and `.many` work as in Version 1.
|
88
|
-
|
89
|
-
|
103
|
+
#
|
104
|
+
# This setting is inherited from parent classes.
|
105
|
+
def default_format(format)
|
106
|
+
define_singleton_method(:_default_format) { format }
|
107
|
+
define_serialization_shortcuts
|
90
108
|
end
|
91
109
|
|
92
|
-
# Public: Allows to sort fields by name instead
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
110
|
+
# Public: Allows to sort fields by name instead of by definition order, or
|
111
|
+
# pass a Proc to apply a custom order.
|
112
|
+
#
|
113
|
+
# This setting is inherited from parent classes.
|
114
|
+
def sort_attributes_by(strategy)
|
115
|
+
case strategy
|
116
|
+
when :name, :definition, Proc
|
117
|
+
define_singleton_method(:_sort_attributes_by) { strategy }
|
97
118
|
else
|
98
|
-
raise ArgumentError, "Unknown sorting option: #{
|
119
|
+
raise ArgumentError, "Unknown sorting option: #{strategy.inspect}"
|
99
120
|
end
|
100
121
|
end
|
101
122
|
|
102
|
-
# Public: Allows to
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
123
|
+
# Public: Allows to transform the JSON keys to camelCase, or pass a Proc
|
124
|
+
# to apply a custom transformation.
|
125
|
+
#
|
126
|
+
# This setting is inherited from parent classes.
|
127
|
+
def transform_keys(strategy = nil, &block)
|
128
|
+
transformer = case (strategy ||= block)
|
129
|
+
when :camelize, :camel_case then ->(key) { key.camelize(:lower) }
|
130
|
+
when :none then nil
|
131
|
+
when Symbol then strategy.to_proc
|
132
|
+
when Proc then strategy
|
108
133
|
else
|
109
|
-
raise(ArgumentError, "Expected transform_keys to be callable, got: #{
|
134
|
+
raise(ArgumentError, "Expected transform_keys to be callable, got: #{strategy.inspect}")
|
110
135
|
end
|
136
|
+
define_singleton_method(:_transform_keys) { transformer }
|
111
137
|
end
|
112
138
|
|
113
139
|
# Public: Creates an alias for the internal object.
|
@@ -125,6 +151,14 @@ protected
|
|
125
151
|
# reuse the same serializer instance to serialize different objects.
|
126
152
|
delegate :write_one, :write_many, :write_to_json, to: :instance
|
127
153
|
|
154
|
+
# Internal: Keep a reference to the default `write_one` method so that we
|
155
|
+
# can use it inside cached overrides and benchmark tests.
|
156
|
+
alias_method :non_cached_write_one, :write_one
|
157
|
+
|
158
|
+
# Internal: Keep a reference to the default `write_many` method so that we
|
159
|
+
# can use it inside cached overrides and benchmark tests.
|
160
|
+
alias_method :non_cached_write_many, :write_many
|
161
|
+
|
128
162
|
# Helper: Serializes one or more items.
|
129
163
|
def render(item, options = nil)
|
130
164
|
many?(item) ? many(item, options) : one(item, options)
|
@@ -190,7 +224,7 @@ protected
|
|
190
224
|
# Internal: Will alias the object according to the name of the wrapper class.
|
191
225
|
def inherited(subclass)
|
192
226
|
object_alias = subclass.name.demodulize.chomp('Serializer').underscore
|
193
|
-
subclass.object_as(object_alias) unless method_defined?(object_alias)
|
227
|
+
subclass.object_as(object_alias) unless method_defined?(object_alias) || object_alias == 'base'
|
194
228
|
super
|
195
229
|
end
|
196
230
|
|
@@ -203,7 +237,88 @@ protected
|
|
203
237
|
|
204
238
|
protected
|
205
239
|
|
206
|
-
|
240
|
+
# Internal: Calculates the cache_key used to cache one serialized item.
|
241
|
+
def item_cache_key(item, cache_key_proc)
|
242
|
+
ActiveSupport::Cache.expand_cache_key(cache_key_proc.call(item))
|
243
|
+
end
|
244
|
+
|
245
|
+
# Public: Allows to define a cache key strategy for the serializer.
|
246
|
+
# Defaults to calling cache_key in the object if no key is provided.
|
247
|
+
#
|
248
|
+
# NOTE: Benchmark it, sometimes caching is actually SLOWER.
|
249
|
+
def cached(cache_key_proc = :cache_key.to_proc)
|
250
|
+
cache_options = { namespace: "#{name}#write_to_json", version: OjSerializers::VERSION }.freeze
|
251
|
+
cache_hash_options = { namespace: "#{name}#render_as_hash", version: OjSerializers::VERSION }.freeze
|
252
|
+
|
253
|
+
# Internal: Redefine `one_as_hash` to use the cache for the serialized hash.
|
254
|
+
define_singleton_method(:one_as_hash) do |item, options = nil|
|
255
|
+
CACHE.fetch(item_cache_key(item, cache_key_proc), cache_hash_options) do
|
256
|
+
instance.render_as_hash(item, options)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Internal: Redefine `many_as_hash` to use the cache for the serialized hash.
|
261
|
+
define_singleton_method(:many_as_hash) do |items, options = nil|
|
262
|
+
# We define a one-off method for the class to receive the entire object
|
263
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
264
|
+
# key, and we would need to build a Hash to retrieve the object.
|
265
|
+
#
|
266
|
+
# NOTE: The assignment is important, as queries would return different
|
267
|
+
# objects when expanding with the splat in fetch_multi.
|
268
|
+
items = items.entries.each do |item|
|
269
|
+
item_key = item_cache_key(item, cache_key_proc)
|
270
|
+
item.define_singleton_method(:cache_key) { item_key }
|
271
|
+
end
|
272
|
+
|
273
|
+
# Fetch all items at once by leveraging `read_multi`.
|
274
|
+
#
|
275
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
276
|
+
# store to use Redis performance would improve a lot for this case.
|
277
|
+
CACHE.fetch_multi(*items, cache_hash_options) do |item|
|
278
|
+
instance.render_as_hash(item, options)
|
279
|
+
end.values
|
280
|
+
end
|
281
|
+
|
282
|
+
# Internal: Redefine `write_one` to use the cache for the serialized JSON.
|
283
|
+
define_singleton_method(:write_one) do |external_writer, item, options = nil|
|
284
|
+
cached_item = CACHE.fetch(item_cache_key(item, cache_key_proc), cache_options) do
|
285
|
+
writer = new_json_writer
|
286
|
+
non_cached_write_one(writer, item, options)
|
287
|
+
writer.to_json
|
288
|
+
end
|
289
|
+
external_writer.push_json("#{cached_item}\n") # Oj.dump expects a new line terminator.
|
290
|
+
end
|
291
|
+
|
292
|
+
# Internal: Redefine `write_many` to use fetch_multi from cache.
|
293
|
+
define_singleton_method(:write_many) do |external_writer, items, options = nil|
|
294
|
+
# We define a one-off method for the class to receive the entire object
|
295
|
+
# inside the `fetch_multi` block. Otherwise we would only get the cache
|
296
|
+
# key, and we would need to build a Hash to retrieve the object.
|
297
|
+
#
|
298
|
+
# NOTE: The assignment is important, as queries would return different
|
299
|
+
# objects when expanding with the splat in fetch_multi.
|
300
|
+
items = items.entries.each do |item|
|
301
|
+
item_key = item_cache_key(item, cache_key_proc)
|
302
|
+
item.define_singleton_method(:cache_key) { item_key }
|
303
|
+
end
|
304
|
+
|
305
|
+
# Fetch all items at once by leveraging `read_multi`.
|
306
|
+
#
|
307
|
+
# NOTE: Memcached does not support `write_multi`, if we switch the cache
|
308
|
+
# store to use Redis performance would improve a lot for this case.
|
309
|
+
cached_items = CACHE.fetch_multi(*items, cache_options) do |item|
|
310
|
+
writer = new_json_writer
|
311
|
+
non_cached_write_one(writer, item, options)
|
312
|
+
writer.to_json
|
313
|
+
end.values
|
314
|
+
external_writer.push_json("#{OjSerializers::JsonValue.array(cached_items)}\n") # Oj.dump expects a new line terminator.
|
315
|
+
end
|
316
|
+
|
317
|
+
define_serialization_shortcuts
|
318
|
+
end
|
319
|
+
alias_method :cached_with_key, :cached
|
320
|
+
|
321
|
+
def define_serialization_shortcuts(format = _default_format)
|
207
322
|
case format
|
208
323
|
when :json, :hash
|
209
324
|
singleton_class.alias_method :one, :"one_as_#{format}"
|
@@ -219,20 +334,27 @@ protected
|
|
219
334
|
end
|
220
335
|
|
221
336
|
# Public: Identifiers are always serialized first.
|
337
|
+
#
|
338
|
+
# NOTE: We skip the id for non-persisted documents, since it doesn't
|
339
|
+
# actually identify the document (it will change once it's persisted).
|
222
340
|
def identifier(name = :id, **options)
|
223
|
-
add_attribute(name,
|
341
|
+
add_attribute(name, attribute: :method, if: -> { !@object.new_record? }, **options, identifier: true)
|
224
342
|
end
|
225
343
|
|
226
344
|
# Public: Specify a collection of objects that should be serialized using
|
227
345
|
# the specified serializer.
|
228
|
-
def has_many(name, serializer:,
|
229
|
-
|
346
|
+
def has_many(name, serializer:, **options, &block)
|
347
|
+
define_method(name, &block) if block
|
348
|
+
add_attribute(name, association: :many, serializer: serializer, **options)
|
230
349
|
end
|
231
350
|
|
232
351
|
# Public: Specify an object that should be serialized using the serializer.
|
233
|
-
def has_one(name, serializer:,
|
234
|
-
|
352
|
+
def has_one(name, serializer:, **options, &block)
|
353
|
+
define_method(name, &block) if block
|
354
|
+
add_attribute(name, association: :one, serializer: serializer, **options)
|
235
355
|
end
|
356
|
+
# Alias: From a serializer perspective, the association type does not matter.
|
357
|
+
alias_method :belongs_to, :has_one
|
236
358
|
|
237
359
|
# Public: Specify an object that should be serialized using the serializer,
|
238
360
|
# but unlike `has_one`, this one will write the attributes directly without
|
@@ -243,9 +365,8 @@ protected
|
|
243
365
|
|
244
366
|
# Public: Specify which attributes are going to be obtained from indexing
|
245
367
|
# the object.
|
246
|
-
def hash_attributes(*
|
247
|
-
|
248
|
-
method_names.each { |name| _attributes[name] = options }
|
368
|
+
def hash_attributes(*attr_names, **options)
|
369
|
+
attributes(*attr_names, **options, attribute: :hash)
|
249
370
|
end
|
250
371
|
|
251
372
|
# Public: Specify which attributes are going to be obtained from indexing
|
@@ -254,48 +375,58 @@ protected
|
|
254
375
|
# Automatically renames `_id` to `id` for Mongoid models.
|
255
376
|
#
|
256
377
|
# See ./benchmarks/document_benchmark.rb
|
257
|
-
def mongo_attributes(*
|
258
|
-
|
259
|
-
|
378
|
+
def mongo_attributes(*attr_names, **options)
|
379
|
+
identifier(:_id, as: :id, attribute: :mongoid, **options) if attr_names.delete(:id)
|
380
|
+
attributes(*attr_names, **options, attribute: :mongoid)
|
260
381
|
end
|
261
382
|
|
262
383
|
# Public: Specify which attributes are going to be obtained by calling a
|
263
384
|
# method in the object.
|
264
|
-
def attributes(*
|
265
|
-
|
385
|
+
def attributes(*attr_names, **methods_with_options)
|
386
|
+
attr_options = methods_with_options.extract!(:if, :as, :attribute)
|
387
|
+
attr_options[:attribute] ||= :method
|
388
|
+
|
389
|
+
attr_names.each do |attr_name|
|
390
|
+
add_attribute(attr_name, **attr_options)
|
391
|
+
end
|
392
|
+
|
393
|
+
methods_with_options.each do |attr_name, options|
|
394
|
+
options = { as: options } if options.is_a?(Symbol)
|
395
|
+
add_attribute(attr_name, **options)
|
396
|
+
end
|
266
397
|
end
|
267
|
-
alias_method :attribute, :attributes
|
268
398
|
|
269
399
|
# Public: Specify which attributes are going to be obtained by calling a
|
270
400
|
# method in the serializer.
|
271
|
-
|
272
|
-
|
273
|
-
def serializer_attributes(*method_names, **options)
|
274
|
-
add_attributes(method_names, **options, attribute: :serializer)
|
401
|
+
def serializer_attributes(*attr_names, **options)
|
402
|
+
attributes(*attr_names, **options, attribute: :serializer)
|
275
403
|
end
|
276
404
|
|
277
405
|
# Syntax Sugar: Allows to use it before a method name.
|
278
406
|
#
|
279
407
|
# Example:
|
280
|
-
#
|
408
|
+
# attribute
|
281
409
|
# def full_name
|
282
410
|
# "#{ first_name } #{ last_name }"
|
283
411
|
# end
|
284
|
-
def
|
412
|
+
def attribute(name = nil, **options, &block)
|
413
|
+
options[:attribute] = :serializer
|
285
414
|
if name
|
286
|
-
|
415
|
+
define_method(name, &block) if block
|
416
|
+
add_attribute(name, **options)
|
287
417
|
else
|
288
|
-
@
|
418
|
+
@_current_attribute_options = options
|
289
419
|
end
|
290
420
|
end
|
421
|
+
alias_method :attr, :attribute
|
291
422
|
|
292
423
|
# Internal: Intercept a method definition, tying a type that was
|
293
424
|
# previously specified to the name of the attribute.
|
294
425
|
def method_added(name)
|
295
426
|
super(name)
|
296
|
-
if @
|
297
|
-
|
298
|
-
@
|
427
|
+
if @_current_attribute_options
|
428
|
+
add_attribute(name, **@_current_attribute_options)
|
429
|
+
@_current_attribute_options = nil
|
299
430
|
end
|
300
431
|
end
|
301
432
|
|
@@ -303,43 +434,34 @@ protected
|
|
303
434
|
# calling a method in the serializer, or using `read_attribute_for_serialization`.
|
304
435
|
#
|
305
436
|
# NOTE: Prefer to use `attributes` or `serializer_attributes` explicitly.
|
306
|
-
def ams_attributes(*
|
307
|
-
|
308
|
-
define_method(
|
437
|
+
def ams_attributes(*attr_names, **options)
|
438
|
+
attr_names.each do |attr_name|
|
439
|
+
define_method(attr_name) { @object.read_attribute_for_serialization(attr_name) } unless method_defined?(attr_name)
|
309
440
|
end
|
310
|
-
|
441
|
+
attributes(*attr_names, **options, attribute: :serializer)
|
311
442
|
end
|
312
443
|
|
313
|
-
|
314
|
-
#
|
315
|
-
# This setting is inherited from parent classes.
|
316
|
-
def _sort_attributes_by
|
317
|
-
@_sort_attributes_by = superclass.try(:_sort_attributes_by) unless defined?(@_sort_attributes_by)
|
318
|
-
@_sort_attributes_by
|
319
|
-
end
|
444
|
+
private
|
320
445
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
def _transform_keys
|
325
|
-
@_transform_keys = superclass.try(:_transform_keys) unless defined?(@_transform_keys)
|
326
|
-
@_transform_keys
|
327
|
-
end
|
446
|
+
def add_attribute(value_from, root: nil, as: nil, **options)
|
447
|
+
# Because it's so common, automatically mark id as an identifier.
|
448
|
+
options[:identifier] = true if value_from == :id && !options.key?(:identifier)
|
328
449
|
|
329
|
-
|
450
|
+
# Hash attributes could be numbers or symbols.
|
451
|
+
value_from = value_from.to_s unless options[:attribute] == :hash
|
330
452
|
|
331
|
-
|
332
|
-
|
333
|
-
end
|
453
|
+
# Obtain the JSON key to use for the attribute.
|
454
|
+
key = (root || as || value_from).to_s
|
334
455
|
|
335
|
-
|
336
|
-
_attributes
|
337
|
-
end
|
456
|
+
# Should be able to add "duplicate" flat associations.
|
457
|
+
key += _attributes.count.to_s if options[:association] == :flat
|
338
458
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
459
|
+
# Check for typos in options.
|
460
|
+
if DEV_MODE && (option, = options.find { |option, _value| !KNOWN_ATTRIBUTE_OPTIONS.include?(option) })
|
461
|
+
raise ArgumentError, "Unknown option #{option.inspect} for attribute #{value_from.inspect} in #{name}. Please check for typos."
|
462
|
+
end
|
463
|
+
|
464
|
+
_attributes[key.freeze] = { value_from: value_from, **options }.freeze
|
343
465
|
end
|
344
466
|
|
345
467
|
# Internal: Whether the object should be serialized as a collection.
|
@@ -355,19 +477,19 @@ protected
|
|
355
477
|
#
|
356
478
|
# As a result, the performance is the same as writing the most efficient
|
357
479
|
# code by hand.
|
358
|
-
def code_to_write_to_json
|
480
|
+
def code_to_write_to_json(attributes)
|
359
481
|
<<~WRITE_TO_JSON
|
360
482
|
# Public: Writes this serializer content to a provided Oj::StringWriter.
|
361
483
|
def write_to_json(writer, item, options = nil)
|
362
484
|
@object = item
|
363
485
|
@options = options
|
364
486
|
@memo.clear if defined?(@memo)
|
365
|
-
#{
|
366
|
-
|
487
|
+
#{ attributes.map { |key, options|
|
488
|
+
code_to_write_conditionally(options) {
|
367
489
|
if options[:association]
|
368
|
-
code_to_write_association(
|
490
|
+
code_to_write_association(key, options)
|
369
491
|
else
|
370
|
-
code_to_write_attribute(
|
492
|
+
code_to_write_attribute(key, options)
|
371
493
|
end
|
372
494
|
}
|
373
495
|
}.join("\n ") }#{code_to_rescue_no_method if DEV_MODE}
|
@@ -381,7 +503,7 @@ protected
|
|
381
503
|
#
|
382
504
|
# As a result, the performance is the same as writing the most efficient
|
383
505
|
# code by hand.
|
384
|
-
def code_to_render_as_hash
|
506
|
+
def code_to_render_as_hash(attributes)
|
385
507
|
<<~RENDER_AS_HASH
|
386
508
|
# Public: Writes this serializer content to a Hash.
|
387
509
|
def render_as_hash(item, options = nil)
|
@@ -389,12 +511,12 @@ protected
|
|
389
511
|
@options = options
|
390
512
|
@memo.clear if defined?(@memo)
|
391
513
|
{
|
392
|
-
#{
|
393
|
-
code_to_render_conditionally(
|
514
|
+
#{attributes.map { |key, options|
|
515
|
+
code_to_render_conditionally(options) {
|
394
516
|
if options[:association]
|
395
|
-
code_to_render_association(
|
517
|
+
code_to_render_association(key, options)
|
396
518
|
else
|
397
|
-
code_to_render_attribute(
|
519
|
+
code_to_render_attribute(key, options)
|
398
520
|
end
|
399
521
|
}
|
400
522
|
}.join(",\n ")}
|
@@ -409,7 +531,7 @@ protected
|
|
409
531
|
rescue NoMethodError => e
|
410
532
|
key = e.name.to_s.inspect
|
411
533
|
message = if respond_to?(e.name)
|
412
|
-
raise e, "Perhaps you meant to call \#{key} in \#{self.class} instead?\nTry using `
|
534
|
+
raise e, "Perhaps you meant to call \#{key} in \#{self.class} instead?\nTry using `attribute :\#{key} do` or `attribute def \#{key}`.\n\#{e.message}"
|
413
535
|
elsif @object.respond_to?(e.name)
|
414
536
|
raise e, "Perhaps you meant to call \#{key} in \#{@object.class} instead?\nTry using `attributes :\#{key}`.\n\#{e.message}"
|
415
537
|
else
|
@@ -420,18 +542,28 @@ protected
|
|
420
542
|
RESCUE_NO_METHOD
|
421
543
|
end
|
422
544
|
|
545
|
+
# Internal: Detects any include methods defined in the serializer, or defines
|
546
|
+
# one by using the lambda passed in the `if` option, if any.
|
547
|
+
def check_conditional_method(options)
|
548
|
+
value_from = options.fetch(:value_from)
|
549
|
+
include_method_name = "include_#{value_from}#{'?' unless value_from.to_s.ends_with?('?')}"
|
550
|
+
if render_if = options[:if]
|
551
|
+
if render_if.is_a?(Symbol)
|
552
|
+
alias_method(include_method_name, render_if)
|
553
|
+
else
|
554
|
+
define_method(include_method_name, &render_if)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
include_method_name if method_defined?(include_method_name)
|
558
|
+
end
|
559
|
+
|
423
560
|
# Internal: Returns the code to render an attribute or association
|
424
561
|
# conditionally.
|
425
562
|
#
|
426
563
|
# NOTE: Detects any include methods defined in the serializer, or defines
|
427
564
|
# one by using the lambda passed in the `if` option, if any.
|
428
|
-
def
|
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)
|
565
|
+
def code_to_write_conditionally(options)
|
566
|
+
if (include_method_name = check_conditional_method(options))
|
435
567
|
"if #{include_method_name};#{yield};end\n"
|
436
568
|
else
|
437
569
|
yield
|
@@ -439,56 +571,52 @@ protected
|
|
439
571
|
end
|
440
572
|
|
441
573
|
# Internal: Returns the code for the association method.
|
442
|
-
def code_to_write_attribute(
|
443
|
-
|
574
|
+
def code_to_write_attribute(key, options)
|
575
|
+
value_from = options.fetch(:value_from)
|
444
576
|
|
445
|
-
case strategy = options.fetch(:attribute)
|
577
|
+
value = case (strategy = options.fetch(:attribute))
|
446
578
|
when :serializer
|
447
579
|
# Obtains the value by calling a method in the serializer.
|
448
|
-
|
580
|
+
value_from
|
449
581
|
when :method
|
450
582
|
# Obtains the value by calling a method in the object, and writes it.
|
451
|
-
"
|
583
|
+
"@object.#{value_from}"
|
452
584
|
when :hash
|
453
585
|
# Writes a Hash value to JSON, works with String or Symbol keys.
|
454
|
-
"
|
586
|
+
"@object[#{value_from.inspect}]"
|
455
587
|
when :mongoid
|
456
588
|
# Writes an Mongoid attribute to JSON, this is the fastest strategy.
|
457
|
-
"
|
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?"
|
589
|
+
"@object.attributes['#{value_from}']"
|
464
590
|
else
|
465
591
|
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
466
592
|
end
|
593
|
+
|
594
|
+
"writer.push_value(#{value}, #{key.inspect})"
|
467
595
|
end
|
468
596
|
|
469
597
|
# Internal: Returns the code for the association method.
|
470
|
-
def code_to_write_association(
|
598
|
+
def code_to_write_association(key, options)
|
471
599
|
# Use a serializer method if defined, else call the association in the object.
|
472
|
-
|
473
|
-
|
600
|
+
value_from = options.fetch(:value_from)
|
601
|
+
value = method_defined?(value_from) ? value_from : "@object.#{value_from}"
|
474
602
|
serializer_class = options.fetch(:serializer)
|
475
603
|
|
476
604
|
case type = options.fetch(:association)
|
477
605
|
when :one
|
478
606
|
<<~WRITE_ONE
|
479
|
-
if
|
607
|
+
if __value = #{value}
|
480
608
|
writer.push_key('#{key}')
|
481
|
-
#{serializer_class}.write_one(writer,
|
609
|
+
#{serializer_class}.write_one(writer, __value)
|
482
610
|
end
|
483
611
|
WRITE_ONE
|
484
612
|
when :many
|
485
613
|
<<~WRITE_MANY
|
486
614
|
writer.push_key('#{key}')
|
487
|
-
#{serializer_class}.write_many(writer, #{
|
615
|
+
#{serializer_class}.write_many(writer, #{value})
|
488
616
|
WRITE_MANY
|
489
617
|
when :flat
|
490
618
|
<<~WRITE_FLAT
|
491
|
-
#{serializer_class}.write_to_json(writer, #{
|
619
|
+
#{serializer_class}.write_to_json(writer, #{value})
|
492
620
|
WRITE_FLAT
|
493
621
|
else
|
494
622
|
raise ArgumentError, "Unknown association type: #{type.inspect}"
|
@@ -500,14 +628,8 @@ protected
|
|
500
628
|
#
|
501
629
|
# NOTE: Detects any include methods defined in the serializer, or defines
|
502
630
|
# one by using the lambda passed in the `if` option, if any.
|
503
|
-
def code_to_render_conditionally(
|
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)
|
631
|
+
def code_to_render_conditionally(options)
|
632
|
+
if (include_method_name = check_conditional_method(options))
|
511
633
|
"**(#{include_method_name} ? {#{yield}} : {})"
|
512
634
|
else
|
513
635
|
yield
|
@@ -515,38 +637,39 @@ protected
|
|
515
637
|
end
|
516
638
|
|
517
639
|
# Internal: Returns the code for the attribute method.
|
518
|
-
def code_to_render_attribute(
|
519
|
-
|
520
|
-
|
640
|
+
def code_to_render_attribute(key, options)
|
641
|
+
value_from = options.fetch(:value_from)
|
642
|
+
|
643
|
+
value = case (strategy = options.fetch(:attribute))
|
521
644
|
when :serializer
|
522
|
-
|
645
|
+
value_from
|
523
646
|
when :method
|
524
|
-
"
|
647
|
+
"@object.#{value_from}"
|
525
648
|
when :hash
|
526
|
-
"
|
649
|
+
"@object[#{value_from.inspect}]"
|
527
650
|
when :mongoid
|
528
|
-
"
|
529
|
-
when :id
|
530
|
-
"**(@object.new_record? ? {} : {id: @object.attributes['_id']})"
|
651
|
+
"@object.attributes['#{value_from}']"
|
531
652
|
else
|
532
653
|
raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
|
533
654
|
end
|
655
|
+
|
656
|
+
"#{key}: #{value}"
|
534
657
|
end
|
535
658
|
|
536
659
|
# Internal: Returns the code for the association method.
|
537
|
-
def code_to_render_association(
|
660
|
+
def code_to_render_association(key, options)
|
538
661
|
# Use a serializer method if defined, else call the association in the object.
|
539
|
-
|
540
|
-
|
662
|
+
value_from = options.fetch(:value_from)
|
663
|
+
value = method_defined?(value_from) ? value_from : "@object.#{value_from}"
|
541
664
|
serializer_class = options.fetch(:serializer)
|
542
665
|
|
543
666
|
case type = options.fetch(:association)
|
544
667
|
when :one
|
545
|
-
"#{key}: (
|
668
|
+
"#{key}: (__value = #{value}) ? #{serializer_class}.one_as_hash(__value) : nil"
|
546
669
|
when :many
|
547
|
-
"#{key}: #{serializer_class}.many_as_hash(#{
|
670
|
+
"#{key}: #{serializer_class}.many_as_hash(#{value})"
|
548
671
|
when :flat
|
549
|
-
"**#{serializer_class}.one_as_hash(#{
|
672
|
+
"**#{serializer_class}.one_as_hash(#{value})"
|
550
673
|
else
|
551
674
|
raise ArgumentError, "Unknown association type: #{type.inspect}"
|
552
675
|
end
|
@@ -567,25 +690,36 @@ protected
|
|
567
690
|
@instance_key ||= begin
|
568
691
|
# We take advantage of the fact that this method will always be called
|
569
692
|
# before instantiating a serializer, to apply last minute adjustments.
|
570
|
-
|
693
|
+
prepare_serializer
|
571
694
|
"#{name.underscore}_instance_#{object_id}".to_sym
|
572
695
|
end
|
573
696
|
end
|
574
697
|
|
575
698
|
# Internal: Generates write_to_json and render_as_hash methods optimized for
|
576
699
|
# the specified configuration.
|
577
|
-
def
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
700
|
+
def prepare_serializer
|
701
|
+
attributes = prepare_attributes
|
702
|
+
class_eval(code_to_write_to_json(attributes))
|
703
|
+
class_eval(code_to_render_as_hash(attributes))
|
704
|
+
end
|
705
|
+
|
706
|
+
# Internal: Returns attributes sorted and with keys transformed using
|
707
|
+
# the specified strategies.
|
708
|
+
def prepare_attributes(transform_keys: try(:_transform_keys), sort_by: try(:_sort_attributes_by))
|
709
|
+
attributes = _attributes
|
710
|
+
attributes = attributes.transform_keys(&transform_keys) if transform_keys
|
711
|
+
|
712
|
+
if sort_by == :name
|
713
|
+
sort_by = ->(name, options, _) { options[:identifier] ? "__#{name}" : name }
|
714
|
+
elsif !sort_by || sort_by == :definition
|
715
|
+
sort_by = ->(name, options, index) { options[:identifier] ? "__#{name}" : "zzz#{index}" }
|
582
716
|
end
|
583
|
-
|
584
|
-
|
717
|
+
|
718
|
+
attributes.sort_by.with_index { |(name, options), index| sort_by.call(name, options, index) }.to_h
|
585
719
|
end
|
586
720
|
end
|
587
721
|
|
588
|
-
|
722
|
+
default_format :hash
|
589
723
|
end
|
590
724
|
|
591
725
|
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.
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Maximo Mussini
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-02 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:
|