computed_model 0.1.0 → 0.3.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/.github/dependabot.yml +18 -0
- data/.github/workflows/test.yml +24 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +126 -0
- data/CONCEPTS.ja.md +324 -0
- data/CONCEPTS.md +330 -0
- data/Migration-0.3.md +343 -0
- data/README.ja.md +168 -0
- data/README.md +112 -70
- data/Rakefile +14 -0
- data/computed_model.gemspec +10 -2
- data/lib/computed_model.rb +78 -153
- data/lib/computed_model/dep_graph.rb +245 -0
- data/lib/computed_model/model.rb +447 -0
- data/lib/computed_model/plan.rb +48 -0
- data/lib/computed_model/version.rb +1 -1
- metadata +102 -7
- data/.travis.yml +0 -6
@@ -0,0 +1,447 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
# A mixin for batch-loadable compound models. This is the main API of ComputedModel.
|
6
|
+
#
|
7
|
+
# See {ComputedModel::Model::ClassMethods} for methods you can use in the including classes.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# require 'computed_model'
|
11
|
+
#
|
12
|
+
# # Consider them external sources (ActiveRecord or resources obtained via HTTP)
|
13
|
+
# RawUser = Struct.new(:id, :name, :title)
|
14
|
+
# Preference = Struct.new(:user_id, :name_public)
|
15
|
+
#
|
16
|
+
# class User
|
17
|
+
# include ComputedModel::Model
|
18
|
+
#
|
19
|
+
# attr_reader :id
|
20
|
+
# def initialize(raw_user)
|
21
|
+
# @id = raw_user.id
|
22
|
+
# @raw_user = raw_user
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def self.list(ids, with:)
|
26
|
+
# bulk_load_and_compute(Array(with), ids: ids)
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# define_primary_loader :raw_user do |_subfields, ids:, **|
|
30
|
+
# # In ActiveRecord:
|
31
|
+
# # raw_users = RawUser.where(id: ids).to_a
|
32
|
+
# raw_users = [
|
33
|
+
# RawUser.new(1, "Tanaka Taro", "Mr. "),
|
34
|
+
# RawUser.new(2, "Yamada Hanako", "Dr. "),
|
35
|
+
# ].filter { |u| ids.include?(u.id) }
|
36
|
+
# raw_users.map { |u| User.new(u) }
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# define_loader :preference, key: -> { id } do |user_ids, _subfields, **|
|
40
|
+
# # In ActiveRecord:
|
41
|
+
# # Preference.where(user_id: user_ids).index_by(&:user_id)
|
42
|
+
# {
|
43
|
+
# 1 => Preference.new(1, true),
|
44
|
+
# 2 => Preference.new(2, false),
|
45
|
+
# }.filter { |k, _v| user_ids.include?(k) }
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# delegate_dependency :name, to: :raw_user
|
49
|
+
# delegate_dependency :title, to: :raw_user
|
50
|
+
# delegate_dependency :name_public, to: :preference
|
51
|
+
#
|
52
|
+
# dependency :name, :name_public
|
53
|
+
# computed def public_name
|
54
|
+
# name_public ? name : "Anonymous"
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# dependency :public_name, :title
|
58
|
+
# computed def public_name_with_title
|
59
|
+
# "#{title}#{public_name}"
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# # You can only access the field you requested ahead of time
|
64
|
+
# users = User.list([1, 2], with: [:public_name_with_title])
|
65
|
+
# users.map(&:public_name_with_title) # => ["Mr. Tanaka Taro", "Dr. Anonymous"]
|
66
|
+
# users.map(&:public_name) # => error (ForbiddenDependency)
|
67
|
+
#
|
68
|
+
# users = User.list([1, 2], with: [:public_name_with_title, :public_name])
|
69
|
+
# users.map(&:public_name_with_title) # => ["Mr. Tanaka Taro", "Dr. Anonymous"]
|
70
|
+
# users.map(&:public_name) # => ["Tanaka Taro", "Anonymous"]
|
71
|
+
#
|
72
|
+
# # In this case, preference will not be loaded.
|
73
|
+
# users = User.list([1, 2], with: [:title])
|
74
|
+
# users.map(&:title) # => ["Mr. ", "Dr. "]
|
75
|
+
|
76
|
+
module ComputedModel::Model
|
77
|
+
extend ActiveSupport::Concern
|
78
|
+
|
79
|
+
# A set of class methods for {ComputedModel::Model}. Automatically included to the
|
80
|
+
# singleton class when you include {ComputedModel::Model}.
|
81
|
+
#
|
82
|
+
# See {ComputedModel::Model} for examples.
|
83
|
+
module ClassMethods
|
84
|
+
# Declares the dependency of a computed field.
|
85
|
+
# Normally a call to this method will be followed by a call to {#computed} (or {#define_loader}).
|
86
|
+
#
|
87
|
+
# @param deps [Array<Symbol, Hash{Symbol=>Array, Object}>]
|
88
|
+
# Dependency list. Most simply an array of Symbols (field names).
|
89
|
+
#
|
90
|
+
# It also accepts Hashes. In this case, the keys of the hashes are field names.
|
91
|
+
# The values are called subfield selectors.
|
92
|
+
#
|
93
|
+
# Subfield selector is one of the following:
|
94
|
+
#
|
95
|
+
# - nil, true, or false (constant condition)
|
96
|
+
# - `#call`able objects accepting one argument (dynamic selector)
|
97
|
+
# - other objects (static selector)
|
98
|
+
#
|
99
|
+
# Multiple subfield selectors can be specified at once as an array.
|
100
|
+
#
|
101
|
+
# See CONCEPTS.md for the more detailed description of dependency formats.
|
102
|
+
# @return [void]
|
103
|
+
# @raise [RuntimeError] if the dependency list contains values other than Symbol or Hash
|
104
|
+
#
|
105
|
+
# @example declaring dependencies
|
106
|
+
# dependency :user, :user_external_resource
|
107
|
+
# computed def something
|
108
|
+
# # Use user and user_external_resource ...
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# @example declaring dependencies with subfield selectors
|
112
|
+
# dependency user: [:user_names, :premium], user_external_resource: [:received_stars]
|
113
|
+
# computed def something
|
114
|
+
# # Use user and user_external_resource ...
|
115
|
+
# end
|
116
|
+
#
|
117
|
+
# @example declaring dynamic dependencies
|
118
|
+
# dependency user: -> (subfields) { "..." }
|
119
|
+
# computed def something
|
120
|
+
# # Use user ...
|
121
|
+
# end
|
122
|
+
def dependency(*deps)
|
123
|
+
@__computed_model_next_dependency ||= []
|
124
|
+
@__computed_model_next_dependency.push(*deps)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Declares a computed field. Normally it follows a call to {#dependency}.
|
128
|
+
#
|
129
|
+
# @param meth_name [Symbol] a method name to promote to a computed field.
|
130
|
+
# Typically used in the form of `computed def ...`.
|
131
|
+
# @return [Symbol] the first argument as-is.
|
132
|
+
#
|
133
|
+
# @example define a field which is calculated from other fields
|
134
|
+
# dependency :user, :user_external_resource
|
135
|
+
# computed def something
|
136
|
+
# # Use user and user_external_resource ...
|
137
|
+
# end
|
138
|
+
def computed(meth_name)
|
139
|
+
var_name = :"@#{meth_name}"
|
140
|
+
meth_name_orig = :"#{meth_name}_orig"
|
141
|
+
compute_meth_name = :"compute_#{meth_name}"
|
142
|
+
|
143
|
+
__computed_model_graph << ComputedModel::DepGraph::Node.new(:computed, meth_name, @__computed_model_next_dependency)
|
144
|
+
remove_instance_variable(:@__computed_model_next_dependency) if defined?(@__computed_model_next_dependency)
|
145
|
+
|
146
|
+
alias_method meth_name_orig, meth_name
|
147
|
+
define_method(meth_name) do
|
148
|
+
raise ComputedModel::NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
|
149
|
+
|
150
|
+
__computed_model_check_availability(meth_name)
|
151
|
+
instance_variable_get(var_name)
|
152
|
+
end
|
153
|
+
define_method(compute_meth_name) do
|
154
|
+
@__computed_model_stack << @__computed_model_plan[meth_name]
|
155
|
+
begin
|
156
|
+
instance_variable_set(var_name, send(meth_name_orig))
|
157
|
+
ensure
|
158
|
+
@__computed_model_stack.pop
|
159
|
+
end
|
160
|
+
end
|
161
|
+
if public_method_defined?(meth_name_orig)
|
162
|
+
public meth_name
|
163
|
+
elsif protected_method_defined?(meth_name_orig)
|
164
|
+
protected meth_name
|
165
|
+
else # elsif private_method_defined?(meth_name_orig)
|
166
|
+
private meth_name
|
167
|
+
end
|
168
|
+
|
169
|
+
meth_name
|
170
|
+
end
|
171
|
+
|
172
|
+
# A shorthand for simple computed field.
|
173
|
+
#
|
174
|
+
# Use {#computed} for more complex definition.
|
175
|
+
#
|
176
|
+
# @param methods [Array<Symbol>] method names to delegate
|
177
|
+
# @param to [Symbol] which field to delegate the methods to.
|
178
|
+
# This parameter is used for the dependency declaration too.
|
179
|
+
# @param allow_nil [nil, Boolean] If `true`,
|
180
|
+
# nil receivers are ignored, and nil is returned instead.
|
181
|
+
# @param prefix [nil, Symbol] A prefix for the delegating method name.
|
182
|
+
# @param include_subfields [nil, Boolean] If `true`,
|
183
|
+
# it includes meth_name as a subfield selector.
|
184
|
+
# @return [void]
|
185
|
+
#
|
186
|
+
# @example delegate name from raw_user
|
187
|
+
# delegate_dependency :name, to: :raw_user
|
188
|
+
#
|
189
|
+
# @example delegate name from raw_user, but expose as user_name
|
190
|
+
# delegate_dependency :name, to: :raw_user, prefix: :user
|
191
|
+
def delegate_dependency(*methods, to:, allow_nil: nil, prefix: nil, include_subfields: nil)
|
192
|
+
method_prefix = prefix ? "#{prefix}_" : ""
|
193
|
+
methods.each do |meth_name|
|
194
|
+
pmeth_name = :"#{method_prefix}#{meth_name}"
|
195
|
+
if include_subfields
|
196
|
+
dependency to=>meth_name
|
197
|
+
else
|
198
|
+
dependency to
|
199
|
+
end
|
200
|
+
if allow_nil
|
201
|
+
define_method(pmeth_name) do
|
202
|
+
send(to)&.public_send(meth_name)
|
203
|
+
end
|
204
|
+
else
|
205
|
+
define_method(pmeth_name) do
|
206
|
+
send(to).public_send(meth_name)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
computed pmeth_name
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Declares a loaded field. See {#dependency} and {#define_primary_loader} too.
|
214
|
+
#
|
215
|
+
# `define_loader :foo do ... end` generates a reader `foo` and a writer `foo=`.
|
216
|
+
# The writer only exists for historical reasons.
|
217
|
+
#
|
218
|
+
# The block passed to `define_loader` is called a loader.
|
219
|
+
# Loader should return a hash containing field values.
|
220
|
+
#
|
221
|
+
# - The keys of the hash must match `record.instance_exec(&key)`.
|
222
|
+
# - The values of the hash represents the field values.
|
223
|
+
#
|
224
|
+
# @param meth_name [Symbol] the name of the loaded field.
|
225
|
+
# @param key [Proc] The proc to collect keys. In the proc, `self` evaluates to the record instance.
|
226
|
+
# Typically `-> { id }`.
|
227
|
+
# @return [void]
|
228
|
+
# @raise [ArgumentError] if no block is given
|
229
|
+
# @yield [keys, subfields, **options]
|
230
|
+
# @yieldparam keys [Array] the array of keys.
|
231
|
+
# @yieldparam subfields [Array] subfield selectors
|
232
|
+
# @yieldparam options [Hash] the batch-loading parameters.
|
233
|
+
# The keyword arguments to {#bulk_load_and_compute} will be passed down here as-is.
|
234
|
+
# @yieldreturn [Hash] a hash containing field values.
|
235
|
+
#
|
236
|
+
# @example define a loader for ActiveRecord-based models
|
237
|
+
# define_loader :user_aux_data, key: -> { id } do |user_ids, subfields, **options|
|
238
|
+
# UserAuxData.where(user_id: user_ids).preload(subfields).group_by(&:id)
|
239
|
+
# end
|
240
|
+
def define_loader(meth_name, key:, &block)
|
241
|
+
raise ArgumentError, "No block given" unless block
|
242
|
+
|
243
|
+
var_name = :"@#{meth_name}"
|
244
|
+
loader_name = :"__computed_model_load_#{meth_name}"
|
245
|
+
writer_name = :"#{meth_name}="
|
246
|
+
|
247
|
+
__computed_model_graph << ComputedModel::DepGraph::Node.new(:loaded, meth_name, @__computed_model_next_dependency)
|
248
|
+
remove_instance_variable(:@__computed_model_next_dependency) if defined?(@__computed_model_next_dependency)
|
249
|
+
define_singleton_method(loader_name) do |objs, subfields, **options|
|
250
|
+
keys = objs.map { |o| o.instance_exec(&key) }
|
251
|
+
field_values = block.call(keys, subfields, **options)
|
252
|
+
objs.zip(keys) do |obj, key|
|
253
|
+
obj.send(writer_name, field_values[key])
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
define_method(meth_name) do
|
258
|
+
raise ComputedModel::NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
|
259
|
+
|
260
|
+
__computed_model_check_availability(meth_name)
|
261
|
+
instance_variable_get(var_name)
|
262
|
+
end
|
263
|
+
# TODO: remove writer?
|
264
|
+
attr_writer meth_name
|
265
|
+
end
|
266
|
+
|
267
|
+
# Declares a primary field. See {#define_loader} and {#dependency} too.
|
268
|
+
# ComputedModel should have exactly one primary field.
|
269
|
+
#
|
270
|
+
# `define_primary_loader :foo do ... end` generates a reader `foo` and
|
271
|
+
# a writer `foo=`.
|
272
|
+
# The writer only exists for historical reasons.
|
273
|
+
#
|
274
|
+
# The block passed to `define_loader` is called a primary loader.
|
275
|
+
# The primary loader's responsibility is batch loading + enumeration (search).
|
276
|
+
# In contrast to {#define_loader}, where a hash of field values are returned,
|
277
|
+
# the primary loader should return an array of record objects.
|
278
|
+
#
|
279
|
+
# For example, if your class is `User`, the primary loader must return `Array<User>`.
|
280
|
+
#
|
281
|
+
# Additionally, the primary loader must initialize all the record objects
|
282
|
+
# so that the same instance variable `@#{meth_name}` is set.
|
283
|
+
#
|
284
|
+
# @param meth_name [Symbol] the name of the loaded field.
|
285
|
+
# @return [Array] an array of record objects.
|
286
|
+
# @raise [ArgumentError] if no block is given
|
287
|
+
# @raise [ArgumentError] if it follows a {#dependency} declaration
|
288
|
+
# @yield [subfields, **options]
|
289
|
+
# @yieldparam subfields [Array] subfield selectors
|
290
|
+
# @yieldparam options [Hash] the batch-loading parameters.
|
291
|
+
# The keyword arguments to {#bulk_load_and_compute} will be passed down here as-is.
|
292
|
+
# @yieldreturn [void]
|
293
|
+
#
|
294
|
+
# @example define a primary loader for ActiveRecord-based models
|
295
|
+
# class User
|
296
|
+
# include ComputedModel::Model
|
297
|
+
#
|
298
|
+
# def initialize(raw_user)
|
299
|
+
# # @raw_user must match the name of the primary loader
|
300
|
+
# @raw_user = raw_user
|
301
|
+
# end
|
302
|
+
#
|
303
|
+
# define_primary_loader :raw_user do |subfields, **options|
|
304
|
+
# raw_users = RawUser.where(id: user_ids).preload(subfields)
|
305
|
+
# # Create User instances
|
306
|
+
# raw_users.map { |raw_user| User.new(raw_user) }
|
307
|
+
# end
|
308
|
+
# end
|
309
|
+
def define_primary_loader(meth_name, &block)
|
310
|
+
# TODO: The current API requires the user to initialize a specific instance variable.
|
311
|
+
# TODO: this design is a bit ugly.
|
312
|
+
if defined?(@__computed_model_next_dependency)
|
313
|
+
remove_instance_variable(:@__computed_model_next_dependency)
|
314
|
+
raise ArgumentError, 'primary field cannot have a dependency'
|
315
|
+
end
|
316
|
+
raise ArgumentError, "No block given" unless block
|
317
|
+
|
318
|
+
var_name = :"@#{meth_name}"
|
319
|
+
loader_name = :"__computed_model_enumerate_#{meth_name}"
|
320
|
+
|
321
|
+
__computed_model_graph << ComputedModel::DepGraph::Node.new(:primary, meth_name, {})
|
322
|
+
define_singleton_method(loader_name) do |subfields, **options|
|
323
|
+
block.call(subfields, **options)
|
324
|
+
end
|
325
|
+
|
326
|
+
define_method(meth_name) do
|
327
|
+
raise ComputedModel::NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
|
328
|
+
|
329
|
+
__computed_model_check_availability(meth_name)
|
330
|
+
instance_variable_get(var_name)
|
331
|
+
end
|
332
|
+
# TODO: remove writer?
|
333
|
+
attr_writer meth_name
|
334
|
+
end
|
335
|
+
|
336
|
+
# The core routine for batch-loading.
|
337
|
+
#
|
338
|
+
# Each model class is expected to provide its own wrapper of this method. See CONCEPTS.md for examples.
|
339
|
+
#
|
340
|
+
# @param deps [Array<Symbol, Hash{Symbol=>Array, Object}>] dependency list. Same format as {#dependency}.
|
341
|
+
# See {ComputedModel.normalize_dependencies} too.
|
342
|
+
# @param options [Hash] the batch-loading parameters.
|
343
|
+
# Passed down as-is to loaders ({#define_loader}) and the primary loader ({#define_primary_loader}).
|
344
|
+
# @return [Array<Object>] The array of record objects, with requested fields filled in.
|
345
|
+
# @raise [ComputedModel::CyclicDependency] if the graph has a cycle
|
346
|
+
# @raise [ArgumentError] if the graph lacks a primary field
|
347
|
+
# @raise [RuntimeError] if the graph has multiple primary fields
|
348
|
+
# @raise [RuntimeError] if the graph has a dangling dependency (reference to an undefined field)
|
349
|
+
def bulk_load_and_compute(deps, **options)
|
350
|
+
objs = nil
|
351
|
+
sorted = __computed_model_sorted_graph
|
352
|
+
plan = sorted.plan(deps)
|
353
|
+
plan.load_order.each do |node|
|
354
|
+
case sorted.original[node.name].type
|
355
|
+
when :primary
|
356
|
+
loader_name = :"__computed_model_enumerate_#{node.name}"
|
357
|
+
objs = send(loader_name, ComputedModel.filter_subfields(node.subfields), **options)
|
358
|
+
dummy_toplevel_node = ComputedModel::Plan::Node.new(nil, plan.toplevel, nil)
|
359
|
+
objs.each do |obj|
|
360
|
+
obj.instance_variable_set(:@__computed_model_plan, plan)
|
361
|
+
obj.instance_variable_set(:@__computed_model_stack, [dummy_toplevel_node])
|
362
|
+
end
|
363
|
+
when :loaded
|
364
|
+
loader_name = :"__computed_model_load_#{node.name}"
|
365
|
+
objs.each do |obj|
|
366
|
+
obj.instance_variable_get(:@__computed_model_stack) << node
|
367
|
+
end
|
368
|
+
begin
|
369
|
+
send(loader_name, objs, ComputedModel.filter_subfields(node.subfields), **options)
|
370
|
+
ensure
|
371
|
+
objs.each do |obj|
|
372
|
+
obj.instance_variable_get(:@__computed_model_stack).pop
|
373
|
+
end
|
374
|
+
end
|
375
|
+
else # when :computed
|
376
|
+
objs.each do |obj|
|
377
|
+
obj.send(:"compute_#{node.name}")
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
objs
|
383
|
+
end
|
384
|
+
|
385
|
+
# Verifies the dependency graph for errors. Useful for early error detection.
|
386
|
+
# It also prevents concurrency issues.
|
387
|
+
#
|
388
|
+
# Place it after all the relevant declarations. Otherwise a mysterious bug may occur.
|
389
|
+
#
|
390
|
+
# @return [void]
|
391
|
+
# @raise [ComputedModel::CyclicDependency] if the graph has a cycle
|
392
|
+
# @raise [ArgumentError] if the graph lacks a primary field
|
393
|
+
# @raise [RuntimeError] if the graph has multiple primary fields
|
394
|
+
# @raise [RuntimeError] if the graph has a dangling dependency (reference to an undefined field)
|
395
|
+
# @example
|
396
|
+
# class User
|
397
|
+
# computed def foo
|
398
|
+
# # ...
|
399
|
+
# end
|
400
|
+
#
|
401
|
+
# # ...
|
402
|
+
#
|
403
|
+
# verify_dependencies
|
404
|
+
# end
|
405
|
+
def verify_dependencies
|
406
|
+
__computed_model_sorted_graph
|
407
|
+
nil
|
408
|
+
end
|
409
|
+
|
410
|
+
# @return [ComputedModel::DepGraph::Sorted]
|
411
|
+
private def __computed_model_sorted_graph
|
412
|
+
@__computed_model_sorted_graph ||= __computed_model_merged_graph.tsort
|
413
|
+
end
|
414
|
+
|
415
|
+
# @return [ComputedModel::DepGraph]
|
416
|
+
private def __computed_model_merged_graph
|
417
|
+
graphs = ancestors.reverse.map { |m| m.respond_to?(:__computed_model_graph, true) ? m.send(:__computed_model_graph) : nil }.compact
|
418
|
+
ComputedModel::DepGraph.merge(graphs)
|
419
|
+
end
|
420
|
+
|
421
|
+
# @return [ComputedModel::DepGraph]
|
422
|
+
private def __computed_model_graph
|
423
|
+
@__computed_model_graph ||= ComputedModel::DepGraph.new
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Returns dependency of the currently computing field,
|
428
|
+
# or the toplevel dependency if called outside of computed fields.
|
429
|
+
# @return [Set<Symbol>]
|
430
|
+
def current_deps
|
431
|
+
@__computed_model_stack.last.deps
|
432
|
+
end
|
433
|
+
|
434
|
+
# Returns subfield selectors passed to the currently computing field,
|
435
|
+
# or nil if called outside of computed fields.
|
436
|
+
# @return [ComputedModel::NormalizableArray, nil]
|
437
|
+
def current_subfields
|
438
|
+
@__computed_model_stack.last.subfields
|
439
|
+
end
|
440
|
+
|
441
|
+
# @param name [Symbol]
|
442
|
+
private def __computed_model_check_availability(name)
|
443
|
+
return if @__computed_model_stack.last.deps.include?(name)
|
444
|
+
|
445
|
+
raise ComputedModel::ForbiddenDependency, "Not a direct dependency: #{name}"
|
446
|
+
end
|
447
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module ComputedModel
|
6
|
+
# A plan for batch loading. Created by {ComputedModel::DepGraph::Sorted#plan}.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
class Plan
|
10
|
+
# @return [Array<ComputedModel::Plan::Node>] fields in load order
|
11
|
+
attr_reader :load_order
|
12
|
+
# @return [Set<Symbol>] toplevel dependencies
|
13
|
+
attr_reader :toplevel
|
14
|
+
|
15
|
+
# @param load_order [Array<ComputedModel::Plan::Node>] fields in load order
|
16
|
+
# @param toplevel [Set<Symbol>] toplevel dependencies
|
17
|
+
def initialize(load_order, toplevel)
|
18
|
+
@load_order = load_order.freeze
|
19
|
+
@nodes = load_order.map { |node| [node.name, node] }.to_h
|
20
|
+
@toplevel = toplevel
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param name [Symbol]
|
24
|
+
# @return [ComputedModel::Plan::Node, nil]
|
25
|
+
def [](name)
|
26
|
+
@nodes[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
# A set of information necessary to invoke the loader or the computed def.
|
30
|
+
class Node
|
31
|
+
# @return [Symbol] field name
|
32
|
+
attr_reader :name
|
33
|
+
# @return [Set<Symbol>] set of dependency names
|
34
|
+
attr_reader :deps
|
35
|
+
# @return [ComputedModel::NormalizableArray] subfield selectors, payloads sent to the dependency
|
36
|
+
attr_reader :subfields
|
37
|
+
|
38
|
+
# @param name [Symbol] field name
|
39
|
+
# @param deps [Set<Symbol>] set of dependency names
|
40
|
+
# @param subfields [ComputedModel::NormalizableArray] subfield selectors, payloads sent to the dependency
|
41
|
+
def initialize(name, deps, subfields)
|
42
|
+
@name = name
|
43
|
+
@deps = deps
|
44
|
+
@subfields = subfields
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|