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 +4 -4
- data/VERSION +1 -1
- data/lib/serega/attribute_normalizer.rb +6 -8
- data/lib/serega/engine/level.rb +57 -0
- data/lib/serega/engine/level_queue.rb +54 -0
- data/lib/serega/{batch → engine}/loader.rb +1 -1
- data/lib/serega/object_serializer.rb +61 -56
- data/lib/serega/plan.rb +0 -9
- data/lib/serega/plan_point.rb +56 -5
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +19 -1
- data/lib/serega/plugins/if/if.rb +9 -10
- data/lib/serega/plugins/presenter/presenter.rb +7 -4
- data/lib/serega.rb +13 -23
- metadata +4 -5
- data/lib/serega/batch/attribute_loader.rb +0 -82
- data/lib/serega/batch/attribute_loaders.rb +0 -117
- data/lib/serega/batch/level.rb +0 -48
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 384da177845e2385a0100d23147cea0034860b148cd46738dc3d6ec7ea7d7eda
|
|
4
|
+
data.tar.gz: f08d482a83cb0cde30ae4c03e924f50be6aaabea24d76564bc47a7a1eadf4f98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9f2f95d8a74ce4951f7acef89a1cf603fcba2f15af50d246f85d58e1456b06c03b749fef620c741f8e2482999c6f7471fb5dff9a904b275f79f039999ab7fdc
|
|
7
|
+
data.tar.gz: 69c1b8f898f81317310e875a8771db096b3d4613b34769024d66fe801554e53094fd59e3cfea923ef81bb5d837a31ef28ceadc825003c95a4a2340f6b0b0bf74
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.38.0
|
|
@@ -109,9 +109,9 @@ class Serega
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
#
|
|
112
|
-
#
|
|
112
|
+
# Preloads declared for this attribute, passed through as provided.
|
|
113
113
|
#
|
|
114
|
-
# @return [
|
|
114
|
+
# @return [Object, nil] declared preloads, or nil when the attribute has none
|
|
115
115
|
#
|
|
116
116
|
def preloads
|
|
117
117
|
return @preloads if instance_variable_defined?(:@preloads)
|
|
@@ -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
|
-
|
|
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
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
4
|
#
|
|
5
|
-
# Low-level class
|
|
6
|
-
#
|
|
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, :
|
|
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
|
-
@
|
|
30
|
+
@level_queue = opts[:level_queue]
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
#
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 :
|
|
64
|
-
def
|
|
65
|
-
|
|
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
|
-
|
|
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 =
|
|
77
|
-
|
|
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 (
|
|
82
|
-
def
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
#
|
data/lib/serega/plan_point.rb
CHANGED
|
@@ -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
|
-
# @
|
|
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
|
|
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
|
|
data/lib/serega/plugins/if/if.rb
CHANGED
|
@@ -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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
#
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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/
|
|
35
|
-
require_relative "serega/
|
|
36
|
-
require_relative "serega/
|
|
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 `
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const_set(:
|
|
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::
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
subclass.const_set(:
|
|
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[:
|
|
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[:
|
|
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.
|
|
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
|
data/lib/serega/batch/level.rb
DELETED
|
@@ -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
|