serega 0.35.0 → 0.37.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 +74 -79
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +0 -5
- data/lib/serega/attribute_normalizer.rb +28 -70
- data/lib/serega/batch/attribute_loader.rb +35 -20
- data/lib/serega/batch/attribute_loaders.rb +65 -16
- data/lib/serega/batch/level.rb +48 -0
- data/lib/serega/config.rb +8 -10
- data/lib/serega/data_builder.rb +52 -0
- data/lib/serega/object_serializer.rb +7 -1
- data/lib/serega/plan.rb +33 -10
- data/lib/serega/plan_point.rb +0 -19
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +9 -54
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +1 -1
- data/lib/serega/plugins/if/if.rb +23 -0
- data/lib/serega/plugins/if/validations/check_opt_if.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +7 -4
- data/lib/serega/plugins/root/root.rb +80 -0
- data/lib/serega/validations/attribute/check_opt_preload.rb +11 -0
- data/lib/serega/validations/check_attribute_params.rb +0 -1
- data/lib/serega.rb +108 -28
- metadata +5 -8
- data/lib/serega/plugins/activerecord_preloads/lib/active_record_objects.rb +0 -31
- data/lib/serega/utils/format_user_preloads.rb +0 -58
- data/lib/serega/utils/preload_paths.rb +0 -53
- data/lib/serega/utils/preloads_constructor.rb +0 -77
- data/lib/serega/validations/attribute/check_opt_preload_path.rb +0 -89
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf4c5f1a34256084ebedcba18d08dcdc30f9a3090ca3b555a7ee91fc8e0dfc44
|
|
4
|
+
data.tar.gz: 05022337ad49d098c50f25c267f33a2db6c210b169f3243b88c010a5f9bc519e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9b5a2797086500903904617124810a4401f0a223ea75ba88d78c6cf3cba90157e9ff666f54fd76183fbbed4ab54d7be33704fd7c45665537e825f5f0f67222c
|
|
7
|
+
data.tar.gz: 150fcf62d92d01076c61d38a4189a31d789904fc643c2ce06c569280099d598582e3e07b7ff32a3482e0754d47045123dcf7f01baf7f7f65afebe2c1465e3c93
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Serega Ruby Serializer
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/serega)
|
|
4
|
-
[![GitHub Actions]
|
|
4
|
+
[![GitHub Actions][build-badge]][build]
|
|
5
5
|
|
|
6
6
|
The Serega Ruby Serializer provides easy and powerful DSL to describe your
|
|
7
7
|
objects and serialize them to Hash or JSON.
|
|
@@ -141,7 +141,7 @@ end
|
|
|
141
141
|
|
|
142
142
|
### Serializing
|
|
143
143
|
|
|
144
|
-
We can serialize objects using class method `.call` aliased
|
|
144
|
+
We can serialize objects using class method `.call` aliased as `.to_h` and
|
|
145
145
|
same instance methods `#call` and its alias `#to_h`.
|
|
146
146
|
|
|
147
147
|
```ruby
|
|
@@ -155,6 +155,18 @@ UserSerializer.to_h(user) # => {username: "serega"}
|
|
|
155
155
|
UserSerializer.to_h([user]) # => [{username: "serega"}]
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
Use `.to_data` / `#to_data` to get the same result as Ruby `Data` objects
|
|
159
|
+
(immutable value objects, Ruby 3.2+). Nested serialized relations are also
|
|
160
|
+
converted to `Data` objects.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
result = UserSerializer.to_data(user)
|
|
164
|
+
result # => #<data username="serega">
|
|
165
|
+
result.username # => "serega"
|
|
166
|
+
|
|
167
|
+
UserSerializer.to_data([user]) # => [#<data username="serega">]
|
|
168
|
+
```
|
|
169
|
+
|
|
158
170
|
If serialized fields are constant, then it's a good idea to initiate the
|
|
159
171
|
serializer and reuse it.
|
|
160
172
|
It will be a bit faster (the serialization plan will be prepared only once).
|
|
@@ -416,28 +428,38 @@ end
|
|
|
416
428
|
|
|
417
429
|
## Preloads
|
|
418
430
|
|
|
419
|
-
Serega includes built-in preloads functionality that
|
|
420
|
-
`:
|
|
421
|
-
|
|
431
|
+
Serega includes built-in preloads functionality that lets you declare
|
|
432
|
+
`:preload` on attributes. It is used by the `:activerecord_preloads` plugin:
|
|
433
|
+
enable it and every association you declare is loaded once during
|
|
434
|
+
serialization, automatically, with no N+1.
|
|
422
435
|
|
|
423
436
|
Configuration options:
|
|
424
437
|
|
|
425
438
|
- `config.auto_preload` - default `false` (use `true` or `{ has_delegate_option: true, has_serializer_option: true }`)
|
|
426
|
-
- `config.hide_by_default` - default `false` (use `
|
|
439
|
+
- `config.hide_by_default` - default `false` (use `:auto` to hide attributes with preloads or batch loaders)
|
|
427
440
|
|
|
428
441
|
These options are extremely useful if you want to forget about finding
|
|
429
442
|
preloads manually.
|
|
430
443
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
`preload: :
|
|
444
|
+
The `:preload` value is passed to the preload handler exactly as written — a
|
|
445
|
+
Symbol, Array, Hash, or any custom value your ORM understands. Preloads can be
|
|
446
|
+
disabled with `preload: false` (or `preload: nil`); `preload: true` is not a
|
|
447
|
+
valid value. Automatically added preloads can be overwritten with the manually
|
|
448
|
+
specified `preload: :xxx` option.
|
|
449
|
+
|
|
450
|
+
The actual loading is performed by a handler registered with `preload_with`. The
|
|
451
|
+
`:activerecord_preloads` plugin registers one for you; to preload with another
|
|
452
|
+
ORM — or to attach data to plain non-ORM objects by your own rules — register
|
|
453
|
+
your own (see [Custom preloading](#custom-preloading)). Declaring `:preload`
|
|
454
|
+
without a registered handler raises an error, so a preload never silently does
|
|
455
|
+
nothing.
|
|
434
456
|
|
|
435
457
|
For some examples, **please read the comments in the code below**
|
|
436
458
|
|
|
437
459
|
```ruby
|
|
438
460
|
class AppSerializer < Serega
|
|
439
461
|
config.auto_preload = true
|
|
440
|
-
config.hide_by_default =
|
|
462
|
+
config.hide_by_default = :auto
|
|
441
463
|
end
|
|
442
464
|
|
|
443
465
|
class UserSerializer < AppSerializer
|
|
@@ -461,36 +483,30 @@ class AlbumSerializer < AppSerializer
|
|
|
461
483
|
attribute :images_count, delegate: { to: :album_stats }
|
|
462
484
|
end
|
|
463
485
|
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
|
|
467
|
-
#
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
#
|
|
471
|
-
|
|
472
|
-
UserSerializer.new(with: %i[followers_count comments_count]).preloads
|
|
473
|
-
# => {:user_stats=>{}}
|
|
474
|
-
|
|
475
|
-
UserSerializer.new(
|
|
476
|
-
with: [:followers_count, :comments_count, { albums: :images_count }]
|
|
477
|
-
).preloads
|
|
478
|
-
# => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
|
|
486
|
+
# With `hide_by_default = :auto`, attributes that declare a preload are hidden
|
|
487
|
+
# unless explicitly requested with `:with`. When requested, their associations
|
|
488
|
+
# are preloaded automatically. For example:
|
|
489
|
+
#
|
|
490
|
+
# UserSerializer.to_h(users, with: [:followers_count, { albums: :images_count }])
|
|
491
|
+
#
|
|
492
|
+
# preloads `:user_stats` and `:albums` on the users, and `:album_stats` on the
|
|
493
|
+
# albums.
|
|
479
494
|
```
|
|
480
495
|
|
|
481
496
|
---
|
|
482
497
|
|
|
483
|
-
### SPECIFIC CASE
|
|
498
|
+
### SPECIFIC CASE: Serializing the same object in association
|
|
484
499
|
|
|
485
500
|
For example, you show your current user as "user" and use the same user object
|
|
486
501
|
to serialize "user_stats". `UserStatSerializer` relies on user fields and any
|
|
487
|
-
other user associations.
|
|
488
|
-
|
|
502
|
+
other user associations. Specify `preload: nil` so that nothing is preloaded for
|
|
503
|
+
this attribute on the "user" object — the associations declared in
|
|
504
|
+
`UserStatSerializer` are still preloaded onto the same user object.
|
|
489
505
|
|
|
490
506
|
```ruby
|
|
491
507
|
class AppSerializer < Serega
|
|
492
508
|
config.auto_preload = true
|
|
493
|
-
config.hide_by_default =
|
|
509
|
+
config.hide_by_default = :auto
|
|
494
510
|
end
|
|
495
511
|
|
|
496
512
|
class UserSerializer < AppSerializer
|
|
@@ -502,69 +518,47 @@ class UserSerializer < AppSerializer
|
|
|
502
518
|
end
|
|
503
519
|
```
|
|
504
520
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
For example, "user" has two relations - "new_profile" and "old_profile". Also
|
|
508
|
-
profiles have the "avatar" association. And you decided to serialize profiles in
|
|
509
|
-
one array. You can specify `preload_path: [[:new_profile], [:old_profile]]` to
|
|
510
|
-
achieve this:
|
|
521
|
+
---
|
|
511
522
|
|
|
512
|
-
|
|
513
|
-
class AppSerializer < Serega
|
|
514
|
-
config.auto_preload = true
|
|
515
|
-
end
|
|
523
|
+
### Custom preloading
|
|
516
524
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
preload: [:new_profile, :old_profile],
|
|
523
|
-
preload_path: [[:new_profile], [:old_profile]] # <--- like here
|
|
524
|
-
end
|
|
525
|
+
The `:activerecord_preloads` plugin works out of the box, but the preload
|
|
526
|
+
mechanism itself is not tied to ActiveRecord — or to ORMs at all. Register a
|
|
527
|
+
handler with `preload_with` and it is called once per preloaded attribute with
|
|
528
|
+
the gathered objects and that attribute's `:preload` value, before the
|
|
529
|
+
attribute is serialized:
|
|
525
530
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
531
|
+
```ruby
|
|
532
|
+
class UserSerializer < Serega
|
|
533
|
+
# `objects` is the gathered users; `preloads` is this attribute's
|
|
534
|
+
# `:preload` value (`:posts`). `MyORM.preload` does the eager loading.
|
|
535
|
+
preload_with { |objects, preloads| MyORM.preload(objects, preloads) }
|
|
529
536
|
|
|
530
|
-
|
|
537
|
+
attribute :name
|
|
538
|
+
attribute :posts, serializer: PostSerializer, preload: :posts
|
|
531
539
|
end
|
|
532
|
-
|
|
533
|
-
UserSerializer.new.preloads
|
|
534
|
-
# => {:new_profile=>{:avatar=>{}}, :old_profile=>{:avatar=>{}}}
|
|
535
540
|
```
|
|
536
541
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
preload_path: [:attachment] # or preload_path: [:attachment, :blob]
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
In this case, we don't know if preloads defined in ImageSerializer, should be
|
|
548
|
-
preloaded to `attachment` or `blob`, so `preload_path` must be specified manually.
|
|
549
|
-
You can specify `preload_path: nil` if you are sure that there are no preloads
|
|
550
|
-
inside ImageSerializer.
|
|
542
|
+
The handler can be a block or any callable taking `(objects, preloads)`. It is
|
|
543
|
+
inherited by child serializers and can be overridden per subclass. The
|
|
544
|
+
`:preload` value reaches the handler untouched, so you can pass whatever your
|
|
545
|
+
handler expects — not necessarily something an ORM understands. The gathered
|
|
546
|
+
objects can be plain Ruby objects too: the handler decides where the data comes
|
|
547
|
+
from (another ORM, an HTTP API, a cache) and how to attach it to the objects,
|
|
548
|
+
by whatever rules you define.
|
|
551
549
|
|
|
552
550
|
---
|
|
553
551
|
|
|
554
|
-
📌 The built-in preloads functionality only allows to group preloads together
|
|
555
|
-
in single Hash, but they should be preloaded manually.
|
|
556
|
-
|
|
557
|
-
There is the [activerecord_preloads][activerecord_preloads] plugin that can be used to preload these associations automatically.
|
|
558
|
-
|
|
559
552
|
## Plugins
|
|
560
553
|
|
|
561
554
|
### Plugin :activerecord_preloads
|
|
562
555
|
|
|
563
556
|
Automatically preloads associations to serialized objects.
|
|
564
557
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
558
|
+
Every association you declare with `:preload` is loaded once during
|
|
559
|
+
serialization using `ActiveRecord::Associations::Preloader`, so there are no
|
|
560
|
+
N+1 queries. The plugin does this by registering a [`preload_with`](#custom-preloading)
|
|
561
|
+
handler; to use a different ORM, register your own instead.
|
|
568
562
|
|
|
569
563
|
```ruby
|
|
570
564
|
class AppSerializer < Serega
|
|
@@ -587,11 +581,10 @@ class AlbumSerializer < AppSerializer
|
|
|
587
581
|
end
|
|
588
582
|
|
|
589
583
|
UserSerializer.to_h(user)
|
|
590
|
-
#
|
|
584
|
+
# preloads :user_stats and :albums on the user; the AlbumSerializer level
|
|
585
|
+
# preloads :downloads on the albums
|
|
591
586
|
```
|
|
592
587
|
|
|
593
|
-
For testing purposes preloading can be done manually with `#preload_associations_to(obj)` instance method
|
|
594
|
-
|
|
595
588
|
### Plugin :root
|
|
596
589
|
|
|
597
590
|
Allows to add root key to your serialized data
|
|
@@ -1016,3 +1009,5 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
1016
1009
|
[root]: #plugin-root
|
|
1017
1010
|
[string_modifiers]: #plugin-string_modifiers
|
|
1018
1011
|
[if]: #plugin-if
|
|
1012
|
+
[build-badge]: https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push
|
|
1013
|
+
[build]: https://github.com/aglushkov/serega/actions/workflows/main.yml
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.37.0
|
data/lib/serega/attribute.rb
CHANGED
|
@@ -33,10 +33,6 @@ class Serega
|
|
|
33
33
|
# @return [Hash, nil] Attribute :preloads option
|
|
34
34
|
attr_reader :preloads
|
|
35
35
|
|
|
36
|
-
# Attribute :preloads_path option
|
|
37
|
-
# @return [Array, nil] Attribute :preloads_path option
|
|
38
|
-
attr_reader :preloads_path
|
|
39
|
-
|
|
40
36
|
# Batch loader names required to detect attribute value
|
|
41
37
|
# @return [Array<Symbol>] Batch loader names
|
|
42
38
|
attr_reader :batch_loaders
|
|
@@ -143,7 +139,6 @@ class Serega
|
|
|
143
139
|
@hide = normalizer.hide
|
|
144
140
|
@serializer = normalizer.serializer
|
|
145
141
|
@preloads = normalizer.preloads
|
|
146
|
-
@preloads_path = normalizer.preloads_path
|
|
147
142
|
@batch_loaders = normalizer.batch_loaders
|
|
148
143
|
end
|
|
149
144
|
end
|
|
@@ -9,6 +9,10 @@ class Serega
|
|
|
9
9
|
# AttributeNormalizer instance methods
|
|
10
10
|
#
|
|
11
11
|
module AttributeNormalizerInstanceMethods
|
|
12
|
+
# Identity loader that routes relation/preload attributes through the batch
|
|
13
|
+
# mechanism. Its result is never read — the attribute's value block still
|
|
14
|
+
# computes the value.
|
|
15
|
+
AUTO_BATCH_LOADER = proc { |records| records.to_h { |record| [record, record] } }
|
|
12
16
|
# Attribute initial params
|
|
13
17
|
# @return [Hash] Attribute initial params
|
|
14
18
|
attr_reader :init_name, :init_opts, :init_block
|
|
@@ -119,17 +123,6 @@ class Serega
|
|
|
119
123
|
@preloads = prepare_preloads
|
|
120
124
|
end
|
|
121
125
|
|
|
122
|
-
#
|
|
123
|
-
# Shows normalized preloads_path for current attribute
|
|
124
|
-
#
|
|
125
|
-
# @return [Array] normalized preloads_path of current attribute
|
|
126
|
-
#
|
|
127
|
-
def preloads_path
|
|
128
|
-
return @preloads_path if instance_variable_defined?(:@preloads_path)
|
|
129
|
-
|
|
130
|
-
@preloads_path = prepare_preloads_path
|
|
131
|
-
end
|
|
132
|
-
|
|
133
126
|
# Shows specified batch loaders names
|
|
134
127
|
# @return [Array<Symbol>] specified serializer
|
|
135
128
|
#
|
|
@@ -158,14 +151,8 @@ class Serega
|
|
|
158
151
|
return hide if (hide == true) || (hide == false)
|
|
159
152
|
|
|
160
153
|
hide_setting = config.hide_by_default
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
when true
|
|
164
|
-
return true
|
|
165
|
-
when Array
|
|
166
|
-
return true if hide_setting.include?(:preload) && preloads # hide when attribute has `:preload` option
|
|
167
|
-
return true if hide_setting.include?(:batch) && batch_loaders.any? # hide when attribute has `:batch` option
|
|
168
|
-
end
|
|
154
|
+
return true if hide_setting == true
|
|
155
|
+
return true if hide_setting == :auto && (preloads || init_opts.key?(:batch))
|
|
169
156
|
|
|
170
157
|
# Return nil for undefined value which means "not hide" but allows
|
|
171
158
|
# to change this value by plugins
|
|
@@ -207,10 +194,16 @@ class Serega
|
|
|
207
194
|
|
|
208
195
|
def prepare_batch_loaders
|
|
209
196
|
batch_opt = init_opts[:batch]
|
|
197
|
+
return explicit_batch_loaders(batch_opt) if batch_opt
|
|
198
|
+
return FROZEN_EMPTY_ARRAY unless serializer || preloads
|
|
210
199
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
200
|
+
loader_name = :"__auto_batch_#{name}__"
|
|
201
|
+
self.class.serializer_class.batch(loader_name, AUTO_BATCH_LOADER)
|
|
202
|
+
[loader_name]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def explicit_batch_loaders(batch_opt)
|
|
206
|
+
if batch_opt == true || batch_opt.respond_to?(:call)
|
|
214
207
|
[name]
|
|
215
208
|
elsif batch_opt.is_a?(Symbol)
|
|
216
209
|
[batch_opt]
|
|
@@ -237,64 +230,29 @@ class Serega
|
|
|
237
230
|
AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, allow_nil)
|
|
238
231
|
end
|
|
239
232
|
|
|
240
|
-
# Prepares preloads
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
233
|
+
# Prepares preloads for this attribute.
|
|
234
|
+
#
|
|
235
|
+
# The value is passed through as provided (Symbol, Array, Hash, or any
|
|
236
|
+
# custom value an ORM understands) — it is the data handed to the
|
|
237
|
+
# serializer's `preload_with` handler.
|
|
238
|
+
#
|
|
239
|
+
# @return [Object, nil] preloads as provided, or nil when the attribute has none
|
|
245
240
|
def prepare_preloads
|
|
246
|
-
preload
|
|
241
|
+
# Explicit preload option (false or nil disables preloading)
|
|
242
|
+
return init_opts[:preload] || nil if init_opts.key?(:preload)
|
|
247
243
|
|
|
248
|
-
#
|
|
249
|
-
if init_opts.key?(:preload)
|
|
250
|
-
return nil unless preload # return nil when false or nil
|
|
251
|
-
return SeregaUtils::FormatUserPreloads.call(preload)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
# Auto-preload for delegate
|
|
244
|
+
# Auto-preload the delegated association
|
|
255
245
|
if config.auto_preload.fetch(:has_delegate_option) && init_opts[:delegate]
|
|
256
|
-
|
|
257
|
-
return SeregaUtils::FormatUserPreloads.call(delegate_to)
|
|
246
|
+
return init_opts[:delegate][:to]
|
|
258
247
|
end
|
|
259
248
|
|
|
260
|
-
# Auto-preload
|
|
249
|
+
# Auto-preload the nested serializer's association
|
|
261
250
|
if config.auto_preload.fetch(:has_serializer_option) && init_opts[:serializer] && !init_opts.key?(:batch)
|
|
262
|
-
return
|
|
251
|
+
return method_name
|
|
263
252
|
end
|
|
264
253
|
|
|
265
254
|
nil
|
|
266
255
|
end
|
|
267
|
-
|
|
268
|
-
def prepare_preloads_path
|
|
269
|
-
path = init_opts.fetch(:preload_path) { default_preload_path(preloads) }
|
|
270
|
-
|
|
271
|
-
if path && path[0].is_a?(Array)
|
|
272
|
-
prepare_many_preload_paths(path)
|
|
273
|
-
else
|
|
274
|
-
prepare_one_preload_path(path)
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def prepare_one_preload_path(path)
|
|
279
|
-
return unless path
|
|
280
|
-
|
|
281
|
-
case path
|
|
282
|
-
when Array
|
|
283
|
-
path.map(&:to_sym).freeze
|
|
284
|
-
else
|
|
285
|
-
[path.to_sym].freeze
|
|
286
|
-
end
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def prepare_many_preload_paths(paths)
|
|
290
|
-
paths.map { |path| prepare_one_preload_path(path) }.freeze
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def default_preload_path(preloads)
|
|
294
|
-
return FROZEN_EMPTY_ARRAY if !preloads || preloads.empty?
|
|
295
|
-
|
|
296
|
-
[preloads.keys.first]
|
|
297
|
-
end
|
|
298
256
|
end
|
|
299
257
|
|
|
300
258
|
extend Serega::SeregaHelpers::SerializerClassHelper
|
|
@@ -10,9 +10,10 @@ class Serega
|
|
|
10
10
|
# Instance methods for AttributeLoader
|
|
11
11
|
module InstanceMethods
|
|
12
12
|
# @param point [SeregaPlanPoint]
|
|
13
|
-
|
|
13
|
+
# @param level [SeregaBatch::Level] Shared objects and results for this point's plan level
|
|
14
|
+
def initialize(point, level)
|
|
14
15
|
@point = point
|
|
15
|
-
@
|
|
16
|
+
@level = level
|
|
16
17
|
@serialized_object_attachers = []
|
|
17
18
|
end
|
|
18
19
|
|
|
@@ -21,35 +22,51 @@ class Serega
|
|
|
21
22
|
# @param attacher [Proc] Proc that attaches found values to originated attributes
|
|
22
23
|
# @return [void]
|
|
23
24
|
def store(object, attacher)
|
|
24
|
-
objects << object
|
|
25
25
|
serialized_object_attachers << [object, attacher]
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
28
|
+
# Preloads this attribute's associations onto the level's objects, once for
|
|
29
|
+
# the attribute (not per named batch loader), via the serializer's registered
|
|
30
|
+
# `preload_with` handler. Raises when `:preload` is declared but no handler
|
|
31
|
+
# exists, so a declared preload never silently does nothing.
|
|
30
32
|
# @return [void]
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
def preload_associations
|
|
34
|
+
preloads = point.attribute.preloads
|
|
35
|
+
return unless preloads
|
|
34
36
|
|
|
35
|
-
point.
|
|
36
|
-
|
|
37
|
+
handler = point.class.serializer_class.preload_with
|
|
38
|
+
unless handler
|
|
39
|
+
raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
|
|
37
40
|
end
|
|
38
41
|
|
|
42
|
+
handler.call(level.objects, preloads)
|
|
43
|
+
rescue => error
|
|
44
|
+
reraise_with_serialized_attribute_details(error)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Loads a single named batch for this level's objects. Caching across
|
|
48
|
+
# attributes that reuse the loader is handled by the Level.
|
|
49
|
+
# @param loader [SeregaBatch::Loader] Named batch loader
|
|
50
|
+
# @return [Object] Loaded batch values
|
|
51
|
+
def load_batch(loader)
|
|
52
|
+
level.load(loader)
|
|
53
|
+
rescue => error
|
|
54
|
+
reraise_with_serialized_attribute_details(error)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Attaches already loaded batch values to every stored object.
|
|
58
|
+
# @param batches [Hash] Loaded values grouped by batch loader name
|
|
59
|
+
# @return [void]
|
|
60
|
+
def attach(batches)
|
|
39
61
|
serialized_object_attachers.each do |object, attacher|
|
|
40
62
|
attacher.call(object, batches)
|
|
41
63
|
end
|
|
42
64
|
end
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
attr_reader :point
|
|
67
|
+
attr_reader :level
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
# - plugin :activerecord_preloads (preloads associations to found AR objects)
|
|
48
|
-
def load_one(serializer_class, batch_loader_name, context)
|
|
49
|
-
serializer_class.batch_loaders[batch_loader_name].load(objects, context)
|
|
50
|
-
rescue => error
|
|
51
|
-
reraise_with_serialized_attribute_details(error)
|
|
52
|
-
end
|
|
69
|
+
private
|
|
53
70
|
|
|
54
71
|
def reraise_with_serialized_attribute_details(error)
|
|
55
72
|
raise error.exception(<<~MESSAGE.strip)
|
|
@@ -58,8 +75,6 @@ class Serega
|
|
|
58
75
|
MESSAGE
|
|
59
76
|
end
|
|
60
77
|
|
|
61
|
-
attr_reader :point
|
|
62
|
-
attr_reader :objects
|
|
63
78
|
attr_reader :serialized_object_attachers
|
|
64
79
|
end
|
|
65
80
|
|
|
@@ -13,12 +13,27 @@ class Serega
|
|
|
13
13
|
#
|
|
14
14
|
# Initializes new batch loaders
|
|
15
15
|
#
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# @param context [Hash] Serialization context, constant for the whole run
|
|
17
|
+
#
|
|
18
|
+
def initialize(context)
|
|
19
|
+
@context = context
|
|
20
|
+
@attribute_loaders = []
|
|
18
21
|
@point_index = {}.compare_by_identity
|
|
22
|
+
@levels = {}.compare_by_identity
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Gathers a chunk of objects serialized at a plan level, so every attribute at
|
|
26
|
+
# that level shares one set of objects to batch load against.
|
|
27
|
+
#
|
|
28
|
+
# @param plan [SeregaPlan] Plan serializing these objects
|
|
29
|
+
# @param objects [Array] Objects being serialized at this level
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
32
|
+
def add_objects(plan, objects)
|
|
33
|
+
level_for(plan).add_objects(objects)
|
|
19
34
|
end
|
|
20
35
|
|
|
21
|
-
# Remembers data for batch
|
|
36
|
+
# Remembers data for batch serialization:
|
|
22
37
|
#
|
|
23
38
|
# @param point [SeregaPlanPoint] Serialization plan point
|
|
24
39
|
# @param object [Object] Serialized object
|
|
@@ -26,34 +41,68 @@ class Serega
|
|
|
26
41
|
#
|
|
27
42
|
# @return [void]
|
|
28
43
|
def remember(point, object, attacher)
|
|
29
|
-
|
|
44
|
+
attribute_loader = point_index[point]
|
|
30
45
|
|
|
31
|
-
unless
|
|
46
|
+
unless attribute_loader
|
|
32
47
|
serializer_class = point.class.serializer_class
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
point_index[point] =
|
|
48
|
+
attribute_loader = serializer_class::SeregaBatchAttributeLoader.new(point, level_for(point.plan))
|
|
49
|
+
attribute_loaders << attribute_loader
|
|
50
|
+
point_index[point] = attribute_loader
|
|
36
51
|
end
|
|
37
52
|
|
|
38
|
-
|
|
53
|
+
attribute_loader.store(object, attacher)
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
#
|
|
42
|
-
# Loads all registered batches and
|
|
57
|
+
# Loads all registered batches and attaches loaded values.
|
|
43
58
|
#
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
# Iterates over each loader including loaders added during iteration
|
|
60
|
+
# (child serializers discovered inside a batch flush add new loaders).
|
|
61
|
+
def load_all
|
|
62
|
+
i = 0
|
|
63
|
+
while i < attribute_loaders.size
|
|
64
|
+
attribute_loader = attribute_loaders[i]
|
|
65
|
+
attribute_loader.attach(load_batches(attribute_loader))
|
|
66
|
+
i += 1
|
|
47
67
|
end
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
private
|
|
51
71
|
|
|
52
|
-
#
|
|
53
|
-
attr_reader :
|
|
72
|
+
# Serialization context shared by every level
|
|
73
|
+
attr_reader :context
|
|
54
74
|
|
|
55
|
-
# keeps
|
|
75
|
+
# keeps all AttributeLoader instances
|
|
76
|
+
attr_reader :attribute_loaders
|
|
77
|
+
|
|
78
|
+
# keeps tracking of already added points
|
|
56
79
|
attr_reader :point_index
|
|
80
|
+
|
|
81
|
+
# keeps one Level per plan (compare_by_identity: plan instance identifies a level)
|
|
82
|
+
attr_reader :levels
|
|
83
|
+
|
|
84
|
+
def level_for(plan)
|
|
85
|
+
levels[plan] ||= Level.new(context)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Builds the { batch_loader_name => loaded_values } hash for one attribute.
|
|
89
|
+
#
|
|
90
|
+
# Loading is delegated to the attribute's Level, which loads each named batch
|
|
91
|
+
# once for the whole level: attributes sharing a loader over the same objects
|
|
92
|
+
# reuse a single result, deeper levels load separately.
|
|
93
|
+
def load_batches(attribute_loader)
|
|
94
|
+
attribute_loader.preload_associations
|
|
95
|
+
|
|
96
|
+
point = attribute_loader.point
|
|
97
|
+
serializer_class = point.class.serializer_class
|
|
98
|
+
|
|
99
|
+
batches = {}
|
|
100
|
+
point.batch_loaders.each do |batch_loader_name|
|
|
101
|
+
loader = serializer_class.batch_loaders[batch_loader_name]
|
|
102
|
+
batches[batch_loader_name] = attribute_loader.load_batch(loader)
|
|
103
|
+
end
|
|
104
|
+
batches
|
|
105
|
+
end
|
|
57
106
|
end
|
|
58
107
|
end
|
|
59
108
|
end
|