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 +4 -4
- data/VERSION +1 -1
- data/lib/serega/attribute_normalizer.rb +18 -11
- 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 +63 -65
- 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/utils/serialized_attribute_error.rb +28 -0
- data/lib/serega.rb +14 -23
- metadata +5 -5
- data/lib/serega/batch/attribute_loader.rb +0 -85
- data/lib/serega/batch/attribute_loaders.rb +0 -108
- 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
|
|
@@ -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
|
-
#
|
|
112
|
+
# Preloads declared for this attribute, passed through as provided.
|
|
117
113
|
#
|
|
118
|
-
# @return [
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
@@ -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,92 +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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
plan.points.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 :
|
|
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
|
|
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
|
#
|
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
|
|
@@ -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/
|
|
34
|
-
require_relative "serega/
|
|
35
|
-
require_relative "serega/
|
|
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 `
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const_set(:
|
|
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::
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
subclass.const_set(:
|
|
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[:
|
|
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[:
|
|
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.
|
|
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
|
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
|