serega 0.37.2 → 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 +87 -14
- data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
- data/lib/serega/config.rb +48 -1
- data/lib/serega/engine/level.rb +57 -0
- data/lib/serega/engine/level_queue.rb +54 -0
- data/lib/serega/{batch → engine}/loader.rb +1 -1
- data/lib/serega/object_serializer.rb +61 -56
- data/lib/serega/plan.rb +0 -9
- data/lib/serega/plan_point.rb +56 -5
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +21 -1
- 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/if/if.rb +9 -10
- data/lib/serega/plugins/presenter/presenter.rb +101 -5
- 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 +33 -29
- metadata +6 -5
- data/lib/serega/batch/attribute_loader.rb +0 -82
- data/lib/serega/batch/attribute_loaders.rb +0 -117
- data/lib/serega/batch/level.rb +0 -48
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)
|
|
@@ -109,9 +109,9 @@ class Serega
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
#
|
|
112
|
-
#
|
|
112
|
+
# Preloads declared for this attribute, passed through as provided.
|
|
113
113
|
#
|
|
114
|
-
# @return [
|
|
114
|
+
# @return [Object, nil] declared preloads, or nil when the attribute has none
|
|
115
115
|
#
|
|
116
116
|
def preloads
|
|
117
117
|
return @preloads if instance_variable_defined?(:@preloads)
|
|
@@ -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
|
|
@@ -199,16 +264,14 @@ class Serega
|
|
|
199
264
|
AttributeValueResolvers::BatchResolver.get(self.class.serializer_class, name, batch_opt)
|
|
200
265
|
end
|
|
201
266
|
|
|
267
|
+
# Batch loader names whose loaded data this attribute's value needs. Only
|
|
268
|
+
# explicit `:batch` attributes have any — everything else resolves its value
|
|
269
|
+
# from the record itself, so the list is empty (no loader to run).
|
|
202
270
|
def prepare_batch_loaders
|
|
203
271
|
batch_opt = init_opts[:batch]
|
|
204
272
|
return explicit_batch_loaders(batch_opt) if batch_opt
|
|
205
|
-
return FROZEN_EMPTY_ARRAY unless serializer || preloads
|
|
206
273
|
|
|
207
|
-
|
|
208
|
-
# (so objects are gathered per level and preloads run) — there is nothing
|
|
209
|
-
# to load, since the value comes from the attribute's own resolver. Mark
|
|
210
|
-
# them with the reserved marker; the batch machinery skips it.
|
|
211
|
-
[SeregaBatch::AUTO_BATCH_LOADER_NAME]
|
|
274
|
+
FROZEN_EMPTY_ARRAY
|
|
212
275
|
end
|
|
213
276
|
|
|
214
277
|
def explicit_batch_loaders(batch_opt)
|
|
@@ -252,16 +315,26 @@ class Serega
|
|
|
252
315
|
|
|
253
316
|
# Auto-preload the delegated association
|
|
254
317
|
if config.auto_preload.fetch(:has_delegate_option) && init_opts[:delegate]
|
|
255
|
-
return init_opts[:delegate][:to]
|
|
318
|
+
return auto_preload_value(init_opts[:delegate][:to])
|
|
256
319
|
end
|
|
257
320
|
|
|
258
321
|
# Auto-preload the nested serializer's association
|
|
259
|
-
|
|
260
|
-
|
|
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)
|
|
261
325
|
end
|
|
262
326
|
|
|
263
327
|
nil
|
|
264
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
|
|
265
338
|
end
|
|
266
339
|
|
|
267
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
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Batch feature main module
|
|
6
|
+
#
|
|
7
|
+
module SeregaEngine
|
|
8
|
+
#
|
|
9
|
+
# One serialization level: all objects serialized under a single plan and the
|
|
10
|
+
# result containers they fill. Objects from different parents that share a plan
|
|
11
|
+
# (e.g. every user's posts) accumulate into one level, so a named batch loader
|
|
12
|
+
# or preload runs once over the whole set. A deeper level (its own plan) loads
|
|
13
|
+
# separately.
|
|
14
|
+
#
|
|
15
|
+
class Level
|
|
16
|
+
# @param serializer [SeregaObjectSerializer] serializer that resolves this level
|
|
17
|
+
def initialize(serializer)
|
|
18
|
+
@serializer = serializer
|
|
19
|
+
@objects = []
|
|
20
|
+
@containers = []
|
|
21
|
+
@results = {}.compare_by_identity
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Array] Objects serialized at this level
|
|
25
|
+
attr_reader :objects
|
|
26
|
+
|
|
27
|
+
# @return [Array<Hash>] Result container per object (filled in place)
|
|
28
|
+
attr_reader :containers
|
|
29
|
+
|
|
30
|
+
# Accumulates a chunk of objects into this level, creating one empty result
|
|
31
|
+
# container per object. The containers are returned so the caller can hand
|
|
32
|
+
# them to its parent; they are filled in place later during #process.
|
|
33
|
+
#
|
|
34
|
+
# @param objects [Array] objects serialized at this level
|
|
35
|
+
# @return [Array<Hash>] the created containers, aligned with objects
|
|
36
|
+
def add(objects)
|
|
37
|
+
containers = Array.new(objects.size) { {} }
|
|
38
|
+
@objects.concat(objects)
|
|
39
|
+
@containers.concat(containers)
|
|
40
|
+
containers
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resolves every attribute of this level onto the containers.
|
|
44
|
+
# @return [void]
|
|
45
|
+
def process
|
|
46
|
+
@serializer.process(self)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Loads a named batch loader once for this level's objects.
|
|
50
|
+
# @param loader [SeregaEngine::Loader] Named batch loader
|
|
51
|
+
# @return [Object] Loaded values
|
|
52
|
+
def fetch(loader)
|
|
53
|
+
@results[loader] ||= loader.load(@objects, @serializer.context)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Batch feature main module
|
|
6
|
+
#
|
|
7
|
+
module SeregaEngine
|
|
8
|
+
#
|
|
9
|
+
# Level-order queue of serialization levels for one serialization run.
|
|
10
|
+
#
|
|
11
|
+
# `#serialize` on the object serializer enqueues a level (objects + their result
|
|
12
|
+
# containers) instead of resolving values inline. `#run` then processes the
|
|
13
|
+
# queue: each level resolves its attributes and, for relations, enqueues child
|
|
14
|
+
# levels — which the loop picks up because it re-reads the size each iteration.
|
|
15
|
+
# Processing level-by-level lets a named batch loader and preloads run once per
|
|
16
|
+
# level over all of its objects.
|
|
17
|
+
#
|
|
18
|
+
class LevelQueue
|
|
19
|
+
def initialize
|
|
20
|
+
@levels = []
|
|
21
|
+
@levels_by_plan = {}.compare_by_identity
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Adds objects to the level for their plan (creating it on first use), so
|
|
25
|
+
# objects serialized under the same plan — even from different parents —
|
|
26
|
+
# share one level and load once. The level creates a result container per
|
|
27
|
+
# object and returns them for the caller to embed in its parent.
|
|
28
|
+
#
|
|
29
|
+
# @param serializer [SeregaObjectSerializer] serializer that resolves the level
|
|
30
|
+
# @param objects [Array] objects serialized at this level
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<Hash>] the created result containers, aligned with objects
|
|
33
|
+
def enqueue(serializer, objects)
|
|
34
|
+
level = @levels_by_plan[serializer.plan]
|
|
35
|
+
unless level
|
|
36
|
+
level = Level.new(serializer)
|
|
37
|
+
@levels_by_plan[serializer.plan] = level
|
|
38
|
+
@levels << level
|
|
39
|
+
end
|
|
40
|
+
level.add(objects)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Processes every level, including child levels enqueued while processing.
|
|
44
|
+
# @return [void]
|
|
45
|
+
def run
|
|
46
|
+
i = 0
|
|
47
|
+
while i < @levels.size
|
|
48
|
+
@levels[i].process
|
|
49
|
+
i += 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|