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
data/lib/computed_model.rb
CHANGED
@@ -1,166 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "computed_model/version"
|
4
|
-
require
|
5
|
-
|
4
|
+
require "computed_model/plan"
|
5
|
+
require "computed_model/dep_graph"
|
6
|
+
require "computed_model/model"
|
7
|
+
|
8
|
+
# ComputedModel is a universal batch loader which comes with a dependency-resolution algorithm.
|
9
|
+
#
|
10
|
+
# - Thanks to the dependency resolution, it allows you to the following trifecta at once, without breaking abstraction.
|
11
|
+
# - Process information gathered from datasources (such as ActiveRecord) and return the derived one.
|
12
|
+
# - Prevent N+1 problem via batch loading.
|
13
|
+
# - Load only necessary data.
|
14
|
+
# - Can load data from multiple datasources.
|
15
|
+
# - Designed to be universal and datasource-independent.
|
16
|
+
# For example, you can gather data from both HTTP and ActiveRecord and return the derived one.
|
17
|
+
#
|
18
|
+
# See {ComputedModel::Model} for basic usage.
|
6
19
|
module ComputedModel
|
20
|
+
# An error raised when you tried to read from a loaded/computed attribute,
|
21
|
+
# but that attribute isn't loaded by the batch loader.
|
7
22
|
class NotLoaded < StandardError; end
|
8
23
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
public meth_name
|
37
|
-
elsif protected_method_defined?(meth_name_orig)
|
38
|
-
protected meth_name
|
39
|
-
elsif private_method_defined?(meth_name_orig)
|
40
|
-
private meth_name
|
41
|
-
end
|
42
|
-
|
43
|
-
meth_name
|
44
|
-
end
|
45
|
-
|
46
|
-
# @param methods [Array<Symbol>]
|
47
|
-
# @param to [Symbol]
|
48
|
-
# @param allow_nil [nil, Boolean]
|
49
|
-
# @param prefix [nil, Symbol]
|
50
|
-
# @param include_subdeps [nil, Boolean]
|
51
|
-
# @return [void]
|
52
|
-
def delegate_dependency(*methods, to:, allow_nil: nil, prefix: nil, include_subdeps: nil)
|
53
|
-
method_prefix = prefix ? "#{prefix_}" : ""
|
54
|
-
methods.each do |meth_name|
|
55
|
-
pmeth_name = :"#{method_prefix}#{meth_name}"
|
56
|
-
if include_subdeps
|
57
|
-
dependency to=>meth_name
|
58
|
-
else
|
59
|
-
dependency to
|
60
|
-
end
|
61
|
-
if allow_nil
|
62
|
-
define_method(pmeth_name) do
|
63
|
-
send(to)&.public_send(meth_name)
|
64
|
-
end
|
65
|
-
else
|
66
|
-
define_method(pmeth_name) do
|
67
|
-
send(to).public_send(meth_name)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
computed pmeth_name
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
# @param meth_name [Symbol]
|
75
|
-
# @yieldparam objects [Array]
|
76
|
-
# @yieldparam options [Hash]
|
77
|
-
# @yieldreturn [void]
|
78
|
-
def define_loader(meth_name, &block)
|
79
|
-
raise ArgumentError, "No block given" unless block
|
80
|
-
|
81
|
-
var_name = :"@#{meth_name}"
|
82
|
-
|
83
|
-
@__computed_model_loaders[meth_name] = block
|
84
|
-
|
85
|
-
define_method(meth_name) do
|
86
|
-
raise NotLoaded, "the field #{meth_name} is not loaded" unless instance_variable_defined?(var_name)
|
87
|
-
instance_variable_get(var_name)
|
88
|
-
end
|
89
|
-
attr_writer meth_name
|
90
|
-
end
|
91
|
-
|
92
|
-
# @param objs [Array]
|
93
|
-
# @param deps [Array]
|
94
|
-
def bulk_load_and_compute(objs, deps, **options)
|
95
|
-
plan = computing_plan(deps)
|
96
|
-
plan.load_order.each do |dep_name|
|
97
|
-
if @__computed_model_dependencies.key?(dep_name)
|
98
|
-
objs.each do |obj|
|
99
|
-
obj.send(:"compute_#{dep_name}")
|
100
|
-
end
|
101
|
-
elsif @__computed_model_loaders.key?(dep_name)
|
102
|
-
@__computed_model_loaders[dep_name].call(objs, plan.subdeps_hash[dep_name], **options)
|
103
|
-
else
|
104
|
-
raise "No dependency info for #{self}##{dep_name}"
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# @param deps [Array]
|
110
|
-
# @return [Plan]
|
111
|
-
def computing_plan(deps)
|
112
|
-
normalized = ComputedModel.normalize_dependencies(deps)
|
113
|
-
load_order = []
|
114
|
-
subdeps_hash = {}
|
115
|
-
visiting = Set[]
|
116
|
-
visited = Set[]
|
117
|
-
normalized.each do |dep_name, dep_subdeps|
|
118
|
-
computing_plan_dfs(dep_name, dep_subdeps, load_order, subdeps_hash, visiting, visited)
|
119
|
-
end
|
120
|
-
|
121
|
-
Plan.new(load_order, subdeps_hash)
|
122
|
-
end
|
123
|
-
|
124
|
-
# @param meth_name [Symbol]
|
125
|
-
# @param meth_subdeps [Array]
|
126
|
-
# @param load_order [Array<Symbol>]
|
127
|
-
# @param subdeps_hash [Hash{Symbol=>Array}]
|
128
|
-
# @param visiting [Set<Symbol>]
|
129
|
-
# @param visited [Set<Symbol>]
|
130
|
-
private def computing_plan_dfs(meth_name, meth_subdeps, load_order, subdeps_hash, visiting, visited)
|
131
|
-
(subdeps_hash[meth_name] ||= []).push(*meth_subdeps)
|
132
|
-
return if visited.include?(meth_name)
|
133
|
-
raise "Cyclic dependency for #{self}##{meth_name}" if visiting.include?(meth_name)
|
134
|
-
visiting.add(meth_name)
|
135
|
-
|
136
|
-
if @__computed_model_dependencies.key?(meth_name)
|
137
|
-
@__computed_model_dependencies[meth_name].each do |dep_name, dep_subdeps|
|
138
|
-
computing_plan_dfs(dep_name, dep_subdeps, load_order, subdeps_hash, visiting, visited)
|
139
|
-
end
|
140
|
-
elsif @__computed_model_loaders.key?(meth_name)
|
141
|
-
else
|
142
|
-
raise "No dependency info for #{self}##{meth_name}"
|
143
|
-
end
|
144
|
-
|
145
|
-
load_order << meth_name
|
146
|
-
visiting.delete(meth_name)
|
147
|
-
visited.add(meth_name)
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
# @param deps [Array<Symbol, Hash>]
|
152
|
-
# @return [Hash{Symbol=>Array}]
|
24
|
+
# An error raised when you tried to read from a loaded/computed attribute,
|
25
|
+
# but that attribute isn't listed in the dependencies list.
|
26
|
+
class ForbiddenDependency < StandardError; end
|
27
|
+
|
28
|
+
# An error raised when the dependency graph contains a cycle.
|
29
|
+
class CyclicDependency < StandardError; end
|
30
|
+
|
31
|
+
# Normalizes dependency list as a hash.
|
32
|
+
#
|
33
|
+
# Normally you don't need to call it directly.
|
34
|
+
# {ComputedModel::Model::ClassMethods#dependency}, {ComputedModel::Model::ClassMethods#bulk_load_and_compute}, and
|
35
|
+
# {ComputedModel::NormalizableArray#normalized} will internally use this function.
|
36
|
+
#
|
37
|
+
# @param deps [Array<(Symbol, Hash)>, Hash, Symbol] dependency list
|
38
|
+
# @return [Hash{Symbol=>Array}] normalized dependency hash
|
39
|
+
# @raise [RuntimeError] if the dependency list contains values other than Symbol or Hash
|
40
|
+
# @example
|
41
|
+
# ComputedModel.normalize_dependencies([:foo, :bar])
|
42
|
+
# # => { foo: [true], bar: [true] }
|
43
|
+
#
|
44
|
+
# @example
|
45
|
+
# ComputedModel.normalize_dependencies([:foo, bar: :baz])
|
46
|
+
# # => { foo: [true], bar: [true, :baz] }
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# ComputedModel.normalize_dependencies(foo: -> (subfields) { true })
|
50
|
+
# # => { foo: [#<Proc:...>] }
|
153
51
|
def self.normalize_dependencies(deps)
|
154
52
|
normalized = {}
|
155
|
-
deps
|
53
|
+
deps = [deps] if deps.is_a?(Hash)
|
54
|
+
Array(deps).each do |elem|
|
156
55
|
case elem
|
157
56
|
when Symbol
|
158
|
-
normalized[elem] ||= []
|
57
|
+
normalized[elem] ||= [true]
|
159
58
|
when Hash
|
160
59
|
elem.each do |k, v|
|
161
60
|
v = [v] if v.is_a?(Hash)
|
162
61
|
normalized[k] ||= []
|
163
62
|
normalized[k].push(*Array(v))
|
63
|
+
normalized[k].push(true) if v == []
|
164
64
|
end
|
165
65
|
else; raise "Invalid dependency: #{elem.inspect}"
|
166
66
|
end
|
@@ -168,10 +68,35 @@ module ComputedModel
|
|
168
68
|
normalized
|
169
69
|
end
|
170
70
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
71
|
+
# Removes `nil`, `true` and `false` from the given array.
|
72
|
+
#
|
73
|
+
# Normally you don't need to call it directly.
|
74
|
+
# {ComputedModel::Model::ClassMethods#define_loader},
|
75
|
+
# {ComputedModel::Model::ClassMethods#define_primary_loader}, and
|
76
|
+
# {ComputedModel::NormalizableArray#normalized} will internally use this function.
|
77
|
+
#
|
78
|
+
# @param subfields [Array] subfield selector list
|
79
|
+
# @return [Array] the filtered one
|
80
|
+
# @example
|
81
|
+
# ComputedModel.filter_subfields([false, {}, true, nil, { foo: :bar }])
|
82
|
+
# # => [{}, { foo: :bar }]
|
83
|
+
def self.filter_subfields(subfields)
|
84
|
+
subfields.select { |x| x && x != true }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Convenience class to easily access normalized version of dependencies.
|
88
|
+
#
|
89
|
+
# You don't need to directly use it.
|
90
|
+
#
|
91
|
+
# - {ComputedModel::Model#current_subfields} returns NormalizableArray.
|
92
|
+
# - Procs passed to {ComputedModel::Model::ClassMethods#dependency} will receive NormalizeArray.
|
93
|
+
class NormalizableArray < Array
|
94
|
+
# Returns the normalized hash of the dependencies.
|
95
|
+
# @return [Hash{Symbol=>Array}] the normalized hash of the dependencies
|
96
|
+
# @raise [RuntimeError] if the list isn't valid as a dependency list.
|
97
|
+
# See {ComputedModel.normalize_dependencies} for details.
|
98
|
+
def normalized
|
99
|
+
@normalized ||= ComputedModel.normalize_dependencies(ComputedModel.filter_subfields(self))
|
100
|
+
end
|
176
101
|
end
|
177
102
|
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module ComputedModel
|
6
|
+
# A dependency graph representation used within ComputedModel::Model.
|
7
|
+
# Usually you don't need to use this class directly.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# graph = ComputedModel::DepGraph.new
|
13
|
+
# graph << ComputedModel::DepGraph::Node.new(:computed, :foo, { bar: [] })
|
14
|
+
# graph << ComputedModel::DepGraph::Node.new(:loaded, :bar, {})
|
15
|
+
# plan = graph.tsort.plan([:foo])
|
16
|
+
class DepGraph
|
17
|
+
def initialize
|
18
|
+
@nodes = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the node with the specified name.
|
22
|
+
#
|
23
|
+
# @param name [Symbol] the name of the node
|
24
|
+
# @return [Node, nil]
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# graph = ComputedModel::DepGraph.new
|
28
|
+
# graph[:foo]
|
29
|
+
def [](name)
|
30
|
+
@nodes[name]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Adds the new node.
|
34
|
+
#
|
35
|
+
# @param node [Node]
|
36
|
+
# @return [void]
|
37
|
+
# @raise [ArgumentError] when the node already exists
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# graph = ComputedModel::DepGraph.new
|
41
|
+
# graph << ComputedModel::DepGraph::Node.new(:computed, :foo, {})
|
42
|
+
def <<(node)
|
43
|
+
raise ArgumentError, "Field already declared: #{node.name}" if @nodes.key?(node.name)
|
44
|
+
|
45
|
+
@nodes[node.name] = node
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<ComputedModel::DepGraph::Node>]
|
49
|
+
def nodes
|
50
|
+
@nodes.values
|
51
|
+
end
|
52
|
+
|
53
|
+
# Preprocess the graph by topological sorting. This is a necessary step for loader planning.
|
54
|
+
#
|
55
|
+
# @return [ComputedModel::DepGraph::Sorted]
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# graph = ComputedModel::DepGraph.new
|
59
|
+
# graph << ComputedModel::DepGraph::Node.new(:computed, :foo, { bar: [] })
|
60
|
+
# graph << ComputedModel::DepGraph::Node.new(:loaded, :bar, {})
|
61
|
+
# sorted = graph.tsort
|
62
|
+
def tsort
|
63
|
+
load_order = []
|
64
|
+
visiting = Set[]
|
65
|
+
visited = Set[]
|
66
|
+
|
67
|
+
@nodes.each_value do |node|
|
68
|
+
next unless node.type == :primary
|
69
|
+
|
70
|
+
load_order << node.name
|
71
|
+
visiting.add node.name
|
72
|
+
visited.add node.name
|
73
|
+
end
|
74
|
+
|
75
|
+
raise ArgumentError, 'No primary loader defined' if load_order.empty?
|
76
|
+
raise "Multiple primary fields: #{load_order.inspect}" if load_order.size > 1
|
77
|
+
|
78
|
+
@nodes.each_value do |node|
|
79
|
+
tsort_dfs(node.name, load_order, visiting, visited)
|
80
|
+
end
|
81
|
+
|
82
|
+
nodes_in_order = load_order.reverse.map { |name| @nodes[name] }
|
83
|
+
ComputedModel::DepGraph::Sorted.new(self, nodes_in_order)
|
84
|
+
end
|
85
|
+
|
86
|
+
private def tsort_dfs(name, load_order, visiting, visited)
|
87
|
+
return if visited.include?(name)
|
88
|
+
raise ComputedModel::CyclicDependency, "Cyclic dependency for ##{name}" if visiting.include?(name)
|
89
|
+
raise "No dependency info for ##{name}" unless @nodes.key?(name)
|
90
|
+
|
91
|
+
visiting.add(name)
|
92
|
+
|
93
|
+
@nodes[name].edges.each_value do |edge|
|
94
|
+
tsort_dfs(edge.name, load_order, visiting, visited)
|
95
|
+
end
|
96
|
+
|
97
|
+
load_order << name
|
98
|
+
visiting.delete(name)
|
99
|
+
visited.add(name)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param graphs [Array<ComputedModel::DepGraph>]
|
103
|
+
# @return [ComputedModel::DepGraph]
|
104
|
+
def self.merge(graphs)
|
105
|
+
new_graph = ComputedModel::DepGraph.new
|
106
|
+
nodes_by_name = graphs.flat_map(&:nodes).group_by(&:name)
|
107
|
+
nodes_by_name.each do |name, nodes|
|
108
|
+
type = nodes.first.type
|
109
|
+
raise ArgumentError, "Field #{name} has multiple different types" unless nodes.all? { |node| node.type == type }
|
110
|
+
|
111
|
+
new_graph << ComputedModel::DepGraph::Node.new(type, name, nodes.map { |n| n.edges.transform_values { |e| e.spec } })
|
112
|
+
end
|
113
|
+
new_graph
|
114
|
+
end
|
115
|
+
|
116
|
+
# A node in the dependency graph. That is, a field in a computed model.
|
117
|
+
#
|
118
|
+
# @example computed node with plain dependencies
|
119
|
+
# Node.new(:computed, :field1, { field2: [], field3: [] })
|
120
|
+
# @example computed node with subfield selectors
|
121
|
+
# Node.new(:computed, :field1, { field2: [:foo, bar: []], field3: [] })
|
122
|
+
# @example loaded and primary dependencies
|
123
|
+
# Node.new(:loaded, :field1, {})
|
124
|
+
# Node.new(:primary, :field1, {})
|
125
|
+
class Node
|
126
|
+
# @return [Symbol] the type of the node. One of :computed, :loaded and :primary.
|
127
|
+
attr_reader :type
|
128
|
+
# @return [Symbol] the name of the node.
|
129
|
+
attr_reader :name
|
130
|
+
# @return [Hash{Symbol => Edge}] edges indexed by its name.
|
131
|
+
attr_reader :edges
|
132
|
+
|
133
|
+
ALLOWED_TYPES = %i[computed loaded primary].freeze
|
134
|
+
private_constant :ALLOWED_TYPES
|
135
|
+
|
136
|
+
# @param type [Symbol] the type of the node. One of :computed, :loaded and :primary.
|
137
|
+
# @param name [Symbol] the name of the node.
|
138
|
+
# @param edges [Array<(Symbol, Hash)>, Hash, Symbol] list of edges.
|
139
|
+
def initialize(type, name, edges)
|
140
|
+
raise ArgumentError, "invalid type: #{type.inspect}" unless ALLOWED_TYPES.include?(type)
|
141
|
+
|
142
|
+
edges = ComputedModel.normalize_dependencies(edges)
|
143
|
+
raise ArgumentError, "primary field cannot have dependency: #{name}" if type == :primary && edges.size > 0
|
144
|
+
|
145
|
+
@type = type
|
146
|
+
@name = name
|
147
|
+
@edges = edges.map { |k, v| [k, Edge.new(k, v)] }.to_h.freeze
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# An edge in the dependency graph. That is, a dependency declaration in a computed model.
|
152
|
+
class Edge
|
153
|
+
# @return [Symbol] the name of the dependency (not the dependent)
|
154
|
+
attr_reader :name
|
155
|
+
# @return [Array] an auxiliary data called subfield selectors
|
156
|
+
attr_reader :spec
|
157
|
+
|
158
|
+
# @param name [Symbol] the name of the dependency (not the dependent)
|
159
|
+
# @param spec [Array] an auxiliary data called subfield selectors
|
160
|
+
def initialize(name, spec)
|
161
|
+
@name = name
|
162
|
+
@spec = Array(spec)
|
163
|
+
end
|
164
|
+
|
165
|
+
# @param subfields [Array] incoming list of subfield selectors
|
166
|
+
# @return [Array, nil]
|
167
|
+
def evaluate(subfields)
|
168
|
+
return @spec if @spec.all? { |specelem| !specelem.respond_to?(:call) }
|
169
|
+
|
170
|
+
evaluated = []
|
171
|
+
@spec.each do |specelem|
|
172
|
+
if specelem.respond_to?(:call)
|
173
|
+
ret = specelem.call(subfields)
|
174
|
+
if ret.is_a?(Array)
|
175
|
+
evaluated.push(*ret)
|
176
|
+
else
|
177
|
+
evaluated << ret
|
178
|
+
end
|
179
|
+
else
|
180
|
+
evaluated << specelem
|
181
|
+
end
|
182
|
+
end
|
183
|
+
evaluated
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# A preprocessed graph with topologically sorted order.
|
188
|
+
#
|
189
|
+
# Generated by {ComputedModel::DepGraph#tsort}.
|
190
|
+
class Sorted
|
191
|
+
# @return [ComputedModel::DepGraph]
|
192
|
+
attr_reader :original
|
193
|
+
# @return [Array<ComputedModel::DepGraph::Node>]
|
194
|
+
attr_reader :nodes_in_order
|
195
|
+
|
196
|
+
# @param original [ComputedModel::DepGraph]
|
197
|
+
# @param nodes_in_order [Array<ComputedModel::DepGraph::Node>]
|
198
|
+
def initialize(original, nodes_in_order)
|
199
|
+
@original = original
|
200
|
+
@nodes_in_order = nodes_in_order
|
201
|
+
end
|
202
|
+
|
203
|
+
# Computes the plan for the given requirements.
|
204
|
+
#
|
205
|
+
# @param deps [Array] the list of required nodes. Each dependency can optionally include subfields hashes.
|
206
|
+
# @return [ComputedModel::Plan]
|
207
|
+
#
|
208
|
+
# @example Plain dependencies
|
209
|
+
# sorted.plan([:field1, :field2])
|
210
|
+
#
|
211
|
+
# @example Dependencies with subfields
|
212
|
+
# sorted.plan([:field1, field2: { optional_field: {} }])
|
213
|
+
def plan(deps)
|
214
|
+
normalized = ComputedModel.normalize_dependencies(deps)
|
215
|
+
subfields_hash = {}
|
216
|
+
uses = Set[]
|
217
|
+
plan_nodes = []
|
218
|
+
|
219
|
+
normalized.each do |name, subfields|
|
220
|
+
raise "No dependency info for ##{name}" unless @original[name]
|
221
|
+
|
222
|
+
uses.add(name)
|
223
|
+
(subfields_hash[name] ||= []).unshift(*subfields)
|
224
|
+
end
|
225
|
+
@nodes_in_order.each do |node|
|
226
|
+
uses.add(node.name) if node.type == :primary
|
227
|
+
next unless uses.include?(node.name)
|
228
|
+
|
229
|
+
node_subfields = ComputedModel::NormalizableArray.new(subfields_hash[node.name] || [])
|
230
|
+
deps = Set[]
|
231
|
+
node.edges.each_value do |edge|
|
232
|
+
specval = edge.evaluate(node_subfields)
|
233
|
+
if specval.any?
|
234
|
+
deps.add(edge.name)
|
235
|
+
uses.add(edge.name)
|
236
|
+
(subfields_hash[edge.name] ||= []).unshift(*specval)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
plan_nodes.push(ComputedModel::Plan::Node.new(node.name, deps, node_subfields))
|
240
|
+
end
|
241
|
+
ComputedModel::Plan.new(plan_nodes.reverse, normalized.keys.to_set)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|