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 +4 -4
- data/README.md +124 -26
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +3 -3
- data/lib/serega/attribute_normalizer.rb +81 -6
- data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
- data/lib/serega/config.rb +48 -1
- data/lib/serega/object_serializer.rb +3 -3
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +2 -0
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +3 -2
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +6 -4
- data/lib/serega/plugins/presenter/presenter.rb +95 -2
- data/lib/serega/plugins/root/root.rb +1 -1
- data/lib/serega/utils/collection_detector.rb +26 -0
- data/lib/serega/validations/attribute/check_block.rb +20 -55
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +37 -0
- data/lib/serega/validations/attribute/check_opt_batch.rb +6 -6
- data/lib/serega/validations/attribute/check_opt_const.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_delegate.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_many.rb +8 -7
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -1
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -4
- data/lib/serega/validations/check_attribute_params.rb +8 -7
- data/lib/serega.rb +20 -6
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a223242ae8bce14755fa2cbb444ff78728d95fccb0ce3077c212706e39e4ddcc
|
|
4
|
+
data.tar.gz: b8e1d3bdf11a57091cfd22eb31bb017979382df4b2166b05f746d064dc486f6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
82
|
-
|
|
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
|
|
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
|
|
386
|
+
attribute :email, value: proc { |user, ctx|
|
|
316
387
|
user.email if ctx[:current_user] == user
|
|
317
|
-
|
|
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
|
-
#
|
|
343
|
-
attribute :comments_count, batch:
|
|
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
|
-
#
|
|
347
|
-
attribute :comments_count, batch:
|
|
416
|
+
# Equivalent — when the attribute name matches the loader name
|
|
417
|
+
attribute :comments_count, batch: true
|
|
348
418
|
|
|
349
|
-
#
|
|
350
|
-
attribute :comments_count, batch:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
1
|
+
0.39.0
|
data/lib/serega/attribute.rb
CHANGED
|
@@ -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]
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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
|
|
107
|
-
when TrueClass then
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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) {
|
|
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
|
-
#
|
|
26
|
-
#
|
|
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
|
|
29
|
-
# attribute
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
20
|
+
def call(opts)
|
|
21
21
|
return unless opts.key?(:const)
|
|
22
22
|
|
|
23
|
-
check_usage_with_other_params(opts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
20
|
+
def call(opts)
|
|
21
21
|
return unless opts.key?(:method)
|
|
22
22
|
|
|
23
|
-
check_usage_with_other_params(opts
|
|
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
|
|
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
|
|
20
|
+
def call(opts)
|
|
21
21
|
return unless opts.key?(:value)
|
|
22
22
|
|
|
23
|
-
check_usage_with_other_params(opts
|
|
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
|
|
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::
|
|
63
|
-
Attribute::
|
|
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
|
|
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
|
|
70
|
-
Attribute::CheckOptBatch.call(self.class.serializer_class, opts
|
|
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]
|
|
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] =
|
|
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.
|
|
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
|