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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ebc24ba352b46585589ae760b579009e18e9de772b1d015eb0fdfa465a39b93
4
- data.tar.gz: 475283ebe5897716eb1812a474358ddc3775746e1e0f8ff011204fc3ff76219e
3
+ metadata.gz: 281e961cd6a9da8c3723dd56ce7e1aa8ec112c57d13ab15e763ecef98eb1f659
4
+ data.tar.gz: cd9a42db1ea64aac9c75755550ce0bb3123646ac85020056a4d3bc5dae7e9251
5
5
  SHA512:
6
- metadata.gz: e4a0c4e060fbac9cc04ef2e6ce31406fa5aa83052989a272e2bba6c1b7c30767e82bd22aa51b016716adfff8bb039442a7725f25ddf7093ca4b33de646360181
7
- data.tar.gz: d0d41a7a283e26831222a99b96669498eb01b14dac383da008db053c769052ca0e91b1078f1c0d9c797faadd082b3ecaeabce37fa267d617bc2bc039a05dcaec
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/master/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
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/master/benchmarks/document_benchmark.rb
23
- [sugar]: https://github.com/ElMassimo/oj_serializers/blob/master/lib/oj_serializers/sugar.rb#L14
24
- [migration guide]: https://github.com/ElMassimo/oj_serializers/blob/master/MIGRATION_GUIDE.md
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
- serialize
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
- <br/>
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
- If you are using Rails you can also use something closer to Active Model Serializers by adding [`sugar`][sugar]:
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
- ## Render DSL 🛠
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
- You can use these serializers inside arrays, hashes, or even inside `ActiveModel::Serializer` by using a method in the serializer.
193
-
194
- ## Attributes DSL 🛠
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
- Attributes methods can be used to define which model attributes should be serialized
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
- The internal design is simple and extensible, so creating new strategies requires very little code.
200
- Please open an issue if you need help 😃
174
+ ```ruby
175
+ render json: {
176
+ favorite_album: AlbumSerializer.render(album),
177
+ purchased_albums: AlbumSerializer.render(albums),
178
+ }
179
+ ```
201
180
 
202
- ### `attributes`
181
+ ## Attributes DSL 🪄
203
182
 
204
- Obtains the attribute value by calling a method in the object being serialized.
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
- Have in mind that unlike Active Model Serializers, it will _not_ take into
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
- serialize
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
- Instance methods can access the object by the serializer name without the
235
- `Serializer` suffix, `player` in the example above, or directly as `@object`.
199
+ # or
236
200
 
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
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
- Should only be used when migrating from Active Model Serializers, as it's slower and can create confusion.
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
- Please refer to the [migration guide] for more information.
215
+ ### Associations 🔗
258
216
 
259
- ### `hash_attributes` 🚀
217
+ Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
260
218
 
261
- Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
219
+ You must specificy which serializer to use with the `serializer` option.
262
220
 
263
221
  ```ruby
264
- class PersonSerializer < Oj::Serializer
265
- hash_attributes 'first_name', :last_name
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
- ### `mongo_attributes` 🚀
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 AlbumSerializer < Oj::Serializer
283
- mongo_attributes :id, :name
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
- ## Associations DSL 🛠
238
+ In case you need to pass options, you can call the serializer manually:
288
239
 
289
- Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
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
- The value for the association is obtained from a serializer method if defined,
292
- or by calling the method in the object being serialized.
248
+ ### Aliasing or renaming attributes ↔️
293
249
 
294
- You must specificy which serializer to use with the `serializer` option.
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
- # You can also compose serializers using `flat_one`.
302
- flat_one :song, serializer: SongMetadataSerializer
257
+ attributes title: {as: :name}
258
+
259
+ # or as a shortcut
260
+ attributes title: :name
303
261
  end
304
262
  ```
305
263
 
306
- The associations DSL is essentially a shortcut for defining attributes manually:
264
+ ### Conditional attributes
265
+
266
+ You can render attributes and associations conditionally by using `:if`.
307
267
 
308
268
  ```ruby
309
- class SongSerializer < SongMetadataSerializer
310
- serialize
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
- serialize
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
- ## Other DSL 🛠
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
- You can use `object_as` to create an alias for the serialized object to access it from instance methods:
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
- serialize
291
+ attribute
334
292
  def latest_albums
335
293
  artist.albums.desc(:year)
336
294
  end
337
295
  end
338
296
  ```
339
297
 
340
- ### Aliasing or renaming attributes
298
+ ### Identifier attributes
341
299
 
342
- You can pass `as` when defining an attribute or association to serialize it
343
- using a different key:
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 SongSerializer < Oj::Serializer
347
- has_one :album, as: :latest_album, serializer: AlbumSerializer
304
+ class AlbumSerializer < Oj::Serializer
305
+ identifier
348
306
 
349
- attribute :title, as: :name
307
+ # or if it's a different field
308
+ identifier :uuid
350
309
  end
351
310
  ```
352
311
 
353
- ### Rendering an attribute conditionally
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
- All the attributes and association methods can take an `if` option to render conditionally.
323
+ Use `transform_keys` to handle that conversion.
356
324
 
357
325
  ```ruby
358
- class AlbumSerializer < Oj::Serializer
359
- mongo_attributes :release_date, if: -> { album.released? }
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
- has_many :songs, serializer: SongSerializer, if: -> { album.songs.any? }
351
+ ### Path helpers 🛣
362
352
 
363
- # You can achieve the same by manually defining a method:
364
- def include_songs?
365
- album.songs.any?
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
- ### Memoization & Local State
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 sometimes it's convenient to store intermediate calculations.
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
- serialize
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
- ### Writing directly to JSON
396
+ ### `hash_attributes` 🚀
396
397
 
397
- In some corner cases it might be faster to serialize using a `Oj::StringWriter`.
398
+ Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
398
399
 
399
- You can toggle this mode at a serializer level by using `default_format :json`,
400
- or configure it globally from your base serializer.
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 # :hash is the default
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
- Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
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
- define_serialization_shortcuts(value)
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
- def define_serialization_shortcuts(format)
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, **options, attribute: :method, identifier: true, if: -> { !@object.new_record? })
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
- add_attribute('id', **options, attribute: :id, identifier: true) if method_names.delete(:id)
259
- add_attributes(method_names, **options, attribute: :mongoid)
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, **options)
265
- add_attributes(method_names, **options, attribute: :method)
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
- add_attributes(method_names, **options, attribute: :serializer)
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
- # serialize
384
+ # attribute
281
385
  # def full_name
282
386
  # "#{ first_name } #{ last_name }"
283
387
  # end
284
- def serialize(name = nil, **options)
388
+ def attribute(name = nil, **options, &block)
389
+ options[:attribute] = :serializer
285
390
  if name
286
- serializer_attributes(name, **options)
391
+ define_method(name, &block) if block
392
+ add_attribute(name, options)
287
393
  else
288
- @_current_attribute = options
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 @_current_attribute
297
- serializer_attributes(name, **@_current_attribute)
298
- @_current_attribute = nil
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
- add_attributes(method_names, **options, attribute: :serializer)
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 = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
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 = "include_#{method_name}#{'?' unless method_name.ends_with?('?')}"
505
-
506
- if render_if = options[:if]
507
- define_method(include_method_name, &render_if)
508
- end
509
-
510
- if method_defined?(include_method_name)
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(:hash)
692
+ define_serialization_shortcuts
589
693
  end
590
694
 
591
695
  Oj::Serializer = OjSerializers::Serializer unless defined?(Oj::Serializer)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OjSerializers
4
- VERSION = '2.0.0-beta.1'
4
+ VERSION = '2.0.0'
5
5
  end
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.pre.beta.1
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-20 00:00:00.000000000 Z
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: 1.3.1
69
+ version: '0'
70
70
  requirements: []
71
71
  rubygems_version: 3.2.32
72
72
  signing_key: