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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ebc24ba352b46585589ae760b579009e18e9de772b1d015eb0fdfa465a39b93
4
- data.tar.gz: 475283ebe5897716eb1812a474358ddc3775746e1e0f8ff011204fc3ff76219e
3
+ metadata.gz: e8fd87262444d89cfbbe11a1b6ae6201de6d71ed996edcd8a4a9ae224320ad7c
4
+ data.tar.gz: bfe507b6e02c01087d99381ab8ef971547e0062085eb4bfb9c9551ceb2796c18
5
5
  SHA512:
6
- metadata.gz: e4a0c4e060fbac9cc04ef2e6ce31406fa5aa83052989a272e2bba6c1b7c30767e82bd22aa51b016716adfff8bb039442a7725f25ddf7093ca4b33de646360181
7
- data.tar.gz: d0d41a7a283e26831222a99b96669498eb01b14dac383da008db053c769052ca0e91b1078f1c0d9c797faadd082b3ecaeabce37fa267d617bc2bc039a05dcaec
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/master/LICENSE.txt"><img alt="License" src="https://img.shields.io/badge/license-MIT-428F7E.svg"/></a>
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/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
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
- serialize
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
- <br/>
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
- 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`.
158
+ ## Rendering 🖨
180
159
 
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:
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
- You can use these serializers inside arrays, hashes, or even inside `ActiveModel::Serializer` by using a method in the serializer.
193
-
194
- ## Attributes DSL 🛠
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
- 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.
173
+ You can use `render` as a shortcut for `one` and `many`, but it might be less readable:
198
174
 
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 😃
175
+ ```ruby
176
+ render json: {
177
+ favorite_album: AlbumSerializer.render(album),
178
+ purchased_albums: AlbumSerializer.render(albums),
179
+ }
180
+ ```
201
181
 
202
- ### `attributes`
182
+ ## Attributes DSL 🪄
203
183
 
204
- Obtains the attribute value by calling a method in the object being serialized.
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
- 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:
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
- serialize
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
- Instance methods can access the object by the serializer name without the
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
- def release
248
- album.release_date.strftime('%B %d, %Y')
202
+ attribute
203
+ def name
204
+ "#{player.first_name} #{player.last_name}"
249
205
  end
250
206
  end
251
207
  ```
252
208
 
253
- Should only be used when migrating from Active Model Serializers, as it's slower and can create confusion.
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
- Please refer to the [migration guide] for more information.
216
+ ### Associations 🔗
258
217
 
259
- ### `hash_attributes` 🚀
218
+ Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
260
219
 
261
- Very convenient when serializing Hash-like structures, this strategy uses the `[]` operator.
220
+ You must specificy which serializer to use with the `serializer` option.
262
221
 
263
222
  ```ruby
264
- class PersonSerializer < Oj::Serializer
265
- hash_attributes 'first_name', :last_name
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
- ### `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.
229
+ Specify a different value for the association by providing a block:
280
230
 
281
231
  ```ruby
282
- class AlbumSerializer < Oj::Serializer
283
- mongo_attributes :id, :name
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
- ## Associations DSL 🛠
239
+ In case you need to pass options, you can call the serializer manually:
288
240
 
289
- Use `has_one` to serialize individual objects, and `has_many` to serialize a collection.
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
- The value for the association is obtained from a serializer method if defined,
292
- or by calling the method in the object being serialized.
249
+ ### Aliasing or renaming attributes ↔️
293
250
 
294
- You must specificy which serializer to use with the `serializer` option.
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
- has_many :composers, serializer: ComposerSerializer
256
+ has_one :album, as: :first_release, serializer: AlbumSerializer
257
+
258
+ attributes title: {as: :name}
300
259
 
301
- # You can also compose serializers using `flat_one`.
302
- flat_one :song, serializer: SongMetadataSerializer
260
+ # or as a shortcut
261
+ attributes title: :name
303
262
  end
304
263
  ```
305
264
 
306
- The associations DSL is essentially a shortcut for defining attributes manually:
265
+ ### Conditional attributes
266
+
267
+ You can render attributes and associations conditionally by using `:if`.
307
268
 
308
269
  ```ruby
309
- class SongSerializer < SongMetadataSerializer
310
- serialize
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
- serialize
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
- ## Other DSL 🛠
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
- You can use `object_as` to create an alias for the serialized object to access it from instance methods:
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
- serialize
292
+ attribute
334
293
  def latest_albums
335
294
  artist.albums.desc(:year)
336
295
  end
337
296
  end
338
297
  ```
339
298
 
340
- ### Aliasing or renaming attributes
299
+ ### Identifier attributes
341
300
 
342
- You can pass `as` when defining an attribute or association to serialize it
343
- using a different key:
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 SongSerializer < Oj::Serializer
347
- has_one :album, as: :latest_album, serializer: AlbumSerializer
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
- attribute :title, as: :name
330
+ # shortcut for
331
+ transform_keys -> (key) { key.to_s.camelize(:lower) }
350
332
  end
351
333
  ```
352
334
 
353
- ### Rendering an attribute conditionally
335
+ This has no performance impact, as keys will be transformed at load time.
354
336
 
355
- All the attributes and association methods can take an `if` option to render conditionally.
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 AlbumSerializer < Oj::Serializer
359
- mongo_attributes :release_date, if: -> { album.released? }
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
- has_many :songs, serializer: SongSerializer, if: -> { album.songs.any? }
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
- # You can achieve the same by manually defining a method:
364
- def include_songs?
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
- ### Memoization & Local State
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
- Serializers are designed to be stateless so that an instanced can be reused, but sometimes it's convenient to store intermediate calculations.
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
- serialize
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
- ### Writing directly to JSON
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
- In some corner cases it might be faster to serialize using a `Oj::StringWriter`.
420
+ ### `mongo_attributes` 🚀
398
421
 
399
- You can toggle this mode at a serializer level by using `default_format :json`,
400
- or configure it globally from your base serializer.
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 # :hash is the default
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
- Follow [this discussion][raw_json] to find out more about [the `raw_json` extensions][raw_json] that made this high level of interoperability possible.
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[memo object options]
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
- def default_format(value)
89
- define_serialization_shortcuts(value)
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
- def sort_attributes_by(value)
94
- @_sort_attributes_by = case value
95
- when :name then ->(name, options) { options[:identifier] ? "__#{name}" : name }
96
- when Proc then value
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: #{value.inspect}"
119
+ raise ArgumentError, "Unknown sorting option: #{strategy.inspect}"
99
120
  end
100
121
  end
101
122
 
102
- # Public: Allows to sort fields by name instead.
103
- def transform_keys(transformer = nil, &block)
104
- @_transform_keys = case (transformer ||= block)
105
- when :camelize, :camel_case then ->(key) { key.to_s.camelize(:lower) }
106
- when Symbol then transformer.to_proc
107
- when Proc then transformer
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: #{transformer.inspect}")
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
- def define_serialization_shortcuts(format)
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, **options, attribute: :method, identifier: true, if: -> { !@object.new_record? })
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:, root: name, as: root, **options)
229
- add_attribute(name, association: :many, as: as, serializer: serializer, **options)
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:, root: name, as: root, **options)
234
- add_attribute(name, association: :one, as: as, serializer: serializer, **options)
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(*method_names, **options)
247
- options = { **options, attribute: :hash }
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(*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)
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(*method_names, **options)
265
- add_attributes(method_names, **options, attribute: :method)
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
- # NOTE: This can be one of the slowest strategies, when in doubt, measure.
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
- # serialize
408
+ # attribute
281
409
  # def full_name
282
410
  # "#{ first_name } #{ last_name }"
283
411
  # end
284
- def serialize(name = nil, **options)
412
+ def attribute(name = nil, **options, &block)
413
+ options[:attribute] = :serializer
285
414
  if name
286
- serializer_attributes(name, **options)
415
+ define_method(name, &block) if block
416
+ add_attribute(name, **options)
287
417
  else
288
- @_current_attribute = options
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 @_current_attribute
297
- serializer_attributes(name, **@_current_attribute)
298
- @_current_attribute = nil
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(*method_names, **options)
307
- method_names.each do |method_name|
308
- define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
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
- add_attributes(method_names, **options, attribute: :serializer)
441
+ attributes(*attr_names, **options, attribute: :serializer)
311
442
  end
312
443
 
313
- # Internal: The strategy to use when sorting the fields.
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
- # Internal: The converter to use for serializer keys.
322
- #
323
- # This setting is inherited from parent classes.
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
- private
450
+ # Hash attributes could be numbers or symbols.
451
+ value_from = value_from.to_s unless options[:attribute] == :hash
330
452
 
331
- def add_attributes(names, options)
332
- names.each { |name| add_attribute(name, options) }
333
- end
453
+ # Obtain the JSON key to use for the attribute.
454
+ key = (root || as || value_from).to_s
334
455
 
335
- def add_attribute(name, options)
336
- _attributes[name.to_s.freeze] = options
337
- end
456
+ # Should be able to add "duplicate" flat associations.
457
+ key += _attributes.count.to_s if options[:association] == :flat
338
458
 
339
- # Internal: Transforms the keys using the provided strategy.
340
- def key_for(method_name, options)
341
- key = options.fetch(:as, method_name)
342
- _transform_keys ? _transform_keys.call(key) : key
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
- #{ _attributes.map { |method_name, options|
366
- code_to_write_conditional(method_name, options) {
487
+ #{ attributes.map { |key, options|
488
+ code_to_write_conditionally(options) {
367
489
  if options[:association]
368
- code_to_write_association(method_name, options)
490
+ code_to_write_association(key, options)
369
491
  else
370
- code_to_write_attribute(method_name, options)
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
- #{_attributes.map { |method_name, options|
393
- code_to_render_conditionally(method_name, options) {
514
+ #{attributes.map { |key, options|
515
+ code_to_render_conditionally(options) {
394
516
  if options[:association]
395
- code_to_render_association(method_name, options)
517
+ code_to_render_association(key, options)
396
518
  else
397
- code_to_render_attribute(method_name, options)
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 `serializer_attributes :\#{key}` or `attribute def \#{key}`.\n\#{e.message}"
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 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)
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(method_name, options)
443
- key = key_for(method_name, options).to_s.inspect
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
- "writer.push_value(#{method_name}, #{key})"
580
+ value_from
449
581
  when :method
450
582
  # Obtains the value by calling a method in the object, and writes it.
451
- "writer.push_value(@object.#{method_name}, #{key})"
583
+ "@object.#{value_from}"
452
584
  when :hash
453
585
  # Writes a Hash value to JSON, works with String or Symbol keys.
454
- "writer.push_value(@object[#{method_name.inspect}], #{key})"
586
+ "@object[#{value_from.inspect}]"
455
587
  when :mongoid
456
588
  # Writes an Mongoid attribute to JSON, this is the fastest strategy.
457
- "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?"
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(method_name, options)
598
+ def code_to_write_association(key, options)
471
599
  # Use a serializer method if defined, else call the association in the object.
472
- association_method = method_defined?(method_name) ? method_name : "@object.#{method_name}"
473
- key = key_for(method_name, options)
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 associated_object = #{association_method}
607
+ if __value = #{value}
480
608
  writer.push_key('#{key}')
481
- #{serializer_class}.write_one(writer, associated_object)
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, #{association_method})
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, #{association_method})
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(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)
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(method_name, options)
519
- key = key_for(method_name, options)
520
- case strategy = options.fetch(:attribute)
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
- "#{key}: #{method_name}"
645
+ value_from
523
646
  when :method
524
- "#{key}: @object.#{method_name}"
647
+ "@object.#{value_from}"
525
648
  when :hash
526
- "#{key}: @object[#{method_name.inspect}]"
649
+ "@object[#{value_from.inspect}]"
527
650
  when :mongoid
528
- "#{key}: @object.attributes['#{method_name}']"
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(method_name, options)
660
+ def code_to_render_association(key, options)
538
661
  # Use a serializer method if defined, else call the association in the object.
539
- association = method_defined?(method_name) ? method_name : "@object.#{method_name}"
540
- key = key_for(method_name, options)
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}: (one_item = #{association}) ? #{serializer_class}.one_as_hash(one_item) : nil"
668
+ "#{key}: (__value = #{value}) ? #{serializer_class}.one_as_hash(__value) : nil"
546
669
  when :many
547
- "#{key}: #{serializer_class}.many_as_hash(#{association})"
670
+ "#{key}: #{serializer_class}.many_as_hash(#{value})"
548
671
  when :flat
549
- "**#{serializer_class}.one_as_hash(#{association})"
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
- _prepare_serializer
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 _prepare_serializer
578
- if _sort_attributes_by
579
- @_attributes = _attributes.sort_by { |key, options|
580
- _sort_attributes_by.call(key, options)
581
- }.to_h
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
- class_eval(code_to_write_to_json)
584
- class_eval(code_to_render_as_hash)
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
- define_serialization_shortcuts(:hash)
722
+ default_format :hash
589
723
  end
590
724
 
591
725
  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.1'
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.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-03-20 00:00:00.000000000 Z
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: 1.3.1
69
+ version: '0'
70
70
  requirements: []
71
71
  rubygems_version: 3.2.32
72
72
  signing_key: