oj_serializers 2.0.0.pre.beta.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: