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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a32fcd6506959b5498ef6357c27110bd7623d0bf16e28d0485703016704e6cf4
4
- data.tar.gz: 4e2bf60aba2c1db746587b1135a3d852e34858606ac30c6947c960d4e2dba726
3
+ metadata.gz: cf4c5f1a34256084ebedcba18d08dcdc30f9a3090ca3b555a7ee91fc8e0dfc44
4
+ data.tar.gz: 05022337ad49d098c50f25c267f33a2db6c210b169f3243b88c010a5f9bc519e
5
5
  SHA512:
6
- metadata.gz: 8cb7d76c325320e144dd7de3dcfc376a03386a7742f7eea68cae1bbb421b0cbe969e2c858fbae253114bbb3ed86e63d4c689395976ac94463813a8229ae66f9e
7
- data.tar.gz: e4595313b09bb2fcb7f37c6e26ebe9376a5caf86c66ed2ab4a19091aae854f1c51fafd0adea54cae0ac97eeb69e770e74f7e66d1aee44a8714d50ae225b8683a
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
  [![Gem Version](https://badge.fury.io/rb/serega.svg)](https://badge.fury.io/rb/serega)
4
- [![GitHub Actions](https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/aglushkov/serega/actions/workflows/main.yml)
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 ad `.to_h` and
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 allows you to define
420
- `:preloads` to attributes and then merge preloads from serialized attributes
421
- into a single associations hash.
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 `[:preload]` to hide attributes with preloads)
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
- Preloads can be disabled with the `preload: false` attribute option.
432
- Automatically added preloads can be overwritten with the manually specified
433
- `preload: :xxx` option.
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 = [:preload]
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
- # By default, preloads are empty, as we specify `hide_by_default = [:preload]`
465
- # so attributes with preloads will be skipped and nothing will be preloaded
466
- UserSerializer.new.preloads
467
- # => {}
468
-
469
- UserSerializer.new(with: :followers_count).preloads
470
- # => {:user_stats=>{}}
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 #1: Serializing the same object in association
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. You should specify `preload: nil` to preload
488
- `UserStatSerializer` nested associations to the "user" object.
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 = [:preload]
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
- ### SPECIFIC CASE #2: Serializing multiple associations as a single relation
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
- ```ruby
513
- class AppSerializer < Serega
514
- config.auto_preload = true
515
- end
523
+ ### Custom preloading
516
524
 
517
- class UserSerializer < AppSerializer
518
- attribute :username
519
- attribute :profiles,
520
- serializer: 'ProfileSerializer',
521
- value: proc { |user| [user.new_profile, user.old_profile] },
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
- class ProfileSerializer < AppSerializer
527
- attribute :avatar, serializer: 'AvatarSerializer'
528
- end
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
- class AvatarSerializer < AppSerializer
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
- ### SPECIFIC CASE #3: Preload association through another association
538
-
539
- ```ruby
540
- attribute :image,
541
- preload: { attachment: :blob }, # <--------- like this one
542
- value: proc { |record| record.attachment },
543
- serializer: ImageSerializer,
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
- It takes all defined preloads from serialized attributes (including attributes
566
- from serialized associations), merges them into a single associations hash, and then
567
- uses ActiveRecord::Associations::Preloader to preload associations to objects.
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
- # => preloads {users_stats: {}, albums: { downloads: {} }}
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.35.0
1
+ 0.37.0
@@ -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
- case hide_setting
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
- if batch_opt.nil?
212
- []
213
- elsif batch_opt == true || batch_opt.respond_to?(:call)
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
- # @return [Hash,nil]
242
- # - Nil means no need to add preloads and nested preloads
243
- # - Hash with any keys will add preloads
244
- # - Empty hash will skip only top-level preloads, but will allow to load nested preloads
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 = init_opts[:preload]
241
+ # Explicit preload option (false or nil disables preloading)
242
+ return init_opts[:preload] || nil if init_opts.key?(:preload)
247
243
 
248
- # Handle explicit preload option
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
- delegate_to = init_opts[:delegate][:to]
257
- return SeregaUtils::FormatUserPreloads.call(delegate_to)
246
+ return init_opts[:delegate][:to]
258
247
  end
259
248
 
260
- # Auto-preload for serializer
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 SeregaUtils::FormatUserPreloads.call(method_name)
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
- def initialize(point)
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
- @objects = []
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
- # Loads serialized values for all stored objects for current attribute
29
- # @param context [Hash] Current serialization context
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 load_all(context)
32
- batches = {}
33
- serializer_class = point.class.serializer_class
33
+ def preload_associations
34
+ preloads = point.attribute.preloads
35
+ return unless preloads
34
36
 
35
- point.batch_loaders.each do |batch_loader_name|
36
- batches[batch_loader_name] ||= load_one(serializer_class, batch_loader_name, context)
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
- private
66
+ attr_reader :point
67
+ attr_reader :level
45
68
 
46
- # Patched in:
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
- def initialize
17
- @point_batch_loaders = []
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 seryalization:
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
- point_batch_loader = point_index[point]
44
+ attribute_loader = point_index[point]
30
45
 
31
- unless point_batch_loader
46
+ unless attribute_loader
32
47
  serializer_class = point.class.serializer_class
33
- point_batch_loader = serializer_class::SeregaBatchAttributeLoader.new(point)
34
- point_batch_loaders << point_batch_loader
35
- point_index[point] = point_batch_loader
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
- point_batch_loader.store(object, attacher)
53
+ attribute_loader.store(object, attacher)
39
54
  end
40
55
 
41
56
  #
42
- # Loads all registered batches and removes them from registered list
57
+ # Loads all registered batches and attaches loaded values.
43
58
  #
44
- def load_all(context)
45
- point_batch_loaders.each do |point_batch_loader|
46
- point_batch_loader.load_all(context)
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
- # keeps all point_batch_loaders list
53
- attr_reader :point_batch_loaders
72
+ # Serialization context shared by every level
73
+ attr_reader :context
54
74
 
55
- # keeps tracking of already added serializers
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