oj_serializers 2.0.0 → 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: 281e961cd6a9da8c3723dd56ce7e1aa8ec112c57d13ab15e763ecef98eb1f659
4
- data.tar.gz: cd9a42db1ea64aac9c75755550ce0bb3123646ac85020056a4d3bc5dae7e9251
3
+ metadata.gz: e8fd87262444d89cfbbe11a1b6ae6201de6d71ed996edcd8a4a9ae224320ad7c
4
+ data.tar.gz: bfe507b6e02c01087d99381ab8ef971547e0062085eb4bfb9c9551ceb2796c18
5
5
  SHA512:
6
- metadata.gz: c3816ef19190cf4dbddfa942d205b21cb0d639ed33cf9ff34a9972038179d677cfabe659fc36f1780e6fb64fb8c7145a756012bfc1a8de3bc4cb0256e419917a
7
- data.tar.gz: 364da01e189d7c00b4abcbaa490c2e8a0432acf9f126a0db67ccab3d1a2c7a1b1724267ec084980ba706ad43fb89c1964749a4684736d72888f6ddb5fe970f3f
6
+ metadata.gz: 060b791a7d8c2f204adab9313873b950b89eb1a80400b26355b1382ab6e08d0efa3e48efeef1ececc96cb8f42729f8f4cea50b37d579ab1acc333b393b060c0d
7
+ data.tar.gz: a7ea8370d3155f8bfd99f9d1cffe67fe05f896930277a0e69a81b3e4eda2b0088a8901730ec2fb910e564dcf3f29ea3200f5cc70d8321dd1892b72c9f9b75bf4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
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
+
1
12
  ## [Oj Serializers 2.0.0 (2023-03-27)](https://github.com/ElMassimo/oj_serializers/pull/9)
2
13
 
3
14
  ### 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>
@@ -28,6 +27,8 @@ Faster JSON serializers for Ruby, built on top of the powerful [`oj`][oj] librar
28
27
  [render dsl]: https://github.com/ElMassimo/oj_serializers#render-dsl-
29
28
  [sorbet]: https://sorbet.org/
30
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
31
32
 
32
33
  ## Why? 🤔
33
34
 
@@ -45,6 +46,7 @@ Learn more about [how this library achieves its performance][design].
45
46
  - Support for `has_one` and `has_many`, compose with `flat_one`
46
47
  - Useful development checks to avoid typos and mistakes
47
48
  - Integrates nicely with Rails controllers
49
+ - [Generate TypeScript interfaces automatically][types_from_serializers]
48
50
 
49
51
  ## Installation 💿
50
52
 
@@ -67,8 +69,7 @@ attributes should be serialized.
67
69
  class AlbumSerializer < Oj::Serializer
68
70
  attributes :name, :genres
69
71
 
70
- attr
71
- def release
72
+ attribute :release do
72
73
  album.release_date.strftime('%B %d, %Y')
73
74
  end
74
75
 
@@ -367,6 +368,16 @@ One slight variation that might make it easier to maintain in the long term is
367
368
  to use a separate singleton service to provide the url helpers and options, and
368
369
  make it available as `urls`.
369
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.
378
+
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
+
370
381
  ### Memoization & local state
371
382
 
372
383
  Serializers are designed to be stateless so that an instanced can be reused, but
@@ -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) }
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,31 @@ 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) 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
+ add_attribute(attr_name, **options)
372
396
  end
373
397
  end
374
398
 
375
399
  # Public: Specify which attributes are going to be obtained by calling a
376
400
  # method in the serializer.
377
- def serializer_attributes(*method_names, **options)
378
- attributes(*method_names, **options, attribute: :serializer)
401
+ def serializer_attributes(*attr_names, **options)
402
+ attributes(*attr_names, **options, attribute: :serializer)
379
403
  end
380
404
 
381
405
  # Syntax Sugar: Allows to use it before a method name.
@@ -389,7 +413,7 @@ protected
389
413
  options[:attribute] = :serializer
390
414
  if name
391
415
  define_method(name, &block) if block
392
- add_attribute(name, options)
416
+ add_attribute(name, **options)
393
417
  else
394
418
  @_current_attribute_options = options
395
419
  end
@@ -401,7 +425,7 @@ protected
401
425
  def method_added(name)
402
426
  super(name)
403
427
  if @_current_attribute_options
404
- add_attribute(name, @_current_attribute_options)
428
+ add_attribute(name, **@_current_attribute_options)
405
429
  @_current_attribute_options = nil
406
430
  end
407
431
  end
@@ -410,45 +434,34 @@ protected
410
434
  # calling a method in the serializer, or using `read_attribute_for_serialization`.
411
435
  #
412
436
  # 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)
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)
416
440
  end
417
- attributes(*method_names, **options, attribute: :serializer)
441
+ attributes(*attr_names, **options, attribute: :serializer)
418
442
  end
419
443
 
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
444
+ private
425
445
 
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
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)
433
449
 
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
450
+ # Hash attributes could be numbers or symbols.
451
+ value_from = value_from.to_s unless options[:attribute] == :hash
441
452
 
442
- private
453
+ # Obtain the JSON key to use for the attribute.
454
+ key = (root || as || value_from).to_s
443
455
 
444
- def add_attribute(name, options)
445
- _attributes[name.to_s.freeze] = options
446
- end
456
+ # Should be able to add "duplicate" flat associations.
457
+ key += _attributes.count.to_s if options[:association] == :flat
447
458
 
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
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
452
465
  end
453
466
 
454
467
  # Internal: Whether the object should be serialized as a collection.
@@ -464,19 +477,19 @@ protected
464
477
  #
465
478
  # As a result, the performance is the same as writing the most efficient
466
479
  # code by hand.
467
- def code_to_write_to_json
480
+ def code_to_write_to_json(attributes)
468
481
  <<~WRITE_TO_JSON
469
482
  # Public: Writes this serializer content to a provided Oj::StringWriter.
470
483
  def write_to_json(writer, item, options = nil)
471
484
  @object = item
472
485
  @options = options
473
486
  @memo.clear if defined?(@memo)
474
- #{ _attributes.map { |method_name, options|
475
- code_to_write_conditional(method_name, options) {
487
+ #{ attributes.map { |key, options|
488
+ code_to_write_conditionally(options) {
476
489
  if options[:association]
477
- code_to_write_association(method_name, options)
490
+ code_to_write_association(key, options)
478
491
  else
479
- code_to_write_attribute(method_name, options)
492
+ code_to_write_attribute(key, options)
480
493
  end
481
494
  }
482
495
  }.join("\n ") }#{code_to_rescue_no_method if DEV_MODE}
@@ -490,7 +503,7 @@ protected
490
503
  #
491
504
  # As a result, the performance is the same as writing the most efficient
492
505
  # code by hand.
493
- def code_to_render_as_hash
506
+ def code_to_render_as_hash(attributes)
494
507
  <<~RENDER_AS_HASH
495
508
  # Public: Writes this serializer content to a Hash.
496
509
  def render_as_hash(item, options = nil)
@@ -498,12 +511,12 @@ protected
498
511
  @options = options
499
512
  @memo.clear if defined?(@memo)
500
513
  {
501
- #{_attributes.map { |method_name, options|
502
- code_to_render_conditionally(method_name, options) {
514
+ #{attributes.map { |key, options|
515
+ code_to_render_conditionally(options) {
503
516
  if options[:association]
504
- code_to_render_association(method_name, options)
517
+ code_to_render_association(key, options)
505
518
  else
506
- code_to_render_attribute(method_name, options)
519
+ code_to_render_attribute(key, options)
507
520
  end
508
521
  }
509
522
  }.join(",\n ")}
@@ -518,7 +531,7 @@ protected
518
531
  rescue NoMethodError => e
519
532
  key = e.name.to_s.inspect
520
533
  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}"
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}"
522
535
  elsif @object.respond_to?(e.name)
523
536
  raise e, "Perhaps you meant to call \#{key} in \#{@object.class} instead?\nTry using `attributes :\#{key}`.\n\#{e.message}"
524
537
  else
@@ -531,8 +544,9 @@ protected
531
544
 
532
545
  # Internal: Detects any include methods defined in the serializer, or defines
533
546
  # 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?('?')}"
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?('?')}"
536
550
  if render_if = options[:if]
537
551
  if render_if.is_a?(Symbol)
538
552
  alias_method(include_method_name, render_if)
@@ -548,8 +562,8 @@ protected
548
562
  #
549
563
  # NOTE: Detects any include methods defined in the serializer, or defines
550
564
  # 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))
565
+ def code_to_write_conditionally(options)
566
+ if (include_method_name = check_conditional_method(options))
553
567
  "if #{include_method_name};#{yield};end\n"
554
568
  else
555
569
  yield
@@ -557,50 +571,52 @@ protected
557
571
  end
558
572
 
559
573
  # 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
574
+ def code_to_write_attribute(key, options)
575
+ value_from = options.fetch(:value_from)
562
576
 
563
- case strategy = options.fetch(:attribute)
577
+ value = case (strategy = options.fetch(:attribute))
564
578
  when :serializer
565
579
  # Obtains the value by calling a method in the serializer.
566
- "writer.push_value(#{method_name}, #{key})"
580
+ value_from
567
581
  when :method
568
582
  # Obtains the value by calling a method in the object, and writes it.
569
- "writer.push_value(@object.#{method_name}, #{key})"
583
+ "@object.#{value_from}"
570
584
  when :hash
571
585
  # Writes a Hash value to JSON, works with String or Symbol keys.
572
- "writer.push_value(@object[#{method_name.inspect}], #{key})"
586
+ "@object[#{value_from.inspect}]"
573
587
  when :mongoid
574
588
  # Writes an Mongoid attribute to JSON, this is the fastest strategy.
575
- "writer.push_value(@object.attributes['#{method_name}'], #{key})"
589
+ "@object.attributes['#{value_from}']"
576
590
  else
577
591
  raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
578
592
  end
593
+
594
+ "writer.push_value(#{value}, #{key.inspect})"
579
595
  end
580
596
 
581
597
  # Internal: Returns the code for the association method.
582
- def code_to_write_association(method_name, options)
598
+ def code_to_write_association(key, options)
583
599
  # 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)
600
+ value_from = options.fetch(:value_from)
601
+ value = method_defined?(value_from) ? value_from : "@object.#{value_from}"
586
602
  serializer_class = options.fetch(:serializer)
587
603
 
588
604
  case type = options.fetch(:association)
589
605
  when :one
590
606
  <<~WRITE_ONE
591
- if associated_object = #{association_method}
607
+ if __value = #{value}
592
608
  writer.push_key('#{key}')
593
- #{serializer_class}.write_one(writer, associated_object)
609
+ #{serializer_class}.write_one(writer, __value)
594
610
  end
595
611
  WRITE_ONE
596
612
  when :many
597
613
  <<~WRITE_MANY
598
614
  writer.push_key('#{key}')
599
- #{serializer_class}.write_many(writer, #{association_method})
615
+ #{serializer_class}.write_many(writer, #{value})
600
616
  WRITE_MANY
601
617
  when :flat
602
618
  <<~WRITE_FLAT
603
- #{serializer_class}.write_to_json(writer, #{association_method})
619
+ #{serializer_class}.write_to_json(writer, #{value})
604
620
  WRITE_FLAT
605
621
  else
606
622
  raise ArgumentError, "Unknown association type: #{type.inspect}"
@@ -612,8 +628,8 @@ protected
612
628
  #
613
629
  # NOTE: Detects any include methods defined in the serializer, or defines
614
630
  # 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))
631
+ def code_to_render_conditionally(options)
632
+ if (include_method_name = check_conditional_method(options))
617
633
  "**(#{include_method_name} ? {#{yield}} : {})"
618
634
  else
619
635
  yield
@@ -621,36 +637,39 @@ protected
621
637
  end
622
638
 
623
639
  # 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)
640
+ def code_to_render_attribute(key, options)
641
+ value_from = options.fetch(:value_from)
642
+
643
+ value = case (strategy = options.fetch(:attribute))
627
644
  when :serializer
628
- "#{key}: #{method_name}"
645
+ value_from
629
646
  when :method
630
- "#{key}: @object.#{method_name}"
647
+ "@object.#{value_from}"
631
648
  when :hash
632
- "#{key}: @object[#{method_name.inspect}]"
649
+ "@object[#{value_from.inspect}]"
633
650
  when :mongoid
634
- "#{key}: @object.attributes['#{method_name}']"
651
+ "@object.attributes['#{value_from}']"
635
652
  else
636
653
  raise ArgumentError, "Unknown attribute strategy: #{strategy.inspect}"
637
654
  end
655
+
656
+ "#{key}: #{value}"
638
657
  end
639
658
 
640
659
  # Internal: Returns the code for the association method.
641
- def code_to_render_association(method_name, options)
660
+ def code_to_render_association(key, options)
642
661
  # 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)
662
+ value_from = options.fetch(:value_from)
663
+ value = method_defined?(value_from) ? value_from : "@object.#{value_from}"
645
664
  serializer_class = options.fetch(:serializer)
646
665
 
647
666
  case type = options.fetch(:association)
648
667
  when :one
649
- "#{key}: (one_item = #{association}) ? #{serializer_class}.one_as_hash(one_item) : nil"
668
+ "#{key}: (__value = #{value}) ? #{serializer_class}.one_as_hash(__value) : nil"
650
669
  when :many
651
- "#{key}: #{serializer_class}.many_as_hash(#{association})"
670
+ "#{key}: #{serializer_class}.many_as_hash(#{value})"
652
671
  when :flat
653
- "**#{serializer_class}.one_as_hash(#{association})"
672
+ "**#{serializer_class}.one_as_hash(#{value})"
654
673
  else
655
674
  raise ArgumentError, "Unknown association type: #{type.inspect}"
656
675
  end
@@ -671,25 +690,36 @@ protected
671
690
  @instance_key ||= begin
672
691
  # We take advantage of the fact that this method will always be called
673
692
  # before instantiating a serializer, to apply last minute adjustments.
674
- _prepare_serializer
693
+ prepare_serializer
675
694
  "#{name.underscore}_instance_#{object_id}".to_sym
676
695
  end
677
696
  end
678
697
 
679
698
  # Internal: Generates write_to_json and render_as_hash methods optimized for
680
699
  # 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
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}" }
686
716
  end
687
- class_eval(code_to_write_to_json)
688
- 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
689
719
  end
690
720
  end
691
721
 
692
- define_serialization_shortcuts
722
+ default_format :hash
693
723
  end
694
724
 
695
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'
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
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-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