serega 0.37.1 → 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: ca01b1c5668c5ce5963e5f2b71540dae1d023eed44c6e9e026d43e63fcead4c6
4
- data.tar.gz: f526d737921efe1789751b51ff15b503b0febe2d5aafbb1a57666625a14b2228
3
+ metadata.gz: 384da177845e2385a0100d23147cea0034860b148cd46738dc3d6ec7ea7d7eda
4
+ data.tar.gz: f08d482a83cb0cde30ae4c03e924f50be6aaabea24d76564bc47a7a1eadf4f98
5
5
  SHA512:
6
- metadata.gz: 643c12f65a16dbce5d05a8262df0f85fc6a5055436a65d2590f0615ccba5a9cb2d1287d4ec1262b9d0f252432262e88799b55d13ca7ce5fed252c58faf9b7a1a
7
- data.tar.gz: fa8ad847f04617c8ea70cf227aca9bf9ede02c1b1449afb6547b821f09cd8a27638cb5d8904ddc576dfe12c01d5cd6991a99e7cb9fa1cf28d46d2053c7650978
6
+ metadata.gz: c9f2f95d8a74ce4951f7acef89a1cf603fcba2f15af50d246f85d58e1456b06c03b749fef620c741f8e2482999c6f7471fb5dff9a904b275f79f039999ab7fdc
7
+ data.tar.gz: 69c1b8f898f81317310e875a8771db096b3d4613b34769024d66fe801554e53094fd59e3cfea923ef81bb5d837a31ef28ceadc825003c95a4a2340f6b0b0bf74
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.37.1
1
+ 0.38.0
@@ -9,10 +9,6 @@ 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] } }
16
12
  # Attribute initial params
17
13
  # @return [Hash] Attribute initial params
18
14
  attr_reader :init_name, :init_opts, :init_block
@@ -113,9 +109,9 @@ class Serega
113
109
  end
114
110
 
115
111
  #
116
- # Shows normalized preloads for current attribute
112
+ # Preloads declared for this attribute, passed through as provided.
117
113
  #
118
- # @return [Hash, nil] normalized preloads of current attribute
114
+ # @return [Object, nil] declared preloads, or nil when the attribute has none
119
115
  #
120
116
  def preloads
121
117
  return @preloads if instance_variable_defined?(:@preloads)
@@ -152,13 +148,24 @@ class Serega
152
148
 
153
149
  hide_setting = config.hide_by_default
154
150
  return true if hide_setting == true
155
- return true if hide_setting == :auto && (preloads || init_opts.key?(:batch))
151
+ return true if hide_setting == :auto && preload_or_batch?
156
152
 
157
153
  # Return nil for undefined value which means "not hide" but allows
158
154
  # to change this value by plugins
159
155
  nil
160
156
  end
161
157
 
158
+ # The `hide_by_default = :auto` criterion: hide attributes that fetch extra
159
+ # data on demand — those declared with `:preload` or an explicit `:batch`.
160
+ #
161
+ # This is intentionally narrower than a "goes through the batch mechanism"
162
+ # check (`SeregaPlanPoint#batch?`): a plain relation (`serializer:` without
163
+ # `:preload`) is batch-processed too, but it only serializes an already-loaded
164
+ # nested object, so it stays visible by default.
165
+ def preload_or_batch?
166
+ !!(preloads || init_opts.key?(:batch))
167
+ end
168
+
162
169
  def config
163
170
  self.class.serializer_class.config
164
171
  end
@@ -192,14 +199,14 @@ class Serega
192
199
  AttributeValueResolvers::BatchResolver.get(self.class.serializer_class, name, batch_opt)
193
200
  end
194
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).
195
205
  def prepare_batch_loaders
196
206
  batch_opt = init_opts[:batch]
197
207
  return explicit_batch_loaders(batch_opt) if batch_opt
198
- return FROZEN_EMPTY_ARRAY unless serializer || preloads
199
208
 
200
- loader_name = :"__auto_batch_#{name}__"
201
- self.class.serializer_class.batch(loader_name, AUTO_BATCH_LOADER)
202
- [loader_name]
209
+ FROZEN_EMPTY_ARRAY
203
210
  end
204
211
 
205
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,92 +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
47
-
48
- def serialize_array(objects)
49
- objects.map { |object| serialize_object(object) }
50
- end
51
-
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
- reraise_with_serialized_attribute_details(error, point)
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
57
+
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)
64
+
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
105
- end
106
-
107
- def reraise_with_serialized_attribute_details(error, point)
108
- raise error.exception(<<~MESSAGE.strip)
109
- #{error.message}
110
- (when serializing '#{point.name}' attribute in #{self.class.serializer_class})
111
- MESSAGE
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
112
110
  end
113
111
  end
114
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
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaUtils
5
+ #
6
+ # Reraises an error, adding which attribute and serializer were being
7
+ # serialized when it happened. Shared by the synchronous serialization walk
8
+ # and the batch attach phase so the message stays identical for every
9
+ # attribute, whether its value is resolved inline or during batch loading.
10
+ #
11
+ module SerializedAttributeError
12
+ module_function
13
+
14
+ #
15
+ # @param error [Exception] Original error
16
+ # @param point [SeregaPlanPoint] Plan point being serialized
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(error, point)
21
+ raise error.exception(<<~MESSAGE.strip)
22
+ #{error.message}
23
+ (when serializing '#{point.name}' attribute in #{point.class.serializer_class})
24
+ MESSAGE
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/serega.rb CHANGED
@@ -22,6 +22,7 @@ require_relative "serega/helpers/serializer_class_helper"
22
22
  require_relative "serega/utils/enum_deep_dup"
23
23
  require_relative "serega/utils/enum_deep_freeze"
24
24
  require_relative "serega/utils/method_signature"
25
+ require_relative "serega/utils/serialized_attribute_error"
25
26
  require_relative "serega/utils/symbol_name"
26
27
  require_relative "serega/utils/to_hash"
27
28
  require_relative "serega/attribute_value_resolvers/batch"
@@ -30,10 +31,9 @@ require_relative "serega/attribute_value_resolvers/delegate"
30
31
  require_relative "serega/attribute_value_resolvers/keyword"
31
32
  require_relative "serega/attribute"
32
33
  require_relative "serega/attribute_normalizer"
33
- require_relative "serega/batch/attribute_loader"
34
- require_relative "serega/batch/attribute_loaders"
35
- require_relative "serega/batch/level"
36
- require_relative "serega/batch/loader"
34
+ require_relative "serega/engine/level_queue"
35
+ require_relative "serega/engine/level"
36
+ require_relative "serega/engine/loader"
37
37
  require_relative "serega/validations/utils/check_allowed_keys"
38
38
  require_relative "serega/validations/utils/check_opt_is_bool"
39
39
  require_relative "serega/validations/utils/check_opt_is_hash"
@@ -85,15 +85,10 @@ class Serega
85
85
  check_batch_loader_params_class.serializer_class = self
86
86
  const_set(:CheckBatchLoaderParams, check_batch_loader_params_class)
87
87
 
88
- # Assigns `SeregaBatchLoader` constant to current class
89
- batch_loader_class = Class.new(SeregaBatch::Loader)
90
- batch_loader_class.serializer_class = self
91
- const_set(:SeregaBatchLoader, batch_loader_class)
92
-
93
- # Assigns `SeregaBatchAttributeLoader` constant to current class
94
- batch_attribute_loader_class = Class.new(SeregaBatch::AttributeLoader)
95
- batch_attribute_loader_class.serializer_class = self
96
- 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)
97
92
 
98
93
  #
99
94
  # Serializers class methods
@@ -213,7 +208,7 @@ class Serega
213
208
  def batch(name, value = nil, &block)
214
209
  raise SeregaError, "Batch loader must be defined with a callable value or block" if (value && block) || (!value && !block)
215
210
 
216
- batch_loader = self::SeregaBatchLoader.new(name: name, block: value || block)
211
+ batch_loader = self::SeregaEngineLoader.new(name: name, block: value || block)
217
212
  batch_loaders[batch_loader.name] = batch_loader
218
213
  end
219
214
 
@@ -332,13 +327,9 @@ class Serega
332
327
  plan_point_class.serializer_class = subclass
333
328
  subclass.const_set(:SeregaPlanPoint, plan_point_class)
334
329
 
335
- batch_loader_class = Class.new(self::SeregaBatchLoader)
336
- batch_loader_class.serializer_class = subclass
337
- subclass.const_set(:SeregaBatchLoader, batch_loader_class)
338
-
339
- batch_attribute_loader_class = Class.new(self::SeregaBatchAttributeLoader)
340
- batch_attribute_loader_class.serializer_class = subclass
341
- 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)
342
333
 
343
334
  object_serializer_class = Class.new(self::SeregaObjectSerializer)
344
335
  object_serializer_class.serializer_class = subclass
@@ -480,7 +471,7 @@ class Serega
480
471
  self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
481
472
 
482
473
  opts[:context] ||= {}
483
- opts[:batch_loaders] = SeregaBatch::AttributeLoaders.new(opts[:context])
474
+ opts[:level_queue] = SeregaEngine::LevelQueue.new
484
475
  opts[:many] = object.is_a?(Enumerable) unless opts.key?(:many)
485
476
  opts[:plan] = plan
486
477
  opts
@@ -492,7 +483,7 @@ class Serega
492
483
  # - plugin :metadata (adds metadata to final result)
493
484
  def serialize(object, opts)
494
485
  result = self.class::SeregaObjectSerializer.new(**opts).serialize(object)
495
- opts[:batch_loaders].load_all
486
+ opts[:level_queue].run
496
487
  result
497
488
  end
498
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.1
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
@@ -74,6 +73,7 @@ files:
74
73
  - lib/serega/utils/enum_deep_dup.rb
75
74
  - lib/serega/utils/enum_deep_freeze.rb
76
75
  - lib/serega/utils/method_signature.rb
76
+ - lib/serega/utils/serialized_attribute_error.rb
77
77
  - lib/serega/utils/symbol_name.rb
78
78
  - lib/serega/utils/to_hash.rb
79
79
  - lib/serega/validations/attribute/check_block.rb
@@ -1,85 +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
- 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)
61
- serialized_object_attachers.each do |object, attacher|
62
- attacher.call(object, batches)
63
- end
64
- end
65
-
66
- attr_reader :point
67
- attr_reader :level
68
-
69
- private
70
-
71
- def reraise_with_serialized_attribute_details(error)
72
- raise error.exception(<<~MESSAGE.strip)
73
- #{error.message}
74
- (when serializing '#{point.name}' attribute in #{point.class.serializer_class})
75
- MESSAGE
76
- end
77
-
78
- attr_reader :serialized_object_attachers
79
- end
80
-
81
- extend SeregaHelpers::SerializerClassHelper
82
- include InstanceMethods
83
- end
84
- end
85
- end
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Serega
4
- #
5
- # Batch feature main module
6
- #
7
- module SeregaBatch
8
- #
9
- # Batch loaders
10
- #
11
- # Remembers data required to batch load all attributes
12
- class AttributeLoaders
13
- #
14
- # Initializes new batch loaders
15
- #
16
- # @param context [Hash] Serialization context, constant for the whole run
17
- #
18
- def initialize(context)
19
- @context = context
20
- @attribute_loaders = []
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)
34
- end
35
-
36
- # Remembers data for batch serialization:
37
- #
38
- # @param point [SeregaPlanPoint] Serialization plan point
39
- # @param object [Object] Serialized object
40
- # @param attacher [Proc] Serialized value attacher
41
- #
42
- # @return [void]
43
- def remember(point, object, attacher)
44
- attribute_loader = point_index[point]
45
-
46
- unless attribute_loader
47
- serializer_class = point.class.serializer_class
48
- attribute_loader = serializer_class::SeregaBatchAttributeLoader.new(point, level_for(point.plan))
49
- attribute_loaders << attribute_loader
50
- point_index[point] = attribute_loader
51
- end
52
-
53
- attribute_loader.store(object, attacher)
54
- end
55
-
56
- #
57
- # Loads all registered batches and attaches loaded values.
58
- #
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
67
- end
68
- end
69
-
70
- private
71
-
72
- # Serialization context shared by every level
73
- attr_reader :context
74
-
75
- # keeps all AttributeLoader instances
76
- attr_reader :attribute_loaders
77
-
78
- # keeps tracking of already added points
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
106
- end
107
- end
108
- 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