serega 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 384da177845e2385a0100d23147cea0034860b148cd46738dc3d6ec7ea7d7eda
4
- data.tar.gz: f08d482a83cb0cde30ae4c03e924f50be6aaabea24d76564bc47a7a1eadf4f98
3
+ metadata.gz: a223242ae8bce14755fa2cbb444ff78728d95fccb0ce3077c212706e39e4ddcc
4
+ data.tar.gz: b8e1d3bdf11a57091cfd22eb31bb017979382df4b2166b05f746d064dc486f6e
5
5
  SHA512:
6
- metadata.gz: c9f2f95d8a74ce4951f7acef89a1cf603fcba2f15af50d246f85d58e1456b06c03b749fef620c741f8e2482999c6f7471fb5dff9a904b275f79f039999ab7fdc
7
- data.tar.gz: 69c1b8f898f81317310e875a8771db096b3d4613b34769024d66fe801554e53094fd59e3cfea923ef81bb5d837a31ef28ceadc825003c95a4a2340f6b0b0bf74
6
+ metadata.gz: d815a4a17625533c34659451851c33806bd4d1e0bf862945b2118d52315726184322c55d989378cf8ac7f9b4d04a392dfa344af196491b984ddc1d094d15c063
7
+ data.tar.gz: ecf2be1cff661bfe8e99103748276969f72442b04b8308dd136d137dc88f28dfb543ad73a5e0880a31eb6f6c4a6a51e6ccab307a92f867551ff4b19b56d38f73
data/README.md CHANGED
@@ -78,8 +78,24 @@ class UserSerializer < Serega
78
78
  # serialized object
79
79
  attribute :first_name, method: :old_first_name
80
80
 
81
- # Block is used to define attribute value
82
- attribute(:first_name) { |user| user.profile&.first_name }
81
+ # Attribute blocks below require a base serializer for nested serializers.
82
+ # See the "Defining a nested serializer with a block" section below.
83
+ config.base_serializer = Serega
84
+
85
+ # Method :itself is handy to serialize the same object with a nested set of
86
+ # attributes. Note: with the :presenter plugin the serialized value will be
87
+ # the Presenter instance itself; use `method: :__getobj__` to serialize the
88
+ # original unwrapped object instead.
89
+ attribute :statistics, method: :itself do
90
+ attribute :likes_count
91
+ attribute :comments_count
92
+ end
93
+
94
+ # Block defines a nested serializer for the attribute value.
95
+ # See the "Defining a nested serializer with a block" section below.
96
+ attribute :author do
97
+ attribute :name
98
+ end
83
99
 
84
100
  # Option :value can be used with a Proc or callable object to define
85
101
  # attribute value
@@ -114,13 +130,13 @@ class UserSerializer < Serega
114
130
 
115
131
  # Option `:many` specifies a has_many relationship. It is optional.
116
132
  # If not specified, it is defined during serialization by checking
117
- # `object.is_a?(Enumerable)`
133
+ # `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`
118
134
  # Also the `:many` changes the default value from `nil` to `[]`.
119
135
  attribute :posts, serializer: PostSerializer, many: true
120
136
 
121
137
  # Option `:preload` allows to specify associations to preload to
122
138
  # attribute value
123
- attribute(:email, preload: :emails) { |user| user.emails.find(&:verified?) }
139
+ attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
124
140
 
125
141
  # Options `:if, :unless, :if_value and :unless_value` can be specified
126
142
  # when `:if` plugin is enabled. They hide the attribute key and value from the
@@ -139,6 +155,70 @@ class UserSerializer < Serega
139
155
  end
140
156
  ```
141
157
 
158
+ ### Defining a nested serializer with a block
159
+
160
+ An attribute block defines an anonymous nested serializer. The attribute value
161
+ is found as usual — by the attribute name or the `:method`, `:value`,
162
+ `:delegate`, `:batch` option — and is serialized by this nested serializer.
163
+
164
+ The nested serializer is a regular subclass of an explicitly chosen **base
165
+ serializer** — usually a settings-only serializer holding your plugins and
166
+ configuration. Choose it with the `base_serializer:` attribute option or the
167
+ `config.base_serializer` setting (attribute option wins; an error is raised
168
+ when none is set):
169
+
170
+ ```ruby
171
+ class AppSerializer < Serega
172
+ plugin :activerecord_preloads
173
+
174
+ # Nested serializers defined with attribute blocks will inherit from
175
+ # AppSerializer
176
+ config.base_serializer = self
177
+ end
178
+
179
+ class UserSerializer < AppSerializer
180
+ attribute :first_name
181
+
182
+ # Serializes user.author with a nested serializer inherited from AppSerializer
183
+ attribute :author do
184
+ attribute :name
185
+ end
186
+
187
+ # Serializes the user itself, exposing only counters
188
+ attribute :statistics, method: :itself, base_serializer: StatsBaseSerializer do
189
+ attribute :likes_count
190
+ attribute :comments_count
191
+ end
192
+ end
193
+
194
+ UserSerializer.to_h(user)
195
+ # => {first_name: "Bruce", author: {name: "Bob"}, statistics: {likes_count: 10, comments_count: 3}}
196
+ ```
197
+
198
+ The nested serializer inherits everything the base serializer has — plugins,
199
+ config, attributes, batch loaders, the preload handler — through the regular
200
+ inheritance mechanism. Any serializer can be used as a base: if it has
201
+ attributes, they are serialized by the nested serializer too, together with
202
+ the attributes defined in the block. Anything the base does not provide can
203
+ be declared inside the block:
204
+
205
+ ```ruby
206
+ attribute :statistics, method: :itself do
207
+ batch(:stats) { |users| Stats.for_users(users) } # => { user_id => stat }
208
+
209
+ attribute :likes_count, batch: :stats, value: proc { |user, batches:| batches[:stats][user.id].likes }
210
+ end
211
+ ```
212
+
213
+ Things to keep in mind:
214
+
215
+ - The nested serializer is created at the moment the attribute is defined.
216
+ Changes made to the base serializer later do not affect already defined
217
+ nested serializers.
218
+ - Errors raised while serializing nested attributes are reported with a
219
+ readable serializer label, for example:
220
+ `(when serializing 'comments_count' attribute in UserSerializer.<statistics>)`.
221
+
142
222
  ### Serializing
143
223
 
144
224
  We can serialize objects using class method `.call` aliased as `.to_h` and
@@ -183,15 +263,6 @@ serializer.to_h(user1)
183
263
  serializer.to_h(user2)
184
264
  ```
185
265
 
186
- ---
187
- ⚠️ When you serialize the `Struct` object, specify manually `many: false`. As Struct
188
- is Enumerable and we check `object.is_a?(Enumerable)` to detect if we should
189
- return array.
190
-
191
- ```ruby
192
- UserSerializer.to_h(user_struct, many: false)
193
- ```
194
-
195
266
  ### Selecting Fields
196
267
 
197
268
  By default, all attributes are serialized (except marked as `hide: true`).
@@ -312,9 +383,9 @@ current_user or any.
312
383
 
313
384
  ```ruby
314
385
  class UserSerializer < Serega
315
- attribute(:email) do |user, ctx|
386
+ attribute :email, value: proc { |user, ctx|
316
387
  user.email if ctx[:current_user] == user
317
- end
388
+ }
318
389
  end
319
390
 
320
391
  user = OpenStruct.new(email: 'email@example.com')
@@ -339,15 +410,18 @@ class UserSerializer < Serega
339
410
  batch :comments_count, ->(users) { Comment.where(user: users).group(:user_id).count }
340
411
  batch :comments_count, CommentsCountLoader # Example with callable class
341
412
 
342
- # Full attribute example
343
- attribute :comments_count, batch: { use: :comments_count },
344
- value: proc { |user, batch:| batch[:comments_count][user.id] }
413
+ # Use a loader by its name
414
+ attribute :comments_count, batch: :comments_count
345
415
 
346
- # Shorter version
347
- attribute :comments_count, batch: { use: :comments_count, id: :id } # Equivalent, id: :id is default
416
+ # Equivalent — when the attribute name matches the loader name
417
+ attribute :comments_count, batch: true
348
418
 
349
- # Shortest version
350
- attribute :comments_count, batch: true # Equivalent
419
+ # Equivalent — the default value resolution spelled out
420
+ attribute :comments_count, batch: :comments_count,
421
+ value: proc { |user, batches:| batches[:comments_count][user.id] }
422
+
423
+ # Hash form — needed only for sub-options, for example a custom `:id` method
424
+ attribute :comments_count, batch: { use: :comments_count, id: :uuid }
351
425
  end
352
426
  ```
353
427
 
@@ -382,7 +456,7 @@ class UserSerializer < Serega
382
456
  # Summarize likes
383
457
  attribute :likes_count,
384
458
  batch: { use: [:facebook_likes, :twitter_likes] },
385
- value: { |user, batch:| batch[:facebook_likes][user.id] + batch[:twitter_likes][user.id] }
459
+ value: proc { |user, batches:| batches[:facebook_likes][user.id] + batches[:twitter_likes][user.id] }
386
460
  end
387
461
  ```
388
462
 
@@ -390,6 +464,13 @@ end
390
464
 
391
465
  Here are the default options. Other options can be added with plugins.
392
466
 
467
+ ⚠️ Attributes are prepared at the moment they are defined. If a config option
468
+ influences attributes (`auto_preload`, `hide_by_default`, `batch_id_option`,
469
+ formatters, etc.), changing it affects only attributes defined **after** the
470
+ change — including nested serializers defined with attribute blocks, which
471
+ are created at their definition point. Configure the serializer before
472
+ defining attributes.
473
+
393
474
  ```ruby
394
475
  class AppSerializer < Serega
395
476
  # With `activerecord_preloads` plugin it automatically adds `preload` option
@@ -413,6 +494,13 @@ class AppSerializer < Serega
413
494
  # proc { |object, batches:| batches[:counter][object.id] }
414
495
  config.batch_id_option = :id
415
496
 
497
+ # Parent class for nested serializers defined with attribute blocks.
498
+ # Usually a settings-only serializer, e.g. `config.base_serializer = self`
499
+ # in an application base serializer class. There is no default — an
500
+ # attribute block raises an error when no base serializer is chosen (it can
501
+ # also be provided per-attribute with the `base_serializer:` option).
502
+ config.base_serializer = self
503
+
416
504
  # Disable/enable validation of modifiers (`:with, :except, :only`)
417
505
  # By default, this validation is enabled.
418
506
  # After disabling, all requested incorrect attributes will be skipped.
@@ -793,7 +881,7 @@ class UserSerializer < Serega
793
881
  end
794
882
  ```
795
883
 
796
- With the plugin, they move into a clean class:
884
+ With the plugin, they move into a `presenter do ... end` block:
797
885
 
798
886
  ```ruby
799
887
  class UserSerializer < Serega
@@ -802,7 +890,7 @@ class UserSerializer < Serega
802
890
  attribute :name
803
891
  attribute :role
804
892
 
805
- class Presenter
893
+ presenter do
806
894
  def name
807
895
  [first_name, last_name].compact.join(' ')
808
896
  end
@@ -814,6 +902,9 @@ class UserSerializer < Serega
814
902
  end
815
903
  ```
816
904
 
905
+ The block is evaluated inside the serializer's own `Presenter` class, so
906
+ multiple `presenter` blocks accumulate.
907
+
817
908
  `Presenter` inherits from `SimpleDelegator`, so every method of the serialized
818
909
  object is available directly inside presenter methods. Any method not explicitly
819
910
  defined on `Presenter` is resolved via `method_missing` on the first call, which
@@ -825,6 +916,12 @@ The original wrapped object is accessible via `__getobj__` (standard
825
916
 
826
917
  The serialization context is accessible via the private method `__ctx__`.
827
918
 
919
+ Objects are wrapped in the `Presenter` only when the serializer's `Presenter`
920
+ class (or an inherited one) actually defines custom methods. Loading the
921
+ plugin in a base serializer adds no overhead to serializers that don't
922
+ customize their presenters — their attribute values, batch loaders and value
923
+ callables keep receiving the raw objects.
924
+
828
925
  ### Plugin :string_modifiers
829
926
 
830
927
  Allows to specify modifiers as strings.
@@ -973,7 +1070,8 @@ end
973
1070
  ### Plugin :explicit_many_option
974
1071
 
975
1072
  The plugin requires adding a `:many` option when adding relationships
976
- (attributes with the `:serializer` option).
1073
+ (attributes with the `:serializer` option or a block defining a nested
1074
+ serializer).
977
1075
 
978
1076
  Adding this plugin makes it clearer to find if some relationship is an array or
979
1077
  a single object.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.38.0
1
+ 0.39.0
@@ -45,10 +45,10 @@ class Serega
45
45
  # @option opts [Symbol] :method Object method name to fetch attribute value
46
46
  # @option opts [Hash] :delegate Allows to fetch value from nested object
47
47
  # @option opts [Boolean] :hide Specify `true` to not serialize this attribute by default
48
- # @option opts [Boolean] :many Specifies has_many relationship. By default is detected via object.is_a?(Enumerable)
48
+ # @option opts [Boolean] :many Specifies has_many relationship. By default is detected via object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
49
49
  # @option opts [Proc, #call] :value Custom block or callable to find attribute value
50
50
  # @option opts [Serega, Proc] :serializer Relationship serializer class. Use `proc { MySerializer }` if serializers have cross references
51
- # @param block [Proc] Custom block to find attribute value
51
+ # @param block [Proc] Defines attributes of a nested anonymous serializer
52
52
  #
53
53
  def initialize(name:, opts: {}, block: nil)
54
54
  serializer_class = self.class.serializer_class
@@ -88,7 +88,7 @@ class Serega
88
88
  # @return [Object] Serialized attribute value
89
89
  #
90
90
  def value(object, context, batches: nil)
91
- # Signatires should match allowed signatures in CheckOptValue and CheckBlock
91
+ # Signatures should match allowed signatures in CheckOptValue
92
92
  result =
93
93
  case value_block_signature
94
94
  when "1" then value_block.call(object)
@@ -133,8 +133,7 @@ class Serega
133
133
  end
134
134
 
135
135
  def prepare_value_block
136
- init_block \
137
- || init_opts[:value] \
136
+ init_opts[:value] \
138
137
  || prepare_const_block \
139
138
  || prepare_delegate_block \
140
139
  || prepare_batch_loader_block \
@@ -175,7 +174,73 @@ class Serega
175
174
  end
176
175
 
177
176
  def prepare_serializer
178
- init_opts[:serializer]
177
+ block = init_block
178
+ return init_opts[:serializer] unless block
179
+
180
+ prepare_block_serializer(block)
181
+ end
182
+
183
+ # Builds an anonymous nested serializer from the attribute block.
184
+ #
185
+ # The serializer is a regular subclass of an explicitly chosen base —
186
+ # the :base_serializer attribute option or `config.base_serializer` —
187
+ # so it inherits everything the base has: plugins, config, attributes,
188
+ # batch loaders, the preload handler. Nested values can be any objects,
189
+ # which is why the base is chosen explicitly instead of inheriting from
190
+ # the current serializer. Its `inspect` is overridden to
191
+ # `CurrentSerializer.<attribute_name>` so error messages and debugging
192
+ # output point back to the defining attribute.
193
+ # Guards against cyclic definitions before building: inheriting the base
194
+ # serializer copies its attributes, and copying a block attribute builds
195
+ # its nested serializer — meeting the same block again while it is being
196
+ # built means the base serializer (transitively) contains the block
197
+ # attribute being built, so inheriting from it would recurse forever.
198
+ # The "in progress" mark is kept on the block itself — its identity is
199
+ # what defines the cycle (the same Proc object is shared by all copies
200
+ # of the attribute).
201
+ def prepare_block_serializer(block)
202
+ if block.instance_variable_defined?(:@serega_building_nested_serializer)
203
+ raise SeregaError,
204
+ "Can not define a nested serializer for attribute :#{name} —" \
205
+ " its base serializer #{block_base_serializer.inspect} (transitively)" \
206
+ " contains this same block attribute (cyclic definition)"
207
+ end
208
+
209
+ block.instance_variable_set(:@serega_building_nested_serializer, true)
210
+ begin
211
+ build_block_serializer(block)
212
+ ensure
213
+ block.remove_instance_variable(:@serega_building_nested_serializer)
214
+ end
215
+ end
216
+
217
+ def build_block_serializer(block)
218
+ label = "#{self.class.serializer_class.inspect}.<#{name}>"
219
+
220
+ serializer = Class.new(block_base_serializer) do
221
+ define_singleton_method(:inspect) { label }
222
+ define_singleton_method(:to_s) { label }
223
+ instance_exec(&block)
224
+ end
225
+
226
+ # An empty nested serializer means the block was intended as an
227
+ # old-style value block — raise the explaining error.
228
+ raise SeregaError, SeregaValidations::Attribute::CheckBlock::ERROR_MESSAGE if serializer.attributes.empty?
229
+
230
+ serializer
231
+ end
232
+
233
+ # Base class for the nested serializer. Must be chosen explicitly —
234
+ # usually a settings-only serializer holding plugins and configuration
235
+ # (e.g. `config.base_serializer = self` in an application base class).
236
+ def block_base_serializer
237
+ base_serializer = init_opts[:base_serializer] || config.base_serializer
238
+ return base_serializer if base_serializer
239
+
240
+ raise SeregaError,
241
+ "Attribute block requires a base serializer for the nested serializer." \
242
+ " Provide the `base_serializer: <SerializerClass>` attribute option" \
243
+ " or set `config.base_serializer = <SerializerClass>`"
179
244
  end
180
245
 
181
246
  def prepare_method_name
@@ -250,16 +315,26 @@ class Serega
250
315
 
251
316
  # Auto-preload the delegated association
252
317
  if config.auto_preload.fetch(:has_delegate_option) && init_opts[:delegate]
253
- return init_opts[:delegate][:to]
318
+ return auto_preload_value(init_opts[:delegate][:to])
254
319
  end
255
320
 
256
321
  # Auto-preload the nested serializer's association
257
- if config.auto_preload.fetch(:has_serializer_option) && init_opts[:serializer] && !init_opts.key?(:batch)
258
- return method_name
322
+ # (a block defines a nested serializer, same as the :serializer option)
323
+ if config.auto_preload.fetch(:has_serializer_option) && (init_opts[:serializer] || init_block) && !init_opts.key?(:batch)
324
+ return auto_preload_value(method_name)
259
325
  end
260
326
 
261
327
  nil
262
328
  end
329
+
330
+ # Skips auto-preloading of methods that return the serialized object
331
+ # itself (:itself by default) — they are not associations,
332
+ # so preloading them would fail or make no sense.
333
+ def auto_preload_value(preload_method)
334
+ return nil if config.auto_preload_excluded_methods.include?(preload_method.to_sym)
335
+
336
+ preload_method
337
+ end
263
338
  end
264
339
 
265
340
  extend Serega::SeregaHelpers::SerializerClassHelper
@@ -18,6 +18,7 @@ class Serega
18
18
  # It handles this cases:
19
19
  # - `attribute :foo, batch: true`
20
20
  # - `attribute :foo, batch: FooLoader`
21
+ # - `attribute :foo, batch: :foo_loader`
21
22
  # - `attribute :foo, batch: { id: :foo_id }`
22
23
  # - `attribute :foo, batch: { use: FooLoader, id: foo_id }`
23
24
  # - `attribute :foo, batch: { use: :foo_loader, id: foo_id }`
@@ -32,6 +33,9 @@ class Serega
32
33
  serializer_class.batch(attribute_name, batch_opt)
33
34
  batch_name = attribute_name
34
35
  batch_id_method = default_method
36
+ elsif batch_opt.is_a?(Symbol) || batch_opt.is_a?(String) # ex: `batch: :foo_loader`
37
+ batch_name = batch_opt.to_sym
38
+ batch_id_method = default_method
35
39
  else
36
40
  use = batch_opt[:use]
37
41
  batch_id_method = batch_opt[:id] || default_method
data/lib/serega/config.rb CHANGED
@@ -24,6 +24,7 @@ class Serega
24
24
  default
25
25
  preload
26
26
  batch
27
+ base_serializer
27
28
  ].freeze,
28
29
  serialize_keys: %i[context many].freeze,
29
30
  check_attribute_name: true,
@@ -31,8 +32,10 @@ class Serega
31
32
  delegate_default_allow_nil: false,
32
33
  max_cached_plans_per_serializer_count: 0,
33
34
  auto_preload: {has_delegate_option: false, has_serializer_option: false},
35
+ auto_preload_excluded_methods: %i[itself].freeze,
34
36
  hide_by_default: false,
35
- batch_id_option: :id
37
+ batch_id_option: :id,
38
+ base_serializer: nil
36
39
  }.freeze
37
40
  # :nocov:
38
41
 
@@ -114,6 +117,50 @@ class Serega
114
117
  opts[:delegate_default_allow_nil] = value
115
118
  end
116
119
 
120
+ # Returns :auto_preload_excluded_methods config option — methods that are
121
+ # never auto-preloaded, as they return the serialized object itself and
122
+ # not an association
123
+ # @return [Array<Symbol>] Current :auto_preload_excluded_methods config option
124
+ def auto_preload_excluded_methods
125
+ opts.fetch(:auto_preload_excluded_methods)
126
+ end
127
+
128
+ # Sets :auto_preload_excluded_methods config option — methods that are
129
+ # never auto-preloaded. Applies to the `:method` option of attributes
130
+ # with a serializer and to the `delegate: {to: ...}` option.
131
+ #
132
+ # @param value [Array<Symbol>] Method names to skip in auto-preload
133
+ #
134
+ # @return [Array<Symbol>] New :auto_preload_excluded_methods config option
135
+ def auto_preload_excluded_methods=(value)
136
+ unless value.is_a?(Array) && value.all?(Symbol)
137
+ raise SeregaError, "Must be an Array of Symbols, #{value.inspect} provided"
138
+ end
139
+
140
+ opts[:auto_preload_excluded_methods] = value
141
+ end
142
+
143
+ # Returns :base_serializer config option — the parent class for nested
144
+ # serializers defined with attribute blocks
145
+ # @return [Class, nil] Current :base_serializer config option
146
+ def base_serializer
147
+ opts.fetch(:base_serializer)
148
+ end
149
+
150
+ # Sets :base_serializer config option — the parent class for nested
151
+ # serializers defined with attribute blocks. Usually a settings-only
152
+ # serializer, e.g. `config.base_serializer = self` in an application
153
+ # base serializer class.
154
+ #
155
+ # @param value [Class] Serega or its subclass
156
+ #
157
+ # @return [Class] New :base_serializer config option
158
+ def base_serializer=(value)
159
+ raise SeregaError, "Must be a Serega subclass, #{value.inspect} provided" if !value.is_a?(Class) || !(value <= Serega)
160
+
161
+ opts[:base_serializer] = value
162
+ end
163
+
117
164
  # Returns :hide_by_default config option
118
165
  # @return [Boolean, Symbol] Current :hide_by_default config option
119
166
  def hide_by_default
@@ -97,14 +97,14 @@ class Serega
97
97
  end
98
98
 
99
99
  # How to serialize `object`, deciding whether the result is a collection or a
100
- # single object and reading `Enumerable` only once:
100
+ # single object and checking the object type only once:
101
101
  # - :many — `many` is on and the object is a collection
102
102
  # - :many_for_one — `many` is on but a sole object was given (wrap it, don't raise)
103
103
  # - :one — serialize the object on its own
104
104
  def serialize_mode(object)
105
105
  case many
106
- when NilClass then object.is_a?(Enumerable) ? :many : :one
107
- when TrueClass then object.is_a?(Enumerable) ? :many : :many_for_one
106
+ when NilClass then SeregaUtils::CollectionDetector.call(object) ? :many : :one
107
+ when TrueClass then SeregaUtils::CollectionDetector.call(object) ? :many : :many_for_one
108
108
  else :one # many == false
109
109
  end
110
110
  end
@@ -91,6 +91,7 @@ class Serega
91
91
  # The underlying records to preload onto. The :presenter plugin wraps every
92
92
  # serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
93
93
  # the real records, so unwrap them via #__getobj__ when presenter is used.
94
+ # Objects are wrapped only when the Presenter class has custom methods.
94
95
  #
95
96
  # @param serializer_class [Class<Serega>] Current serializer class
96
97
  # @param objects [Array] objects serialized at the current level
@@ -99,6 +100,7 @@ class Serega
99
100
  #
100
101
  def self.records(serializer_class, objects)
101
102
  return objects unless serializer_class.plugin_used?(:presenter)
103
+ return objects unless serializer_class.custom_presenter?
102
104
 
103
105
  objects.map(&:__getobj__)
104
106
  end
@@ -6,7 +6,8 @@ class Serega
6
6
  # Plugin :explicit_many_option
7
7
  #
8
8
  # Plugin requires to add :many option when adding relationships
9
- # (relationships are attributes with :serializer option specified)
9
+ # (relationships are attributes with the :serializer option or a block
10
+ # defining a nested serializer)
10
11
  #
11
12
  # Adding this plugin makes it clearer to find if relationship returns array or single object
12
13
  #
@@ -56,7 +57,7 @@ class Serega
56
57
  def check_opts
57
58
  super
58
59
 
59
- CheckOptMany.call(opts)
60
+ CheckOptMany.call(opts, block)
60
61
  end
61
62
  end
62
63
  end
@@ -10,23 +10,25 @@ class Serega
10
10
  class << self
11
11
  #
12
12
  # Checks attribute :many option must be provided with relations
13
+ # (attributes with the :serializer option or a block defining
14
+ # a nested serializer)
13
15
  #
14
16
  # @param opts [Hash] Attribute options
17
+ # @param block [nil, Proc] Attribute block
15
18
  #
16
19
  # @raise [SeregaError] Attribute validation error
17
20
  #
18
21
  # @return [void]
19
22
  #
20
- def call(opts)
21
- serializer = opts[:serializer]
22
- return unless serializer
23
+ def call(opts, block = nil)
24
+ return if !opts[:serializer] && !block
23
25
 
24
26
  many_option_exists = opts.key?(:many)
25
27
  return if many_option_exists
26
28
 
27
29
  raise SeregaError,
28
30
  "Attribute option :many [Boolean] must be provided" \
29
- " for attributes with :serializer option"
31
+ " for attributes with :serializer option or a block"
30
32
  end
31
33
  end
32
34
  end
@@ -15,13 +15,19 @@ class Serega
15
15
  # - The original object is accessible via __getobj__ (standard SimpleDelegator API).
16
16
  # - The serialization context is accessible via the private method __ctx__.
17
17
  #
18
+ # Objects are wrapped in the Presenter only when the serializer's Presenter
19
+ # class (or an inherited one) was actually extended with custom methods —
20
+ # a bare `plugin :presenter` adds no wrapping overhead. The check is
21
+ # denormalized: `SerializerClass.custom_presenter?` is asked once per
22
+ # object serializer and the result is reused for the whole level.
23
+ #
18
24
  # class UserSerializer < Serega
19
25
  # plugin :presenter
20
26
  #
21
27
  # attribute :name
22
28
  # attribute :role
23
29
  #
24
- # class Presenter
30
+ # presenter do
25
31
  # def name
26
32
  # [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
27
33
  # end
@@ -31,6 +37,9 @@ class Serega
31
37
  # end
32
38
  # end
33
39
  # end
40
+ #
41
+ # The `presenter do ... end` block is evaluated inside the serializer's own
42
+ # Presenter class, so multiple blocks accumulate.
34
43
  module Presenter
35
44
  # @return [Symbol] Plugin name
36
45
  def self.plugin_name
@@ -62,6 +71,11 @@ class Serega
62
71
  presenter_class = Class.new(Presenter)
63
72
  presenter_class.serializer_class = serializer_class
64
73
  serializer_class.const_set(:Presenter, presenter_class)
74
+
75
+ # The presenter's unwrap method returns the serialized object itself,
76
+ # not an association — it must never be auto-preloaded.
77
+ config = serializer_class.config
78
+ config.auto_preload_excluded_methods = config.auto_preload_excluded_methods | [:__getobj__]
65
79
  end
66
80
 
67
81
  # Presenter class
@@ -97,6 +111,44 @@ class Serega
97
111
  extend SeregaHelpers::SerializerClassHelper
98
112
  extend Forwardable
99
113
  include InstanceMethods
114
+
115
+ # Tracks whether user code was added to the Presenter class.
116
+ #
117
+ # These singleton methods are defined after the base class body above,
118
+ # so the plugin's own includes do not mark the base class as modified.
119
+ # Lazy delegators defined by #method_missing do mark the class, but
120
+ # they can appear only on presenters that are already wrapping.
121
+ class << self
122
+ #
123
+ # Checks if this Presenter class (or an inherited one) was extended
124
+ # with custom user code and therefore objects must be wrapped
125
+ #
126
+ # @return [Boolean] whether custom presenter methods were defined
127
+ #
128
+ def modified?
129
+ return true if defined?(@modified)
130
+ return false if equal?(Presenter) # the plugin's base class — the walk stops here
131
+
132
+ superclass.modified?
133
+ end
134
+
135
+ def include(*modules)
136
+ @modified = true
137
+ super
138
+ end
139
+
140
+ def prepend(*modules)
141
+ @modified = true
142
+ super
143
+ end
144
+
145
+ private
146
+
147
+ def method_added(name)
148
+ @modified = true
149
+ super
150
+ end
151
+ end
100
152
  end
101
153
 
102
154
  #
@@ -105,6 +157,36 @@ class Serega
105
157
  # @see Serega
106
158
  #
107
159
  module ClassMethods
160
+ #
161
+ # Defines presenter methods — evaluates the block inside the
162
+ # serializer's own Presenter class. Multiple blocks accumulate.
163
+ #
164
+ # presenter do
165
+ # def name
166
+ # [first_name, last_name].compact.join(" ")
167
+ # end
168
+ # end
169
+ #
170
+ # @return [void]
171
+ #
172
+ def presenter(&block)
173
+ raise SeregaError, "Provide a block with presenter methods: `presenter do ... end`" unless block
174
+
175
+ self::Presenter.class_exec(&block)
176
+ nil
177
+ end
178
+
179
+ #
180
+ # Checks if the serializer's Presenter class (or an inherited one) was
181
+ # extended with custom user code. When it was not, serialized objects
182
+ # are not wrapped in the Presenter at all.
183
+ #
184
+ # @return [Boolean] whether custom presenter methods were defined
185
+ #
186
+ def custom_presenter?
187
+ self::Presenter.modified?
188
+ end
189
+
108
190
  private
109
191
 
110
192
  def inherited(subclass)
@@ -122,14 +204,25 @@ class Serega
122
204
  # @see Serega::SeregaObjectSerializer
123
205
  #
124
206
  module SeregaObjectSerializerInstanceMethods
207
+ # The custom-presenter check is made once per object serializer here
208
+ # and its result is reused for every enqueued chunk of the level.
209
+ def initialize(**opts)
210
+ super
211
+ @wrap_in_presenter = self.class.serializer_class.custom_presenter?
212
+ end
213
+
125
214
  private
126
215
 
127
216
  #
128
217
  # Wraps each serialized object in Presenter.new(object, ctx) before it is
129
218
  # enqueued, so the whole level — value resolution and batch loaders alike —
130
- # sees presenters.
219
+ # sees presenters. Objects are not wrapped when the Presenter class has
220
+ # no custom methods — such wrapping would only add overhead and break
221
+ # class checks (object.is_a?, Hash === object) without changing anything.
131
222
  #
132
223
  def enqueue(objects)
224
+ return super unless @wrap_in_presenter
225
+
133
226
  presenter = self.class.serializer_class::Presenter
134
227
  presenters = objects.map { |object| presenter.new(object, context) }
135
228
  super(presenters)
@@ -244,7 +244,7 @@ class Serega
244
244
  return opts[:root] if opts.key?(:root)
245
245
 
246
246
  root = self.class.config.root
247
- (opts.fetch(:many) { object.is_a?(Enumerable) }) ? root.many : root.one
247
+ (opts.fetch(:many) { SeregaUtils::CollectionDetector.call(object) }) ? root.many : root.one
248
248
  end
249
249
  end
250
250
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaUtils
5
+ #
6
+ # Utility to check if an object should be serialized as a collection.
7
+ #
8
+ # Hashes and Structs are Enumerable, but enumerate their own member
9
+ # values, so they are treated as single objects.
10
+ #
11
+ class CollectionDetector
12
+ class << self
13
+ #
14
+ # Checks if provided object is a collection of objects
15
+ #
16
+ # @param object [Object] Serialized object
17
+ #
18
+ # @return [Boolean] whether object should be serialized as a collection
19
+ #
20
+ def call(object)
21
+ object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -10,25 +10,30 @@ class Serega
10
10
  # Attribute `block` parameter validator
11
11
  #
12
12
  class CheckBlock
13
+ # Explains the changed attribute block behavior. Shown when the block
14
+ # looks like an old-style value block — it accepts parameters or
15
+ # defines no attributes.
16
+ ERROR_MESSAGE =
17
+ "Attribute block now defines a nested serializer:" \
18
+ " it is executed in the context of a new serializer class and must define its attributes." \
19
+ " Defining the attribute value with a block is not supported anymore," \
20
+ " use the `value: <callable>` option instead."
21
+
13
22
  class << self
14
23
  #
15
24
  # Checks block parameter provided with attribute.
16
- # Must have up to two arguments - object and context. Context can be
17
- # also provided as keyword argument :ctx.
18
- #
19
- # @example without arguments
20
- # attribute(:email) { CONSTANT_EMAIL }
21
- #
22
- # @example with one argument
23
- # attribute(:email) { |obj| obj.confirmed_email }
24
25
  #
25
- # @example with two arguments
26
- # attribute(:email) { |obj, context| context['is_current'] ? obj.email : nil }
26
+ # The block defines attributes of a nested anonymous serializer, so
27
+ # it is executed in the context of that serializer and must accept
28
+ # no parameters.
27
29
  #
28
- # @example with one argument and keyword context
29
- # attribute(:email) { |obj, ctx:| obj.email if ctx[:show] }
30
+ # @example
31
+ # attribute :statistics, method: :itself do
32
+ # attribute :likes_count
33
+ # attribute :comments_count
34
+ # end
30
35
  #
31
- # @param block [Proc] Block that returns serialized attribute value
36
+ # @param block [Proc] Block that defines nested serializer attributes
32
37
  #
33
38
  # @raise [SeregaError] SeregaError that block has invalid arguments
34
39
  #
@@ -37,48 +42,8 @@ class Serega
37
42
  def call(block)
38
43
  return unless block
39
44
 
40
- check_block(block)
41
- end
42
-
43
- private
44
-
45
- def check_block(block)
46
- signature = SeregaUtils::MethodSignature.call(block, pos_limit: 2, keyword_args: [:ctx, :batches])
47
-
48
- raise SeregaError, signature_error unless valid_signature?(signature)
49
- end
50
-
51
- def valid_signature?(signature)
52
- case signature
53
- when "0" # no parameters
54
- true
55
- when "1" # call(object)
56
- true
57
- when "1_ctx" # call(object, ctx:)
58
- true
59
- when "1_batches" # call(object, batches:)
60
- true
61
- when "1_batches_ctx" # call(object, batches:, ctx:)
62
- true
63
- when "2" # call(object, context)
64
- true
65
- when "2_batches_ctx" # call(object, context, batches:, ctx:) (proc with no params)
66
- true
67
- else
68
- false
69
- end
70
- end
71
-
72
- def signature_error
73
- <<~ERROR.strip
74
- Invalid attribute block parameters, valid parameters signatures:
75
- - () # no parameters
76
- - (object) # one positional parameter
77
- - (object, ctx:) # one positional parameter and :ctx keyword
78
- - (object, batches:) # one positional parameter and :batches keyword
79
- - (object, ctx:, batches:) # one positional parameter, :ctx, and :batches keywords
80
- - (object, context) # two positional parameters
81
- ERROR
45
+ signature = SeregaUtils::MethodSignature.call(block, pos_limit: 0)
46
+ raise SeregaError, ERROR_MESSAGE unless signature == "0"
82
47
  end
83
48
  end
84
49
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaValidations
5
+ module Attribute
6
+ #
7
+ # Attribute `:base_serializer` option validator
8
+ #
9
+ class CheckOptBaseSerializer
10
+ class << self
11
+ #
12
+ # Checks attribute :base_serializer option. It specifies the parent
13
+ # class for the nested serializer defined with the attribute block,
14
+ # so it makes sense only when a block is provided.
15
+ #
16
+ # @param opts [Hash] Attribute options
17
+ # @param block [nil, Proc] Attribute block (defines a nested serializer)
18
+ #
19
+ # @raise [SeregaError] SeregaError that option has invalid value
20
+ #
21
+ # @return [void]
22
+ #
23
+ def call(opts, block = nil)
24
+ return unless opts.key?(:base_serializer)
25
+
26
+ raise SeregaError, "Option :base_serializer can be used only with a block" unless block
27
+
28
+ value = opts[:base_serializer]
29
+ return if value.is_a?(Class) && (value <= Serega)
30
+
31
+ raise SeregaError, "Invalid option :base_serializer => #{value.inspect}. Must be a Serega subclass"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -17,11 +17,11 @@ class Serega
17
17
  #
18
18
  # @return [void]
19
19
  #
20
- def call(serializer_class, opts, block)
20
+ def call(serializer_class, opts)
21
21
  return unless opts.key?(:batch)
22
22
 
23
23
  check_opt_batch(opts, serializer_class)
24
- check_usage_with_other_params(opts, block)
24
+ check_usage_with_other_params(opts)
25
25
  end
26
26
 
27
27
  private
@@ -72,22 +72,22 @@ class Serega
72
72
  Utils::CheckAllowedKeys.call(batch_opts, %i[use id], :batch)
73
73
  end
74
74
 
75
- def check_usage_with_other_params(opts, block)
75
+ def check_usage_with_other_params(opts)
76
76
  batch = opts[:batch]
77
77
  use_id = batch.is_a?(Hash) && batch.key?(:id)
78
78
  use_multiple = batch.is_a?(Hash) && (Array(batch[:use]).size > 1)
79
- value_added = opts.key?(:value) || block
79
+ value_added = opts.key?(:value)
80
80
 
81
81
  if use_multiple && use_id
82
82
  raise SeregaError, "Option `batch.id` should not be used with multiple loaders provided in `batch.use`"
83
83
  end
84
84
 
85
85
  if use_multiple && !value_added
86
- raise SeregaError, "Attribute :value option or block should be provided when selecting multiple batch loaders"
86
+ raise SeregaError, "Attribute :value option should be provided when selecting multiple batch loaders"
87
87
  end
88
88
 
89
89
  if use_id && value_added
90
- raise SeregaError, "Option `batch.id` should not be used when :value or block provided directly"
90
+ raise SeregaError, "Option `batch.id` should not be used when :value option provided directly"
91
91
  end
92
92
 
93
93
  raise SeregaError, "Option :batch can not be used together with option :method" if opts.key?(:method)
@@ -17,19 +17,18 @@ class Serega
17
17
  #
18
18
  # @return [void]
19
19
  #
20
- def call(opts, block = nil)
20
+ def call(opts)
21
21
  return unless opts.key?(:const)
22
22
 
23
- check_usage_with_other_params(opts, block)
23
+ check_usage_with_other_params(opts)
24
24
  end
25
25
 
26
26
  private
27
27
 
28
- def check_usage_with_other_params(opts, block)
28
+ def check_usage_with_other_params(opts)
29
29
  raise SeregaError, "Option :const can not be used together with option :method" if opts.key?(:method)
30
30
  raise SeregaError, "Option :const can not be used together with option :value" if opts.key?(:value)
31
31
  raise SeregaError, "Option :const can not be used together with option :batch" if opts.key?(:batch)
32
- raise SeregaError, "Option :const can not be used together with block" if block
33
32
  end
34
33
  end
35
34
  end
@@ -18,11 +18,11 @@ class Serega
18
18
  #
19
19
  # @return [void]
20
20
  #
21
- def call(opts, block = nil)
21
+ def call(opts)
22
22
  return unless opts.key?(:delegate)
23
23
 
24
24
  check_opt_delegate(opts)
25
- check_usage_with_other_params(opts, block)
25
+ check_usage_with_other_params(opts)
26
26
  end
27
27
 
28
28
  private
@@ -56,12 +56,11 @@ class Serega
56
56
  Utils::CheckAllowedKeys.call(delegate_opts, %i[to method allow_nil], :delegate)
57
57
  end
58
58
 
59
- def check_usage_with_other_params(opts, block)
59
+ def check_usage_with_other_params(opts)
60
60
  raise SeregaError, "Option :delegate can not be used together with option :method" if opts.key?(:method)
61
61
  raise SeregaError, "Option :delegate can not be used together with option :const" if opts.key?(:const)
62
62
  raise SeregaError, "Option :delegate can not be used together with option :value" if opts.key?(:value)
63
63
  raise SeregaError, "Option :delegate can not be used together with option :batch" if opts.key?(:batch)
64
- raise SeregaError, "Option :delegate can not be used together with block" if block
65
64
  end
66
65
  end
67
66
  end
@@ -12,28 +12,29 @@ class Serega
12
12
  # Checks attribute :many option
13
13
  #
14
14
  # @param opts [Hash] Attribute options
15
+ # @param block [nil, Proc] Attribute block
15
16
  #
16
17
  # @raise [SeregaError] SeregaError that option has invalid value
17
18
  #
18
19
  # @return [void]
19
20
  #
20
- def call(opts)
21
+ def call(opts, block = nil)
21
22
  return unless opts.key?(:many)
22
23
 
23
- check_many_option_makes_sence(opts)
24
+ check_many_option_makes_sence(opts, block)
24
25
  Utils::CheckOptIsBool.call(opts, :many)
25
26
  end
26
27
 
27
28
  private
28
29
 
29
- def check_many_option_makes_sence(opts)
30
- return if many_option_makes_sence?(opts)
30
+ def check_many_option_makes_sence(opts, block)
31
+ return if many_option_makes_sence?(opts, block)
31
32
 
32
- raise SeregaError, "Option :many can be provided only together with :serializer or :batch option"
33
+ raise SeregaError, "Option :many can be provided only together with :serializer, :batch option or a block"
33
34
  end
34
35
 
35
- def many_option_makes_sence?(opts)
36
- opts[:serializer] || opts[:batch]
36
+ def many_option_makes_sence?(opts, block)
37
+ opts[:serializer] || opts[:batch] || block
37
38
  end
38
39
  end
39
40
  end
@@ -17,20 +17,19 @@ class Serega
17
17
  #
18
18
  # @return [void]
19
19
  #
20
- def call(opts, block = nil)
20
+ def call(opts)
21
21
  return unless opts.key?(:method)
22
22
 
23
- check_usage_with_other_params(opts, block)
23
+ check_usage_with_other_params(opts)
24
24
  Utils::CheckOptIsStringOrSymbol.call(opts, :method)
25
25
  end
26
26
 
27
27
  private
28
28
 
29
- def check_usage_with_other_params(opts, block)
29
+ def check_usage_with_other_params(opts)
30
30
  raise SeregaError, "Option :method can not be used together with option :const" if opts.key?(:const)
31
31
  raise SeregaError, "Option :method can not be used together with option :value" if opts.key?(:value)
32
32
  raise SeregaError, "Option :method can not be used together with option :batch" if opts.key?(:batch)
33
- raise SeregaError, "Option :method can not be used together with block" if block
34
33
  end
35
34
  end
36
35
  end
@@ -12,14 +12,17 @@ class Serega
12
12
  # Checks attribute :serializer option
13
13
  #
14
14
  # @param opts [Hash] Attribute options
15
+ # @param block [nil, Proc] Attribute block (defines a nested serializer)
15
16
  #
16
17
  # @raise [SeregaError] SeregaError that option has invalid value
17
18
  #
18
19
  # @return [void]
19
20
  #
20
- def call(opts)
21
+ def call(opts, block = nil)
21
22
  return unless opts.key?(:serializer)
22
23
 
24
+ raise SeregaError, "Option :serializer can not be used together with block" if block
25
+
23
26
  value = opts[:serializer]
24
27
  return if valid_serializer?(value)
25
28
 
@@ -17,20 +17,19 @@ class Serega
17
17
  #
18
18
  # @return [void]
19
19
  #
20
- def call(opts, block = nil)
20
+ def call(opts)
21
21
  return unless opts.key?(:value)
22
22
 
23
- check_usage_with_other_params(opts, block)
23
+ check_usage_with_other_params(opts)
24
24
 
25
25
  check_value(opts[:value])
26
26
  end
27
27
 
28
28
  private
29
29
 
30
- def check_usage_with_other_params(opts, block)
30
+ def check_usage_with_other_params(opts)
31
31
  raise SeregaError, "Option :value can not be used together with option :method" if opts.key?(:method)
32
32
  raise SeregaError, "Option :value can not be used together with option :const" if opts.key?(:const)
33
- raise SeregaError, "Option :value can not be used together with block" if block
34
33
  end
35
34
 
36
35
  def check_value(value)
@@ -59,15 +59,16 @@ class Serega
59
59
  def check_opts
60
60
  Utils::CheckAllowedKeys.call(opts, allowed_opts_keys, :attribute)
61
61
 
62
- Attribute::CheckOptConst.call(opts, block)
63
- Attribute::CheckOptDelegate.call(opts, block)
62
+ Attribute::CheckOptBaseSerializer.call(opts, block)
63
+ Attribute::CheckOptConst.call(opts)
64
+ Attribute::CheckOptDelegate.call(opts)
64
65
  Attribute::CheckOptHide.call(opts)
65
- Attribute::CheckOptMethod.call(opts, block)
66
- Attribute::CheckOptMany.call(opts)
66
+ Attribute::CheckOptMethod.call(opts)
67
+ Attribute::CheckOptMany.call(opts, block)
67
68
  Attribute::CheckOptPreload.call(opts)
68
- Attribute::CheckOptSerializer.call(opts)
69
- Attribute::CheckOptValue.call(opts, block)
70
- Attribute::CheckOptBatch.call(self.class.serializer_class, opts, block)
69
+ Attribute::CheckOptSerializer.call(opts, block)
70
+ Attribute::CheckOptValue.call(opts)
71
+ Attribute::CheckOptBatch.call(self.class.serializer_class, opts)
71
72
  end
72
73
 
73
74
  def check_block
data/lib/serega.rb CHANGED
@@ -19,6 +19,7 @@ end
19
19
 
20
20
  require_relative "serega/errors"
21
21
  require_relative "serega/helpers/serializer_class_helper"
22
+ require_relative "serega/utils/collection_detector"
22
23
  require_relative "serega/utils/enum_deep_dup"
23
24
  require_relative "serega/utils/enum_deep_freeze"
24
25
  require_relative "serega/utils/method_signature"
@@ -40,6 +41,7 @@ require_relative "serega/validations/utils/check_opt_is_hash"
40
41
  require_relative "serega/validations/utils/check_opt_is_string_or_symbol"
41
42
  require_relative "serega/validations/attribute/check_block"
42
43
  require_relative "serega/validations/attribute/check_name"
44
+ require_relative "serega/validations/attribute/check_opt_base_serializer"
43
45
  require_relative "serega/validations/attribute/check_opt_const"
44
46
  require_relative "serega/validations/attribute/check_opt_hide"
45
47
  require_relative "serega/validations/attribute/check_opt_delegate"
@@ -168,7 +170,19 @@ class Serega
168
170
  #
169
171
  # @param name [Symbol] Attribute name. Attribute value will be found by executing `object.<name>`
170
172
  # @param opts [Hash] Options to serialize attribute
171
- # @param block [Proc] Custom block to find attribute value. Accepts object and context.
173
+ # @param block [Proc] Defines attributes of a nested anonymous serializer.
174
+ # The block is executed in the context of that serializer, so everything
175
+ # available in a serializer class body can be used inside. The attribute
176
+ # value (found by name/:method/:value/:delegate/:const/:batch as usual)
177
+ # is serialized with this nested serializer. The nested serializer
178
+ # inherits from the `:base_serializer` attribute option or
179
+ # `config.base_serializer` (one of them is required).
180
+ #
181
+ # @example
182
+ # attribute :statistics, method: :itself do
183
+ # attribute :likes_count
184
+ # attribute :comments_count
185
+ # end
172
186
  #
173
187
  # @return [Serega::SeregaAttribute] Added attribute
174
188
  #
@@ -254,7 +268,7 @@ class Serega
254
268
  # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
255
269
  # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
256
270
  # @option opts [Hash] :context Serialization context
257
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
271
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
258
272
  #
259
273
  # @return [Hash] Serialization result
260
274
  #
@@ -275,7 +289,7 @@ class Serega
275
289
  # @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
276
290
  # @option opts [Boolean] :validate Validates provided modifiers (Default is true)
277
291
  # @option opts [Hash] :context Serialization context
278
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
292
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
279
293
  #
280
294
  # @return [Data, Array<Data>, nil] Serialization result as Data object(s)
281
295
  #
@@ -409,7 +423,7 @@ class Serega
409
423
  # @param object [Object] Serialized object
410
424
  # @param opts [Hash, nil] Serializing options
411
425
  # @option opts [Hash] :context Serialization context
412
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
426
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
413
427
  #
414
428
  # @return [Hash] Serialization result
415
429
  #
@@ -431,7 +445,7 @@ class Serega
431
445
  # @param object [Object] Serialized object
432
446
  # @param opts [Hash, nil] Serializing options
433
447
  # @option opts [Hash] :context Serialization context
434
- # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
448
+ # @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`)
435
449
  #
436
450
  # @return [Data] Serialization result
437
451
  #
@@ -472,7 +486,7 @@ class Serega
472
486
 
473
487
  opts[:context] ||= {}
474
488
  opts[:level_queue] = SeregaEngine::LevelQueue.new
475
- opts[:many] = object.is_a?(Enumerable) unless opts.key?(:many)
489
+ opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
476
490
  opts[:plan] = plan
477
491
  opts
478
492
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.38.0
4
+ version: 0.39.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Glushkov
@@ -70,6 +70,7 @@ files:
70
70
  - lib/serega/plugins/root/root.rb
71
71
  - lib/serega/plugins/string_modifiers/parse_string_modifiers.rb
72
72
  - lib/serega/plugins/string_modifiers/string_modifiers.rb
73
+ - lib/serega/utils/collection_detector.rb
73
74
  - lib/serega/utils/enum_deep_dup.rb
74
75
  - lib/serega/utils/enum_deep_freeze.rb
75
76
  - lib/serega/utils/method_signature.rb
@@ -78,6 +79,7 @@ files:
78
79
  - lib/serega/utils/to_hash.rb
79
80
  - lib/serega/validations/attribute/check_block.rb
80
81
  - lib/serega/validations/attribute/check_name.rb
82
+ - lib/serega/validations/attribute/check_opt_base_serializer.rb
81
83
  - lib/serega/validations/attribute/check_opt_batch.rb
82
84
  - lib/serega/validations/attribute/check_opt_const.rb
83
85
  - lib/serega/validations/attribute/check_opt_delegate.rb