oj_serializers 2.0.0 → 2.0.2

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: 281e961cd6a9da8c3723dd56ce7e1aa8ec112c57d13ab15e763ecef98eb1f659
4
- data.tar.gz: cd9a42db1ea64aac9c75755550ce0bb3123646ac85020056a4d3bc5dae7e9251
3
+ metadata.gz: f1a6edc7a526ccc3e6edd655070c28a9c54f2fd38646aaf36bf4db0b58363f36
4
+ data.tar.gz: 5a2541b9005c35d1f4c68a0dc142691b35199c78e0e134f6cc44ad0cd6a3a8bc
5
5
  SHA512:
6
- metadata.gz: c3816ef19190cf4dbddfa942d205b21cb0d639ed33cf9ff34a9972038179d677cfabe659fc36f1780e6fb64fb8c7145a756012bfc1a8de3bc4cb0256e419917a
7
- data.tar.gz: 364da01e189d7c00b4abcbaa490c2e8a0432acf9f126a0db67ccab3d1a2c7a1b1724267ec084980ba706ad43fb89c1964749a4684736d72888f6ddb5fe970f3f
6
+ metadata.gz: 4ee1d2c5e850cd14548dbf1219056f61b25be85a55a83107bc6cf695b991cb52bb54605c45c88aed18e3573e53c72f22bbedbfa7a2bd066e10a17d71a1e25bd1
7
+ data.tar.gz: f8454069164b46838efecc9652e5a56fa816a07e47e7ad1983be49147c469f2c58bcdb67ab54ebb59bbcdd381abe0ee7ff7240ca282f310e581fc8dc0a275a0f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## Oj Serializers 2.0.2 (2023-04-02)
2
+
3
+ ### Features ✨
4
+
5
+ - [Automatically remove `?` when using `transform_keys :camelize`](https://github.com/ElMassimo/oj_serializers/commit/79758a0)
6
+
7
+ ### Fixes 🐞
8
+
9
+ - [Error when defining attributes with options](https://github.com/ElMassimo/oj_serializers/commit/680ab47)
10
+
11
+ ## Oj Serializers 2.0.1 (2023-04-02)
12
+
13
+ ### Features ✨
14
+
15
+ - [Automatically mark `id` as an identifier (rendered first)](https://github.com/ElMassimo/oj_serializers/commit/c4c6de7)
16
+ - [Fail on typos in attribute and association options](https://github.com/ElMassimo/oj_serializers/commit/afd80ac)
17
+
18
+ ### Fixes 🐞
19
+
20
+ - [Aliased attributes should be sorted by the output key](https://github.com/ElMassimo/oj_serializers/commit/fc6f4c1)
21
+
1
22
  ## [Oj Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9)
2
23
 
3
24
  ### Features ✨
data/README.md CHANGED
@@ -2,7 +2,6 @@
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>
@@ -23,11 +22,15 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar
23
22
  [sugar]: https://github.com/ElMassimo/oj_serializers/blob/main/lib/oj_serializers/sugar.rb#L14
24
23
  [migration guide]: https://github.com/ElMassimo/oj_serializers/blob/main/MIGRATION_GUIDE.md
25
24
  [design]: https://github.com/ElMassimo/oj_serializers#design-
25
+ [associations]: https://github.com/ElMassimo/oj_serializers#associations-
26
+ [compose]: https://github.com/ElMassimo/oj_serializers#composing-serializers-
26
27
  [raw_json]: https://github.com/ohler55/oj/issues/542
27
28
  [trailing_commas]: https://maximomussini.com/posts/trailing-commas/
28
29
  [render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
29
30
  [sorbet]: https://sorbet.org/
30
31
  [Discussion]: https://github.com/ElMassimo/oj_serializers/discussions
32
+ [TypeScript]: https://www.typescriptlang.org/
33
+ [types_from_serializers]: https://github.com/ElMassimo/types_from_serializers
31
34
 
32
35
  ## Why? 🤔
33
36
 
@@ -40,11 +43,12 @@ Learn more about [how this library achieves its performance][design].
40
43
 
41
44
  ## Features ⚡️
42
45
 
43
- - Declaration syntax [similar to Active Model Serializers][migration guide]
46
+ - Intuitive declaration syntax, supporting mixins and inheritance
44
47
  - Reduced [memory allocation][benchmarks] and [improved performance][benchmarks]
45
- - Support for `has_one` and `has_many`, compose with `flat_one`
48
+ - Generate [TypeScript interfaces automatically][types_from_serializers]
49
+ - Support for [`has_one`][associations] and [`has_many`][associations], compose with [`flat_one`][compose]
46
50
  - Useful development checks to avoid typos and mistakes
47
- - Integrates nicely with Rails controllers
51
+ - [Migrate easily from Active Model Serializers][migration guide]
48
52
 
49
53
  ## Installation 💿
50
54
 
@@ -67,8 +71,7 @@ attributes should be serialized.
67
71
  class AlbumSerializer < Oj::Serializer
68
72
  attributes :name, :genres
69
73
 
70
- attr
71
- def release
74
+ attribute :release do
72
75
  album.release_date.strftime('%B %d, %Y')
73
76
  end
74
77
 
@@ -154,6 +157,24 @@ class AlbumsController < ApplicationController
154
157
  end
155
158
  ```
156
159
 
160
+ <details>
161
+ <summary>Active Model Serializers style</summary>
162
+
163
+ ```ruby
164
+ require "oj_serializers/sugar" # In an initializer
165
+
166
+ class AlbumsController < ApplicationController
167
+ def show
168
+ render json: album, serializer: AlbumSerializer
169
+ end
170
+
171
+ def index
172
+ render json: albums, root: :albums, each_serializer: AlbumSerializer
173
+ end
174
+ end
175
+ ```
176
+ </details>
177
+
157
178
  ## Rendering 🖨
158
179
 
159
180
  Use `one` to serialize objects, and `many` to serialize enumerables:
@@ -169,7 +190,7 @@ Serializers can be rendered arrays, hashes, or even inside `ActiveModel::Seriali
169
190
  by using a method in the serializer, making it very easy to combine with other
170
191
  libraries and migrate incrementally.
171
192
 
172
- You can use `render` as a shortcut for `one` and `many`, but it might be less readable:
193
+ `render` is a shortcut for `one` and `many`:
173
194
 
174
195
  ```ruby
175
196
  render json: {
@@ -367,6 +388,51 @@ One slight variation that might make it easier to maintain in the long term is
367
388
  to use a separate singleton service to provide the url helpers and options, and
368
389
  make it available as `urls`.
369
390
 
391
+ ### Generating TypeScript automatically 🤖
392
+
393
+ It's easy for the backend and the frontend to become out of sync. Traditionally, preventing bugs requires writing extensive integration tests.
394
+
395
+ [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.
396
+
397
+ [`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.
398
+
399
+ 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.
400
+
401
+ ### Composing serializers 🧱
402
+
403
+ There are three options to compose serializers: inheritance, mixins, and `flat_one`.
404
+
405
+ Use `flat_one` to include all attributes from a different serializer:
406
+
407
+ ```ruby
408
+ class AttachmentSerializer < BaseSerializer
409
+ identifier
410
+
411
+ class BlobSerializer < BaseSerializer
412
+ attributes :filename, :byte_size, :content_type, :created_at
413
+ end
414
+
415
+ flat_one :blob, serializer: BlobSerializer
416
+ end
417
+ ```
418
+
419
+ <details>
420
+ <summary>Example Output</summary>
421
+
422
+ ```ruby
423
+ {
424
+ id: 5,
425
+ filename: "image.jpg,
426
+ byte_size: 256074,
427
+ content_type: "image/jpeg",
428
+ created_at: "2022-08-04T17:25:12.637-07:00",
429
+ }
430
+ ```
431
+ </details>
432
+
433
+ This is especially convenient when using [`types_from_serializers`][types_from_serializers],
434
+ as it enables automatic type inference for the included attributes.
435
+
370
436
  ### Memoization & local state
371
437
 
372
438
  Serializers are designed to be stateless so that an instanced can be reused, but
@@ -469,10 +535,8 @@ end
469
535
  This will change the default shortcuts (`render`, `one`, `one_if`, and `many`),
470
536
  so that the serializer writes directly to JSON instead of returning a Hash.
471
537
 
472
- > **Note**
473
- >
474
- > This was the default behavior in `oj_serializers` v1, but was replaced with
475
- `default_format :hash` in v2.
538
+ Even when using this mode, you can still use rendered values inside arrays,
539
+ hashes, and other serializers, thanks to [the `raw_json` extensions][raw_json].
476
540
 
477
541
  <details>
478
542
  <summary>Example Output</summary>
@@ -558,9 +622,6 @@ so that the serializer writes directly to JSON instead of returning a Hash.
558
622
  ```
559
623
  </details>
560
624
 
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
-
564
625
  ## Design 📐
565
626
 
566
627
  Unlike `ActiveModel::Serializer`, which builds a Hash that then gets encoded to
@@ -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)
@@ -85,30 +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
- @_default_format = value
103
+ #
104
+ # This setting is inherited from parent classes.
105
+ def default_format(format)
106
+ define_singleton_method(:_default_format) { format }
90
107
  define_serialization_shortcuts
91
108
  end
92
109
 
93
- # Public: Allows to sort fields by name instead.
94
- def sort_attributes_by(value)
95
- @_sort_attributes_by = case value
96
- when :name then ->(name, options) { options[:identifier] ? "__#{name}" : name }
97
- 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 }
98
118
  else
99
- raise ArgumentError, "Unknown sorting option: #{value.inspect}"
119
+ raise ArgumentError, "Unknown sorting option: #{strategy.inspect}"
100
120
  end
101
121
  end
102
122
 
103
- # Public: Allows to sort fields by name instead.
104
- def transform_keys(transformer = nil, &block)
105
- @_transform_keys = case (transformer ||= block)
106
- when :camelize, :camel_case then ->(key) { key.to_s.camelize(:lower) }
107
- when Symbol then transformer.to_proc
108
- 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).chomp('?') }
130
+ when :none then nil
131
+ when Symbol then strategy.to_proc
132
+ when Proc then strategy
109
133
  else
110
- raise(ArgumentError, "Expected transform_keys to be callable, got: #{transformer.inspect}")
134
+ raise(ArgumentError, "Expected transform_keys to be callable, got: #{strategy.inspect}")
111
135
  end
136
+ define_singleton_method(:_transform_keys) { transformer }
112
137
  end
113
138
 
114
139
  # Public: Creates an alias for the internal object.
@@ -318,15 +343,15 @@ protected
318
343
 
319
344
  # Public: Specify a collection of objects that should be serialized using
320
345
  # the specified serializer.
321
- def has_many(name, serializer:, root: name, as: root, **options, &block)
346
+ def has_many(name, serializer:, **options, &block)
322
347
  define_method(name, &block) if block
323
- add_attribute(name, association: :many, as: as, serializer: serializer, **options)
348
+ add_attribute(name, association: :many, serializer: serializer, **options)
324
349
  end
325
350
 
326
351
  # Public: Specify an object that should be serialized using the serializer.
327
- def has_one(name, serializer:, root: name, as: root, **options, &block)
352
+ def has_one(name, serializer:, **options, &block)
328
353
  define_method(name, &block) if block
329
- add_attribute(name, association: :one, as: as, serializer: serializer, **options)
354
+ add_attribute(name, association: :one, serializer: serializer, **options)
330
355
  end
331
356
  # Alias: From a serializer perspective, the association type does not matter.
332
357
  alias_method :belongs_to, :has_one
@@ -340,9 +365,8 @@ protected
340
365
 
341
366
  # Public: Specify which attributes are going to be obtained from indexing
342
367
  # the object.
343
- def hash_attributes(*method_names, **options)
344
- options = { **options, attribute: :hash }
345
- method_names.each { |name| _attributes[name] = options }
368
+ def hash_attributes(*attr_names, **options)
369
+ attributes(*attr_names, **options, attribute: :hash)
346
370
  end
347
371
 
348
372
  # Public: Specify which attributes are going to be obtained from indexing
@@ -351,31 +375,32 @@ protected
351
375
  # Automatically renames `_id` to `id` for Mongoid models.
352
376
  #
353
377
  # See ./benchmarks/document_benchmark.rb
354
- def mongo_attributes(*method_names, **options)
355
- identifier(:_id, as: :id, attribute: :mongoid, **options) if method_names.delete(:id)
356
- attributes(*method_names, **options, attribute: :mongoid)
378
+ def mongo_attributes(*attr_names, **options)
379
+ identifier(:_id, as: :id, attribute: :mongoid, **options.slice(:if)) if attr_names.delete(:id)
380
+ attributes(*attr_names, **options, attribute: :mongoid)
357
381
  end
358
382
 
359
383
  # Public: Specify which attributes are going to be obtained by calling a
360
384
  # method in the object.
361
- def attributes(*method_names, **methods_with_options)
385
+ def attributes(*attr_names, **methods_with_options)
362
386
  attr_options = methods_with_options.extract!(:if, :as, :attribute)
363
387
  attr_options[:attribute] ||= :method
364
388
 
365
- method_names.each do |name|
366
- add_attribute(name, attr_options)
389
+ attr_names.each do |attr_name|
390
+ add_attribute(attr_name, **attr_options)
367
391
  end
368
392
 
369
- methods_with_options.each do |name, options|
393
+ methods_with_options.each do |attr_name, options|
370
394
  options = { as: options } if options.is_a?(Symbol)
371
- add_attribute(name, options)
395
+ options[:attribute] ||= attr_options[:attribute]
396
+ add_attribute(attr_name, **options)
372
397
  end
373
398
  end
374
399
 
375
400
  # Public: Specify which attributes are going to be obtained by calling a
376
401
  # method in the serializer.
377
- def serializer_attributes(*method_names, **options)
378
- attributes(*method_names, **options, attribute: :serializer)
402
+ def serializer_attributes(*attr_names, **options)
403
+ attributes(*attr_names, **options, attribute: :serializer)
379
404
  end
380
405
 
381
406
  # Syntax Sugar: Allows to use it before a method name.
@@ -389,7 +414,7 @@ protected
389
414
  options[:attribute] = :serializer
390
415
  if name
391
416
  define_method(name, &block) if block
392
- add_attribute(name, options)
417
+ add_attribute(name, **options)
393
418
  else
394
419
  @_current_attribute_options = options
395
420
  end
@@ -401,7 +426,7 @@ protected
401
426
  def method_added(name)
402
427
  super(name)
403
428
  if @_current_attribute_options
404
- add_attribute(name, @_current_attribute_options)
429
+ add_attribute(name, **@_current_attribute_options)
405
430
  @_current_attribute_options = nil
406
431
  end
407
432
  end
@@ -410,45 +435,34 @@ protected
410
435
  # calling a method in the serializer, or using `read_attribute_for_serialization`.
411
436
  #
412
437
  # NOTE: Prefer to use `attributes` or `serializer_attributes` explicitly.
413
- def ams_attributes(*method_names, **options)
414
- method_names.each do |method_name|
415
- define_method(method_name) { @object.read_attribute_for_serialization(method_name) } unless method_defined?(method_name)
438
+ def ams_attributes(*attr_names, **options)
439
+ attr_names.each do |attr_name|
440
+ define_method(attr_name) { @object.read_attribute_for_serialization(attr_name) } unless method_defined?(attr_name)
416
441
  end
417
- attributes(*method_names, **options, attribute: :serializer)
442
+ attributes(*attr_names, **options, attribute: :serializer)
418
443
  end
419
444
 
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
424
- end
445
+ private
425
446
 
426
- # Internal: The strategy to use when sorting the fields.
427
- #
428
- # This setting is inherited from parent classes.
429
- def _sort_attributes_by
430
- @_sort_attributes_by = superclass.try(:_sort_attributes_by) unless defined?(@_sort_attributes_by)
431
- @_sort_attributes_by
432
- end
447
+ def add_attribute(value_from, root: nil, as: nil, **options)
448
+ # Because it's so common, automatically mark id as an identifier.
449
+ options[:identifier] = true if value_from == :id && !options.key?(:identifier)
433
450
 
434
- # Internal: The converter to use for serializer keys.
435
- #
436
- # This setting is inherited from parent classes.
437
- def _transform_keys
438
- @_transform_keys = superclass.try(:_transform_keys) unless defined?(@_transform_keys)
439
- @_transform_keys
440
- end
451
+ # Hash attributes could be numbers or symbols.
452
+ value_from = value_from.to_s unless options[:attribute] == :hash
441
453
 
442
- private
454
+ # Obtain the JSON key to use for the attribute.
455
+ key = (root || as || value_from).to_s
443
456
 
444
- def add_attribute(name, options)
445
- _attributes[name.to_s.freeze] = options
446
- end
457
+ # Should be able to add "duplicate" flat associations.
458
+ key += _attributes.count.to_s if options[:association] == :flat
447
459
 
448
- # Internal: Transforms the keys using the provided strategy.
449
- def key_for(method_name, options)
450
- key = options.fetch(:as, method_name)
451
- _transform_keys ? _transform_keys.call(key) : key
460
+ # Check for typos in options.
461
+ if DEV_MODE && (option, = options.find { |option, _value| !KNOWN_ATTRIBUTE_OPTIONS.include?(option) })
462
+ raise ArgumentError, "Unknown option #{option.inspect} for attribute #{value_from.inspect} in #{name}. Please check for typos."
463
+ end
464
+
465
+ _attributes[key.freeze] = { value_from: value_from, **options }.freeze
452
466
  end
453
467
 
454
468
  # Internal: Whether the object should be serialized as a collection.
@@ -464,19 +478,19 @@ protected
464
478
  #
465
479
  # As a result, the performance is the same as writing the most efficient
466
480
  # code by hand.
467
- def code_to_write_to_json
481
+ def code_to_write_to_json(attributes)
468
482
  <<~WRITE_TO_JSON
469
483
  # Public: Writes this serializer content to a provided Oj::StringWriter.
470
484
  def write_to_json(writer, item, options = nil)
471
485
  @object = item
472
486
  @options = options
473
487
  @memo.clear if defined?(@memo)
474
- #{ _attributes.map { |method_name, options|
475
- code_to_write_conditional(method_name, options) {
488
+ #{ attributes.map { |key, options|
489
+ code_to_write_conditionally(options) {
476
490
  if options[:association]
477
- code_to_write_association(method_name, options)
491
+ code_to_write_association(key, options)
478
492
  else
479
- code_to_write_attribute(method_name, options)
493
+ code_to_write_attribute(key, options)
480
494
  end
481
495
  }
482
496
  }.join("\n ") }#{code_to_rescue_no_method if DEV_MODE}
@@ -490,7 +504,7 @@ protected
490
504
  #
491
505
  # As a result, the performance is the same as writing the most efficient
492
506
  # code by hand.
493
- def code_to_render_as_hash
507
+ def code_to_render_as_hash(attributes)
494
508
  <<~RENDER_AS_HASH
495
509
  # Public: Writes this serializer content to a Hash.
496
510
  def render_as_hash(item, options = nil)
@@ -498,12 +512,12 @@ protected
498
512
  @options = options
499
513
  @memo.clear if defined?(@memo)
500
514
  {
501
- #{_attributes.map { |method_name, options|
502
- code_to_render_conditionally(method_name, options) {
515
+ #{attributes.map { |key, options|
516
+ code_to_render_conditionally(options) {
503
517
  if options[:association]
504
- code_to_render_association(method_name, options)
518
+ code_to_render_association(key, options)
505
519
  else
506
- code_to_render_attribute(method_name, options)
520
+ code_to_render_attribute(key, options)
507
521
  end
508
522
  }
509
523
  }.join(",\n ")}
@@ -518,7 +532,7 @@ protected
518
532
  rescue NoMethodError => e
519
533
  key = e.name.to_s.inspect
520
534
  message = if respond_to?(e.name)
521
- raise e, "Perhaps you meant to call \#{key} in \#{self.class} instead?\nTry using `serializer_attributes :\#{key}` or `attribute def \#{key}`.\n\#{e.message}"
535
+ raise e, "Perhaps you meant to call \#{key} in \#{self.class} instead?\nTry using `attribute :\#{key} do` or `attribute def \#{key}`.\n\#{e.message}"
522
536
  elsif @object.respond_to?(e.name)
523
537
  raise e, "Perhaps you meant to call \#{key} in \#{@object.class} instead?\nTry using `attributes :\#{key}`.\n\#{e.message}"
524
538
  else
@@ -531,8 +545,9 @@ protected
531
545
 
532
546
  # Internal: Detects any include methods defined in the serializer, or defines
533
547
  # 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?('?')}"
548
+ def check_conditional_method(options)
549
+ value_from = options.fetch(:value_from)
550
+ include_method_name = "include_#{value_from}#{'?' unless value_from.to_s.ends_with?('?')}"
536
551
  if render_if = options[:if]
537
552
  if render_if.is_a?(Symbol)
538
553
  alias_method(include_method_name, render_if)
@@ -548,8 +563,8 @@ protected
548
563
  #
549
564
  # NOTE: Detects any include methods defined in the serializer, or defines
550
565
  # one by using the lambda passed in the `if` option, if any.
551
- def code_to_write_conditional(method_name, options)
552
- if (include_method_name = check_conditional_method(method_name, options))
566
+ def code_to_write_conditionally(options)
567
+ if (include_method_name = check_conditional_method(options))
553
568
  "if #{include_method_name};#{yield};end\n"
554
569
  else
555
570
  yield
@@ -557,50 +572,52 @@ protected
557
572
  end
558
573
 
559
574
  # Internal: Returns the code for the association method.
560
- def code_to_write_attribute(method_name, options)
561
- key = key_for(method_name, options).to_s.inspect
575
+ def code_to_write_attribute(key, options)
576
+ value_from = options.fetch(:value_from)
562
577
 
563
- case strategy = options.fetch(:attribute)
578
+ value = case (strategy = options.fetch(:attribute))
564
579
  when :serializer
565
580
  # Obtains the value by calling a method in the serializer.
566
- "writer.push_value(#{method_name}, #{key})"
581
+ value_from
567
582
  when :method
568
583
  # Obtains the value by calling a method in the object, and writes it.
569
- "writer.push_value(@object.#{method_name}, #{key})"
584
+ "@object.#{value_from}"
570
585
  when :hash
571
586
  # Writes a Hash value to JSON, works with String or Symbol keys.
572
- "writer.push_value(@object[#{method_name.inspect}], #{key})"
587
+ "@object[#{value_from.inspect}]"
573
588
  when :mongoid
574
589
  # Writes an Mongoid attribute to JSON, this is the fastest strategy.
575
- "writer.push_value(@object.attributes['#{method_name}'], #{key})"
590
+ "@object.attributes['#{value_from}']"
576
591
  else
577
592
  raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
578
593
  end
594
+
595
+ "writer.push_value(#{value}, #{key.inspect})"
579
596
  end
580
597
 
581
598
  # Internal: Returns the code for the association method.
582
- def code_to_write_association(method_name, options)
599
+ def code_to_write_association(key, options)
583
600
  # Use a serializer method if defined, else call the association in the object.
584
- association_method = method_defined?(method_name) ? method_name : "@object.#{method_name}"
585
- key = key_for(method_name, options)
601
+ value_from = options.fetch(:value_from)
602
+ value = method_defined?(value_from) ? value_from : "@object.#{value_from}"
586
603
  serializer_class = options.fetch(:serializer)
587
604
 
588
605
  case type = options.fetch(:association)
589
606
  when :one
590
607
  <<~WRITE_ONE
591
- if associated_object = #{association_method}
608
+ if __value = #{value}
592
609
  writer.push_key('#{key}')
593
- #{serializer_class}.write_one(writer, associated_object)
610
+ #{serializer_class}.write_one(writer, __value)
594
611
  end
595
612
  WRITE_ONE
596
613
  when :many
597
614
  <<~WRITE_MANY
598
615
  writer.push_key('#{key}')
599
- #{serializer_class}.write_many(writer, #{association_method})
616
+ #{serializer_class}.write_many(writer, #{value})
600
617
  WRITE_MANY
601
618
  when :flat
602
619
  <<~WRITE_FLAT
603
- #{serializer_class}.write_to_json(writer, #{association_method})
620
+ #{serializer_class}.write_to_json(writer, #{value})
604
621
  WRITE_FLAT
605
622
  else
606
623
  raise ArgumentError, "Unknown association type: #{type.inspect}"
@@ -612,8 +629,8 @@ protected
612
629
  #
613
630
  # NOTE: Detects any include methods defined in the serializer, or defines
614
631
  # one by using the lambda passed in the `if` option, if any.
615
- def code_to_render_conditionally(method_name, options)
616
- if (include_method_name = check_conditional_method(method_name, options))
632
+ def code_to_render_conditionally(options)
633
+ if (include_method_name = check_conditional_method(options))
617
634
  "**(#{include_method_name} ? {#{yield}} : {})"
618
635
  else
619
636
  yield
@@ -621,36 +638,39 @@ protected
621
638
  end
622
639
 
623
640
  # Internal: Returns the code for the attribute method.
624
- def code_to_render_attribute(method_name, options)
625
- key = key_for(method_name, options)
626
- case strategy = options.fetch(:attribute)
641
+ def code_to_render_attribute(key, options)
642
+ value_from = options.fetch(:value_from)
643
+
644
+ value = case (strategy = options.fetch(:attribute))
627
645
  when :serializer
628
- "#{key}: #{method_name}"
646
+ value_from
629
647
  when :method
630
- "#{key}: @object.#{method_name}"
648
+ "@object.#{value_from}"
631
649
  when :hash
632
- "#{key}: @object[#{method_name.inspect}]"
650
+ "@object[#{value_from.inspect}]"
633
651
  when :mongoid
634
- "#{key}: @object.attributes['#{method_name}']"
652
+ "@object.attributes['#{value_from}']"
635
653
  else
636
654
  raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
637
655
  end
656
+
657
+ "#{key}: #{value}"
638
658
  end
639
659
 
640
660
  # Internal: Returns the code for the association method.
641
- def code_to_render_association(method_name, options)
661
+ def code_to_render_association(key, options)
642
662
  # Use a serializer method if defined, else call the association in the object.
643
- association = method_defined?(method_name) ? method_name : "@object.#{method_name}"
644
- key = key_for(method_name, options)
663
+ value_from = options.fetch(:value_from)
664
+ value = method_defined?(value_from) ? value_from : "@object.#{value_from}"
645
665
  serializer_class = options.fetch(:serializer)
646
666
 
647
667
  case type = options.fetch(:association)
648
668
  when :one
649
- "#{key}: (one_item = #{association}) ? #{serializer_class}.one_as_hash(one_item) : nil"
669
+ "#{key}: (__value = #{value}) ? #{serializer_class}.one_as_hash(__value) : nil"
650
670
  when :many
651
- "#{key}: #{serializer_class}.many_as_hash(#{association})"
671
+ "#{key}: #{serializer_class}.many_as_hash(#{value})"
652
672
  when :flat
653
- "**#{serializer_class}.one_as_hash(#{association})"
673
+ "**#{serializer_class}.one_as_hash(#{value})"
654
674
  else
655
675
  raise ArgumentError, "Unknown association type: #{type.inspect}"
656
676
  end
@@ -671,25 +691,36 @@ protected
671
691
  @instance_key ||= begin
672
692
  # We take advantage of the fact that this method will always be called
673
693
  # before instantiating a serializer, to apply last minute adjustments.
674
- _prepare_serializer
694
+ prepare_serializer
675
695
  "#{name.underscore}_instance_#{object_id}".to_sym
676
696
  end
677
697
  end
678
698
 
679
699
  # Internal: Generates write_to_json and render_as_hash methods optimized for
680
700
  # the specified configuration.
681
- def _prepare_serializer
682
- if _sort_attributes_by
683
- @_attributes = _attributes.sort_by { |key, options|
684
- _sort_attributes_by.call(key, options)
685
- }.to_h
701
+ def prepare_serializer
702
+ attributes = prepare_attributes
703
+ class_eval(code_to_write_to_json(attributes))
704
+ class_eval(code_to_render_as_hash(attributes))
705
+ end
706
+
707
+ # Internal: Returns attributes sorted and with keys transformed using
708
+ # the specified strategies.
709
+ def prepare_attributes(transform_keys: try(:_transform_keys), sort_by: try(:_sort_attributes_by))
710
+ attributes = _attributes
711
+ attributes = attributes.transform_keys(&transform_keys) if transform_keys
712
+
713
+ if sort_by == :name
714
+ sort_by = ->(name, options, _) { options[:identifier] ? "__#{name}" : name }
715
+ elsif !sort_by || sort_by == :definition
716
+ sort_by = ->(name, options, index) { options[:identifier] ? "__#{name}" : "zzz#{index}" }
686
717
  end
687
- class_eval(code_to_write_to_json)
688
- class_eval(code_to_render_as_hash)
718
+
719
+ attributes.sort_by.with_index { |(name, options), index| sort_by.call(name, options, index) }.to_h
689
720
  end
690
721
  end
691
722
 
692
- define_serialization_shortcuts
723
+ default_format :hash
693
724
  end
694
725
 
695
726
  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'
4
+ VERSION = '2.0.2'
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
4
+ version: 2.0.2
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-27 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
@@ -68,7 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
70
  requirements: []
71
- rubygems_version: 3.2.32
71
+ rubygems_version: 3.3.7
72
72
  signing_key:
73
73
  specification_version: 4
74
74
  summary: A lighter JSON serializer for Ruby Objects in Rails. Easily migrate away