serega 0.37.2 → 0.38.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: fe536c6d684f24508a5153d1fc679cd451d2360ab3d752ff2ba08a2249b264c9
4
- data.tar.gz: 0110de5fca4df251b1101daaba232bf1fcace80b6290bb95e35a8c892fc1ce1b
3
+ metadata.gz: 384da177845e2385a0100d23147cea0034860b148cd46738dc3d6ec7ea7d7eda
4
+ data.tar.gz: f08d482a83cb0cde30ae4c03e924f50be6aaabea24d76564bc47a7a1eadf4f98
5
5
  SHA512:
6
- metadata.gz: 5a7f60a35e6791337f15772efeb2dfa0363ff96bb99f815c849c1fb5b40125854802b39ef492bea078f6bb614171b29b8f0e5409e421c52e6f24bf39eabfffdd
7
- data.tar.gz: 2d3adb93d754ed3fb42581e5a45493e30e3f1fbb1cda8c68956e5cc047be42417f8cd487074cae5ce11fe72dbfa633ba32ccdc52dd002de17f349b7532c10f4f
6
+ metadata.gz: c9f2f95d8a74ce4951f7acef89a1cf603fcba2f15af50d246f85d58e1456b06c03b749fef620c741f8e2482999c6f7471fb5dff9a904b275f79f039999ab7fdc
7
+ data.tar.gz: 69c1b8f898f81317310e875a8771db096b3d4613b34769024d66fe801554e53094fd59e3cfea923ef81bb5d837a31ef28ceadc825003c95a4a2340f6b0b0bf74
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.37.2
1
+ 0.38.0
@@ -109,9 +109,9 @@ class Serega
109
109
  end
110
110
 
111
111
  #
112
- # Shows normalized preloads for current attribute
112
+ # Preloads declared for this attribute, passed through as provided.
113
113
  #
114
- # @return [Hash, nil] normalized preloads of current attribute
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)
@@ -199,16 +199,14 @@ class Serega
199
199
  AttributeValueResolvers::BatchResolver.get(self.class.serializer_class, name, batch_opt)
200
200
  end
201
201
 
202
+ # Batch loader names whose loaded data this attribute's value needs. Only
203
+ # explicit `:batch` attributes have any — everything else resolves its value
204
+ # from the record itself, so the list is empty (no loader to run).
202
205
  def prepare_batch_loaders
203
206
  batch_opt = init_opts[:batch]
204
207
  return explicit_batch_loaders(batch_opt) if batch_opt
205
- return FROZEN_EMPTY_ARRAY unless serializer || preloads
206
208
 
207
- # Relations and preloads only need to be routed through the batch phase
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]
209
+ FROZEN_EMPTY_ARRAY
212
210
  end
213
211
 
214
212
  def explicit_batch_loaders(batch_opt)
@@ -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
@@ -4,7 +4,7 @@ class Serega
4
4
  #
5
5
  # Batch feature main module
6
6
  #
7
- module SeregaBatch
7
+ module SeregaEngine
8
8
  #
9
9
  # Batch loader
10
10
  #
@@ -2,15 +2,19 @@
2
2
 
3
3
  class Serega
4
4
  #
5
- # Low-level class that is used by more high-level SeregaSerializer
6
- # to construct serialized to hash response
5
+ # Low-level class used by the serializer to construct the serialized response.
6
+ #
7
+ # Serialization is level-by-level. `#serialize` builds the result container(s)
8
+ # and enqueues this level; the batch queue later calls `#process` for each level,
9
+ # which resolves every attribute for every object and enqueues child levels for
10
+ # relations.
7
11
  #
8
12
  class SeregaObjectSerializer
9
13
  #
10
14
  # SeregaObjectSerializer instance methods
11
15
  #
12
16
  module InstanceMethods
13
- attr_reader :context, :plan, :many, :opts, :batch_loaders
17
+ attr_reader :context, :plan, :many, :opts, :level_queue
14
18
 
15
19
  # @param plan [SeregaPlan] Serialization plan
16
20
  # @param context [Hash] Serialization context
@@ -23,85 +27,86 @@ class Serega
23
27
  @plan = plan
24
28
  @many = many
25
29
  @opts = opts
26
- @batch_loaders = opts[:batch_loaders]
30
+ @level_queue = opts[:level_queue]
27
31
  end
28
32
 
29
- # Serializes object(s)
33
+ # Enqueues this level and returns its result container(s). The containers are
34
+ # returned immediately but filled in place once the queue is processed.
30
35
  #
31
- # @param object [Object] Serialized object
36
+ # @param object [Object] Serialized object(s)
32
37
  #
33
38
  # @return [Hash, Array<Hash>, nil] Serialized object(s)
34
39
  def serialize(object)
35
40
  return if object.nil?
36
41
 
37
- if array?(object, many)
38
- batch_loaders.add_objects(plan, object.to_a) if plan.batch?
39
- serialize_array(object)
40
- else
41
- batch_loaders.add_objects(plan, [object]) if plan.batch?
42
- serialize_object(object)
42
+ case serialize_mode(object)
43
+ when :many then enqueue(object.to_a)
44
+ when :many_for_one then enqueue([object]) # `many` on, but a sole object was given — wrap it, don't raise
45
+ else enqueue([object])[0] # :one
43
46
  end
44
47
  end
45
48
 
46
- private
49
+ # Resolves every attribute of one level onto its containers. For each point,
50
+ # preloads and batch loaders run once over all of the level's objects; the
51
+ # value is then resolved and assigned per object, in attribute order.
52
+ #
53
+ # @param level [SeregaEngine::Level] level to resolve
54
+ # @return [void]
55
+ def process(level)
56
+ objects = level.objects
47
57
 
48
- def serialize_array(objects)
49
- objects.map { |object| serialize_object(object) }
50
- end
58
+ plan.points.each do |point|
59
+ point.run_preloads(objects)
60
+ batches = point.load_batches(level)
61
+ # One child serializer per relation point per level (reused for every object),
62
+ # instead of one per object — child objects are grouped into one child level anyway.
63
+ child_serializer = point.child_serializer(context: context, **opts)
51
64
 
52
- # Patched in:
53
- # - plugin :presenter (makes presenter_object and serializes it)
54
- def serialize_object(object)
55
- plan.points.each_with_object({}) do |point, container|
56
- serialize_point(object, point, container)
57
- rescue => error
58
- SeregaUtils::SerializedAttributeError.call(error, point)
65
+ objects.each_with_index do |object, index|
66
+ resolve_point(object, point, level.containers[index], batches, child_serializer)
67
+ rescue => error
68
+ SeregaUtils::SerializedAttributeError.call(error, point)
69
+ end
59
70
  end
60
71
  end
61
72
 
73
+ private
74
+
75
+ # Enqueues this chunk of objects onto the queue and returns their result
76
+ # containers. This is where objects enter their level, so every object a
77
+ # point resolves against and a batch loader receives has the same shape.
78
+ #
62
79
  # Patched in:
63
- # - plugin :if (conditionally skips serializing this point)
64
- def serialize_point(object, point, container)
65
- if point.batch?
66
- attacher = lambda { |obj, batches| attach_value(obj, point, container, batches: batches) }
67
- batch_loaders.remember(point, object, attacher)
68
- container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
69
- else
70
- attach_value(object, point, container, batches: nil)
71
- end
80
+ # - plugin :presenter (wraps each object in a Presenter before enqueueing)
81
+ def enqueue(objects)
82
+ level_queue.enqueue(self, objects)
72
83
  end
73
84
 
74
- def attach_value(object, point, container, batches: nil)
85
+ # Patched in:
86
+ # - plugin :if (skips the point for objects failing an :if/:unless condition)
87
+ def resolve_point(object, point, container, batches, child_serializer)
75
88
  value = point.value(object, context, batches: batches)
76
- final_value = final_value(value, point)
77
- attach_final_value(final_value, point, container)
89
+ final_value = child_serializer ? child_serializer.serialize(value) : value
90
+ write_value(final_value, point, container)
78
91
  end
79
92
 
80
93
  # Patched in:
81
- # - plugin :if (conditionally skips attaching)
82
- def attach_final_value(final_value, point, container)
94
+ # - plugin :if (skips assigning when an :if_value/:unless_value condition fails)
95
+ def write_value(final_value, point, container)
83
96
  container[point.name] = final_value
84
97
  end
85
98
 
86
- def final_value(value, point)
87
- point.child_plan ? relation_value(value, point) : value
88
- end
89
-
90
- def relation_value(value, point)
91
- child_serializer(point).serialize(value)
92
- end
93
-
94
- def child_serializer(point)
95
- point.child_object_serializer.new(
96
- context: context,
97
- plan: point.child_plan,
98
- many: point.many,
99
- **opts
100
- )
101
- end
102
-
103
- def array?(object, many)
104
- many.nil? ? object.is_a?(Enumerable) : many
99
+ # How to serialize `object`, deciding whether the result is a collection or a
100
+ # single object and reading `Enumerable` only once:
101
+ # - :many — `many` is on and the object is a collection
102
+ # - :many_for_one — `many` is on but a sole object was given (wrap it, don't raise)
103
+ # - :one — serialize the object on its own
104
+ def serialize_mode(object)
105
+ case many
106
+ when NilClass then object.is_a?(Enumerable) ? :many : :one
107
+ when TrueClass then object.is_a?(Enumerable) ? :many : :many_for_one
108
+ else :one # many == false
109
+ end
105
110
  end
106
111
  end
107
112
 
data/lib/serega/plan.rb CHANGED
@@ -108,15 +108,6 @@ class Serega
108
108
  self.class.serializer_class
109
109
  end
110
110
 
111
- #
112
- # @return [Boolean] whether any point of this plan is batch loaded
113
- #
114
- def batch?
115
- return @batch if defined?(@batch)
116
-
117
- @batch = points.any?(&:batch?)
118
- end
119
-
120
111
  # Returns the Data class whose members match this plan's serialized fields.
121
112
  # Delegates to the class-level cache so identical field sets share one Data class.
122
113
  #
@@ -68,23 +68,74 @@ class Serega
68
68
  attribute.serializer
69
69
  end
70
70
 
71
- def batch?
72
- !attribute.batch_loaders.empty?
73
- end
74
-
75
71
  # Attribute `batch_loaders`
76
72
  # @see SeregaAttribute::AttributeInstanceMethods#batch_loaders
77
73
  def batch_loaders
78
74
  attribute.batch_loaders
79
75
  end
80
76
 
77
+ # Attribute `preloads`
78
+ # @see SeregaAttribute::AttributeInstanceMethods#preloads
79
+ def preloads
80
+ attribute.preloads
81
+ end
82
+
83
+ # Runs this point's declared preloads over the given objects using the
84
+ # serializer's registered preload handler.
85
+ #
86
+ # @param objects [Array] objects serialized at this point's level
87
+ # @return [void]
88
+ def run_preloads(objects)
89
+ return unless preloads
90
+
91
+ handler = self.class.serializer_class.preload_with
92
+ unless handler
93
+ raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
94
+ end
95
+
96
+ handler.call(objects, preloads)
97
+ rescue => error
98
+ SeregaUtils::SerializedAttributeError.call(error, self)
99
+ end
100
+
101
+ # Loads the batch loaders this point's value needs, each once for the whole
102
+ # level, and returns them keyed by loader name for #value to read from.
81
103
  #
82
- # @return [SeregaObjectSerializer] object serializer for child plan
104
+ # @param level [SeregaEngine::Level] level whose objects are loaded for
105
+ # @return [Hash, nil] loaded data per loader name, or nil when none are needed
106
+ def load_batches(level)
107
+ names = batch_loaders
108
+ return if names.empty?
109
+
110
+ loaders = self.class.serializer_class.batch_loaders
111
+ names.each_with_object({}) do |name, batches|
112
+ batches[name] = level.fetch(loaders[name])
113
+ end
114
+ rescue => error
115
+ SeregaUtils::SerializedAttributeError.call(error, self)
116
+ end
117
+
118
+ #
119
+ # @return [Class<SeregaObjectSerializer>] object serializer class for child plan
83
120
  #
84
121
  def child_object_serializer
85
122
  serializer::SeregaObjectSerializer
86
123
  end
87
124
 
125
+ # Builds the object serializer that serializes this point's relation, or nil
126
+ # when the point has no child plan (a plain attribute). The point owns the
127
+ # static config (child plan, serializer class, `many`); the caller injects the
128
+ # runtime `context` and `opts`.
129
+ #
130
+ # @param context [Hash] serialization context
131
+ # @param opts [Hash] extra object-serializer options (e.g. the level queue)
132
+ # @return [SeregaObjectSerializer, nil] serializer for the child level
133
+ def child_serializer(context:, **opts)
134
+ return unless child_plan
135
+
136
+ child_object_serializer.new(context: context, plan: child_plan, many: many, **opts)
137
+ end
138
+
88
139
  private
89
140
 
90
141
  def set_normalized_vars
@@ -82,7 +82,25 @@ class Serega
82
82
  # @return [void]
83
83
  #
84
84
  def self.after_load_plugin(serializer_class, **_opts)
85
- serializer_class.preload_with { |objects, preloads| Preloader.preload(objects, preloads) }
85
+ serializer_class.preload_with do |objects, preloads|
86
+ Preloader.preload(ActiverecordPreloads.records(serializer_class, objects), preloads)
87
+ end
88
+ end
89
+
90
+ #
91
+ # The underlying records to preload onto. The :presenter plugin wraps every
92
+ # serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
93
+ # the real records, so unwrap them via #__getobj__ when presenter is used.
94
+ #
95
+ # @param serializer_class [Class<Serega>] Current serializer class
96
+ # @param objects [Array] objects serialized at the current level
97
+ #
98
+ # @return [Array] the underlying records
99
+ #
100
+ def self.records(serializer_class, objects)
101
+ return objects unless serializer_class.plugin_used?(:presenter)
102
+
103
+ objects.map(&:__getobj__)
86
104
  end
87
105
  end
88
106
 
@@ -42,9 +42,6 @@ class Serega
42
42
  # end
43
43
  #
44
44
  module If
45
- # This value must be returned to identify that serialization key was skipped
46
- KEY_SKIPPED = :_key_skipped_with_serega_if_plugin
47
-
48
45
  # @return [Symbol] Plugin name
49
46
  def self.plugin_name
50
47
  :if
@@ -270,16 +267,18 @@ class Serega
270
267
  module ObjectSerializerInstanceMethods
271
268
  private
272
269
 
273
- def serialize_point(object, point, _container)
274
- return KEY_SKIPPED unless point.satisfy_if_conditions?(object, context)
270
+ # Skip the whole point for this object (its value is never resolved).
271
+ def resolve_point(object, point, _container, _batches, _child_plan)
272
+ return unless point.satisfy_if_conditions?(object, context)
273
+
275
274
  super
276
275
  end
277
276
 
278
- def attach_final_value(value, point, container)
279
- unless point.satisfy_if_value_conditions?(value, context)
280
- container.delete(point.name) # remove reserved placeholder
281
- return KEY_SKIPPED
282
- end
277
+ # Skip only the assignment based on the resolved value. Levels resolve
278
+ # points in attribute order, so simply not assigning keeps the remaining
279
+ # keys in declared order — no slot to reserve or delete.
280
+ def write_value(value, point, _container)
281
+ return unless point.satisfy_if_value_conditions?(value, context)
283
282
 
284
283
  super
285
284
  end
@@ -125,11 +125,14 @@ class Serega
125
125
  private
126
126
 
127
127
  #
128
- # Replaces serialized object with Presenter.new(object, ctx)
128
+ # Wraps each serialized object in Presenter.new(object, ctx) before it is
129
+ # enqueued, so the whole level — value resolution and batch loaders alike —
130
+ # sees presenters.
129
131
  #
130
- def serialize_object(object)
131
- object = self.class.serializer_class::Presenter.new(object, context)
132
- super
132
+ def enqueue(objects)
133
+ presenter = self.class.serializer_class::Presenter
134
+ presenters = objects.map { |object| presenter.new(object, context) }
135
+ super(presenters)
133
136
  end
134
137
  end
135
138
  end
data/lib/serega.rb CHANGED
@@ -31,10 +31,9 @@ require_relative "serega/attribute_value_resolvers/delegate"
31
31
  require_relative "serega/attribute_value_resolvers/keyword"
32
32
  require_relative "serega/attribute"
33
33
  require_relative "serega/attribute_normalizer"
34
- require_relative "serega/batch/attribute_loader"
35
- require_relative "serega/batch/attribute_loaders"
36
- require_relative "serega/batch/level"
37
- require_relative "serega/batch/loader"
34
+ require_relative "serega/engine/level_queue"
35
+ require_relative "serega/engine/level"
36
+ require_relative "serega/engine/loader"
38
37
  require_relative "serega/validations/utils/check_allowed_keys"
39
38
  require_relative "serega/validations/utils/check_opt_is_bool"
40
39
  require_relative "serega/validations/utils/check_opt_is_hash"
@@ -86,15 +85,10 @@ class Serega
86
85
  check_batch_loader_params_class.serializer_class = self
87
86
  const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
88
87
 
89
- # Assigns `SeregaBatchLoader` constant to current class
90
- batch_loader_class = Class.new(SeregaBatch::Loader)
91
- batch_loader_class.serializer_class = self
92
- const_set(:SeregaBatchLoader, batch_loader_class)
93
-
94
- # Assigns `SeregaBatchAttributeLoader` constant to current class
95
- batch_attribute_loader_class = Class.new(SeregaBatch::AttributeLoader)
96
- batch_attribute_loader_class.serializer_class = self
97
- const_set(:SeregaBatchAttributeLoader, batch_attribute_loader_class)
88
+ # Assigns `SeregaEngineLoader` constant to current class
89
+ engine_loader_class = Class.new(SeregaEngine::Loader)
90
+ engine_loader_class.serializer_class = self
91
+ const_set(:SeregaEngineLoader, engine_loader_class)
98
92
 
99
93
  #
100
94
  # Serializers class methods
@@ -214,7 +208,7 @@ class Serega
214
208
  def batch(name, value = nil, &block)
215
209
  raise SeregaError, "Batch loader must be defined with a callable value or block" if (value && block) || (!value && !block)
216
210
 
217
- batch_loader = self::SeregaBatchLoader.new(name: name, block: value || block)
211
+ batch_loader = self::SeregaEngineLoader.new(name: name, block: value || block)
218
212
  batch_loaders[batch_loader.name] = batch_loader
219
213
  end
220
214
 
@@ -333,13 +327,9 @@ class Serega
333
327
  plan_point_class.serializer_class = subclass
334
328
  subclass.const_set(:SeregaPlanPoint, plan_point_class)
335
329
 
336
- batch_loader_class = Class.new(self::SeregaBatchLoader)
337
- batch_loader_class.serializer_class = subclass
338
- subclass.const_set(:SeregaBatchLoader, batch_loader_class)
339
-
340
- batch_attribute_loader_class = Class.new(self::SeregaBatchAttributeLoader)
341
- batch_attribute_loader_class.serializer_class = subclass
342
- subclass.const_set(:SeregaBatchAttributeLoader, batch_attribute_loader_class)
330
+ engine_loader_class = Class.new(self::SeregaEngineLoader)
331
+ engine_loader_class.serializer_class = subclass
332
+ subclass.const_set(:SeregaEngineLoader, engine_loader_class)
343
333
 
344
334
  object_serializer_class = Class.new(self::SeregaObjectSerializer)
345
335
  object_serializer_class.serializer_class = subclass
@@ -481,7 +471,7 @@ class Serega
481
471
  self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
482
472
 
483
473
  opts[:context] ||= {}
484
- opts[:batch_loaders] = SeregaBatch::AttributeLoaders.new(opts[:context])
474
+ opts[:level_queue] = SeregaEngine::LevelQueue.new
485
475
  opts[:many] = object.is_a?(Enumerable) unless opts.key?(:many)
486
476
  opts[:plan] = plan
487
477
  opts
@@ -493,7 +483,7 @@ class Serega
493
483
  # - plugin :metadata (adds metadata to final result)
494
484
  def serialize(object, opts)
495
485
  result = self.class::SeregaObjectSerializer.new(**opts).serialize(object)
496
- opts[:batch_loaders].load_all
486
+ opts[:level_queue].run
497
487
  result
498
488
  end
499
489
  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.37.2
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Glushkov
@@ -33,12 +33,11 @@ files:
33
33
  - lib/serega/attribute_value_resolvers/const.rb
34
34
  - lib/serega/attribute_value_resolvers/delegate.rb
35
35
  - lib/serega/attribute_value_resolvers/keyword.rb
36
- - lib/serega/batch/attribute_loader.rb
37
- - lib/serega/batch/attribute_loaders.rb
38
- - lib/serega/batch/level.rb
39
- - lib/serega/batch/loader.rb
40
36
  - lib/serega/config.rb
41
37
  - lib/serega/data_builder.rb
38
+ - lib/serega/engine/level.rb
39
+ - lib/serega/engine/level_queue.rb
40
+ - lib/serega/engine/loader.rb
42
41
  - lib/serega/errors.rb
43
42
  - lib/serega/helpers/serializer_class_helper.rb
44
43
  - lib/serega/object_serializer.rb
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- #
5
- # Batch feature main module
6
- #
7
- module SeregaBatch
8
- # Remembers data required to batch load single attribute
9
- class AttributeLoader
10
- # Instance methods for AttributeLoader
11
- module InstanceMethods
12
- # @param point [SeregaPlanPoint]
13
- # @param level [SeregaBatch::Level] Shared objects and results for this point's plan level
14
- def initialize(point, level)
15
- @point = point
16
- @level = level
17
- @serialized_object_attachers = []
18
- end
19
-
20
- # Stores object with attacher to find and attach attribute values in batch later.
21
- # @param object [Object] Serialized object
22
- # @param attacher [Proc] Proc that attaches found values to originated attributes
23
- # @return [void]
24
- def store(object, attacher)
25
- serialized_object_attachers << [object, attacher]
26
- end
27
-
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.
32
- # @return [void]
33
- def preload_associations
34
- preloads = point.attribute.preloads
35
- return unless preloads
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)."
40
- end
41
-
42
- handler.call(level.objects, preloads)
43
- rescue => error
44
- SeregaUtils::SerializedAttributeError.call(error, point)
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
- SeregaUtils::SerializedAttributeError.call(error, point)
55
- end
56
-
57
- # Attaches already loaded batch values to every stored object.
58
- # Value resolution happens here (inside the attacher), so errors are
59
- # annotated with the attribute, matching the synchronous walk.
60
- # @param batches [Hash] Loaded values grouped by batch loader name
61
- # @return [void]
62
- def attach(batches)
63
- serialized_object_attachers.each do |object, attacher|
64
- attacher.call(object, batches)
65
- end
66
- rescue => error
67
- SeregaUtils::SerializedAttributeError.call(error, point)
68
- end
69
-
70
- attr_reader :point
71
- attr_reader :level
72
-
73
- private
74
-
75
- attr_reader :serialized_object_attachers
76
- end
77
-
78
- extend SeregaHelpers::SerializerClassHelper
79
- include InstanceMethods
80
- end
81
- end
82
- end
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- #
5
- # Batch feature main module
6
- #
7
- module SeregaBatch
8
- # Reserved batch-loader name that marks relation/preload attributes as
9
- # batch-processed. It has no registered loader — the attribute's value comes
10
- # from its own resolver — so `load_batches` skips it (nothing to load).
11
- AUTO_BATCH_LOADER_NAME = :__auto_batch__
12
-
13
- #
14
- # Batch loaders
15
- #
16
- # Remembers data required to batch load all attributes
17
- class AttributeLoaders
18
- #
19
- # Initializes new batch loaders
20
- #
21
- # @param context [Hash] Serialization context, constant for the whole run
22
- #
23
- def initialize(context)
24
- @context = context
25
- @attribute_loaders = []
26
- @point_index = {}.compare_by_identity
27
- @levels = {}.compare_by_identity
28
- end
29
-
30
- # Gathers a chunk of objects serialized at a plan level, so every attribute at
31
- # that level shares one set of objects to batch load against.
32
- #
33
- # @param plan [SeregaPlan] Plan serializing these objects
34
- # @param objects [Array] Objects being serialized at this level
35
- #
36
- # @return [void]
37
- def add_objects(plan, objects)
38
- level_for(plan).add_objects(objects)
39
- end
40
-
41
- # Remembers data for batch serialization:
42
- #
43
- # @param point [SeregaPlanPoint] Serialization plan point
44
- # @param object [Object] Serialized object
45
- # @param attacher [Proc] Serialized value attacher
46
- #
47
- # @return [void]
48
- def remember(point, object, attacher)
49
- attribute_loader = point_index[point]
50
-
51
- unless attribute_loader
52
- serializer_class = point.class.serializer_class
53
- attribute_loader = serializer_class::SeregaBatchAttributeLoader.new(point, level_for(point.plan))
54
- attribute_loaders << attribute_loader
55
- point_index[point] = attribute_loader
56
- end
57
-
58
- attribute_loader.store(object, attacher)
59
- end
60
-
61
- #
62
- # Loads all registered batches and attaches loaded values.
63
- #
64
- # Iterates over each loader including loaders added during iteration
65
- # (child serializers discovered inside a batch flush add new loaders).
66
- def load_all
67
- i = 0
68
- while i < attribute_loaders.size
69
- attribute_loader = attribute_loaders[i]
70
- attribute_loader.attach(load_batches(attribute_loader))
71
- i += 1
72
- end
73
- end
74
-
75
- private
76
-
77
- # Serialization context shared by every level
78
- attr_reader :context
79
-
80
- # keeps all AttributeLoader instances
81
- attr_reader :attribute_loaders
82
-
83
- # keeps tracking of already added points
84
- attr_reader :point_index
85
-
86
- # keeps one Level per plan (compare_by_identity: plan instance identifies a level)
87
- attr_reader :levels
88
-
89
- def level_for(plan)
90
- levels[plan] ||= Level.new(context)
91
- end
92
-
93
- # Builds the { batch_loader_name => loaded_values } hash for one attribute.
94
- #
95
- # Loading is delegated to the attribute's Level, which loads each named batch
96
- # once for the whole level: attributes sharing a loader over the same objects
97
- # reuse a single result, deeper levels load separately.
98
- def load_batches(attribute_loader)
99
- attribute_loader.preload_associations
100
-
101
- point = attribute_loader.point
102
- serializer_class = point.class.serializer_class
103
-
104
- batches = {}
105
- point.batch_loaders.each do |batch_loader_name|
106
- # Auto-batched relations/preloads carry the marker, not a real loader:
107
- # their value comes from their own resolver, so there is nothing to load.
108
- next if batch_loader_name == AUTO_BATCH_LOADER_NAME
109
-
110
- loader = serializer_class.batch_loaders[batch_loader_name]
111
- batches[batch_loader_name] = attribute_loader.load_batch(loader)
112
- end
113
- batches
114
- end
115
- end
116
- end
117
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- #
5
- # Batch feature main module
6
- #
7
- module SeregaBatch
8
- #
9
- # Objects serialized at one plan level, together with their loaded batch results.
10
- #
11
- # Every attribute serialized at the same level shares one Level, so a named batch
12
- # loader reused by several of them runs once for the whole set of objects. The same
13
- # loader over a different level (deeper nesting) uses a different Level and loads
14
- # again.
15
- #
16
- class Level
17
- # @return [Array] Objects gathered for this level
18
- attr_reader :objects
19
-
20
- # @param context [Hash] Serialization context
21
- def initialize(context)
22
- @context = context
23
- @objects = []
24
- @results = {}.compare_by_identity
25
- end
26
-
27
- # Adds a chunk of serialized objects to this level.
28
- # @param objects [Array] Objects being serialized at this level
29
- # @return [void]
30
- def add_objects(objects)
31
- @objects.concat(objects)
32
- end
33
-
34
- # Loads and returns values for the given batch loader, once per loader.
35
- #
36
- # Freezes the gathered objects on the first load: all of a level's objects
37
- # are added before any of its batches load, so sealing the set here guards
38
- # against a later stray `add_objects` and against a loader mutating it.
39
- #
40
- # @param loader [SeregaBatch::Loader] Named batch loader
41
- # @return [Object] Loaded values
42
- def load(loader)
43
- @objects.freeze
44
- @results[loader] ||= loader.load(@objects, @context)
45
- end
46
- end
47
- end
48
- end