wolflow 0.0.1
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +239 -0
- data/README.md +393 -0
- data/lib/wolflow/cycle.rb +67 -0
- data/lib/wolflow/dsl.rb +93 -0
- data/lib/wolflow/errors.rb +13 -0
- data/lib/wolflow/exclusive_choice.rb +106 -0
- data/lib/wolflow/extensions.rb +20 -0
- data/lib/wolflow/multi_choice.rb +127 -0
- data/lib/wolflow/multi_merge.rb +20 -0
- data/lib/wolflow/operators/attribute.rb +22 -0
- data/lib/wolflow/operators/base.rb +67 -0
- data/lib/wolflow/operators/literal.rb +22 -0
- data/lib/wolflow/operators/operation.rb +42 -0
- data/lib/wolflow/recursion.rb +21 -0
- data/lib/wolflow/simple.rb +134 -0
- data/lib/wolflow/simple_merge.rb +19 -0
- data/lib/wolflow/start.rb +9 -0
- data/lib/wolflow/structured_loop.rb +106 -0
- data/lib/wolflow/structured_synchronized_merge.rb +45 -0
- data/lib/wolflow/synchronization.rb +17 -0
- data/lib/wolflow/task.rb +195 -0
- data/lib/wolflow/task_spec.rb +114 -0
- data/lib/wolflow/version.rb +5 -0
- data/lib/wolflow/workflow.rb +82 -0
- data/lib/wolflow/workflow_spec.rb +82 -0
- data/lib/wolflow.rb +25 -0
- data/sig/cycle.rbs +7 -0
- data/sig/dsl.rbs +36 -0
- data/sig/exclusive_choice.rbs +12 -0
- data/sig/multi_choice.rbs +23 -0
- data/sig/multi_merge.rbs +6 -0
- data/sig/operators/attribute.rbs +11 -0
- data/sig/operators/base.rbs +21 -0
- data/sig/operators/literal.rbs +15 -0
- data/sig/operators/operation.rbs +10 -0
- data/sig/operators.rbs +7 -0
- data/sig/recursion.rbs +4 -0
- data/sig/simple.rbs +22 -0
- data/sig/simple_merge.rbs +6 -0
- data/sig/start.rbs +4 -0
- data/sig/structured_loop.rbs +8 -0
- data/sig/structured_synchronized_merge.rbs +6 -0
- data/sig/synchronization.rbs +6 -0
- data/sig/task.rbs +66 -0
- data/sig/task_spec.rbs +52 -0
- data/sig/wolflow.rbs +18 -0
- data/sig/workflow.rbs +33 -0
- data/sig/workflow_spec.rbs +20 -0
- metadata +115 -0
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class Simple < TaskSpec
|
5
|
+
attr_reader :next_tasks
|
6
|
+
|
7
|
+
def initialize(next_tasks: [], **kwargs)
|
8
|
+
super(**kwargs)
|
9
|
+
@next_tasks = next_tasks
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_complete(task)
|
13
|
+
task.mark_as_complete!
|
14
|
+
|
15
|
+
predict(task, @next_tasks)
|
16
|
+
end
|
17
|
+
|
18
|
+
def connect(*task_specs)
|
19
|
+
task_specs.each do |task_spec|
|
20
|
+
connect_one(task_spec)
|
21
|
+
end
|
22
|
+
|
23
|
+
if block_given?
|
24
|
+
yield(*task_specs)
|
25
|
+
return self
|
26
|
+
end
|
27
|
+
|
28
|
+
return task_specs.first if task_specs.size <= 1
|
29
|
+
|
30
|
+
task_specs
|
31
|
+
end
|
32
|
+
|
33
|
+
def choose(*else_task_specs, **args)
|
34
|
+
choice = ExclusiveChoice.new(workflow_spec: @workflow_spec, else_tasks: else_task_specs, **args)
|
35
|
+
next_tasks << choice
|
36
|
+
choice.prev_tasks << self
|
37
|
+
|
38
|
+
if block_given?
|
39
|
+
yield(choice, *else_task_specs)
|
40
|
+
return self
|
41
|
+
end
|
42
|
+
|
43
|
+
[choice, *else_task_specs]
|
44
|
+
end
|
45
|
+
|
46
|
+
def precedes?(task_spec)
|
47
|
+
@next_tasks.include?(task_spec)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_hash
|
51
|
+
{
|
52
|
+
id: @id,
|
53
|
+
name: @name,
|
54
|
+
next_tasks: @next_tasks.map(&:id)
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_hash_tree
|
59
|
+
hash_tree = @next_tasks.flat_map(&:to_hash_tree)
|
60
|
+
hash_tree.unshift(to_hash)
|
61
|
+
hash_tree
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"#<#{self.class}:#{hash} " \
|
66
|
+
"@id=#{@id} " \
|
67
|
+
"@next_tasks=#{@next_tasks.map(&:id)}>"
|
68
|
+
end
|
69
|
+
|
70
|
+
def workflow_spec=(workflow_spec)
|
71
|
+
return if workflow_spec == @workflow_spec
|
72
|
+
|
73
|
+
super
|
74
|
+
|
75
|
+
@next_tasks.each do |task_spec|
|
76
|
+
task_spec.workflow_spec = workflow_spec
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# private-ish
|
81
|
+
def connects_with(tasks)
|
82
|
+
return unless @connects_to
|
83
|
+
|
84
|
+
@connects_to.each do |id|
|
85
|
+
connect(tasks.fetch(id))
|
86
|
+
end
|
87
|
+
|
88
|
+
@connects_to = nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_next_tasks(task, i: 0, next_tasks: @next_tasks)
|
92
|
+
children = next_tasks.map do |next_task|
|
93
|
+
id = next_task.id
|
94
|
+
id = "#{id}_#{i}" unless i.zero?
|
95
|
+
task.class.new(
|
96
|
+
id: id,
|
97
|
+
task_spec: next_task,
|
98
|
+
workflow: task.workflow,
|
99
|
+
root: task.root
|
100
|
+
)
|
101
|
+
end
|
102
|
+
task.connect(*children)
|
103
|
+
children.each do |child|
|
104
|
+
child.task_spec.build_next_tasks(child, i: i)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def connect_one(task_spec)
|
111
|
+
if @workflow_spec
|
112
|
+
task_spec.workflow_spec = @workflow_spec
|
113
|
+
elsif task_spec.workflow_spec
|
114
|
+
raise TaskSpecError, "#{task_spec} already defined in a workflow spec"
|
115
|
+
end
|
116
|
+
|
117
|
+
task_spec.prev_tasks << self
|
118
|
+
@next_tasks << task_spec
|
119
|
+
end
|
120
|
+
|
121
|
+
class << self
|
122
|
+
def from_hash(hash)
|
123
|
+
case hash
|
124
|
+
in { next_tasks: [*, String, *] => next_tasks, **args }
|
125
|
+
spec = super(**args)
|
126
|
+
spec.connects_to = next_tasks
|
127
|
+
spec
|
128
|
+
else
|
129
|
+
super
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class SimpleMerge < Simple
|
5
|
+
def join(*task_specs)
|
6
|
+
task_specs.each { |spec| spec.connect(self) }
|
7
|
+
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_complete(task)
|
12
|
+
return unless task.parents.one?(&:completed?)
|
13
|
+
|
14
|
+
task.parents.each { |parent| parent.disconnect(task) unless parent.completed? }
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class StructuredLoop < ExclusiveChoice
|
5
|
+
def connect(condition, *task_specs)
|
6
|
+
raise TaskSpecError, "can only link with one parent" unless task_specs.size == 1
|
7
|
+
|
8
|
+
# @type var task_spec: TaskSpec
|
9
|
+
task_spec = task_specs.first
|
10
|
+
|
11
|
+
raise TaskSpecError, "can only link with parent" unless child_of?(task_spec)
|
12
|
+
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_complete(task)
|
17
|
+
@condition_next_tasks.each do |cond, next_tasks|
|
18
|
+
next unless cond.call(task)
|
19
|
+
|
20
|
+
parent_task = task.children.find { |child_task| next_tasks.include?(child_task.task_spec) }
|
21
|
+
|
22
|
+
raise Error, "parent task not found for #{task}" unless parent_task
|
23
|
+
|
24
|
+
reset_task(parent_task)
|
25
|
+
|
26
|
+
return # rubocop:disable Lint/NonLocalExitFromIterator
|
27
|
+
end
|
28
|
+
task.mark_as_complete!
|
29
|
+
|
30
|
+
predict(task, @else_tasks)
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_hash
|
34
|
+
hs = super
|
35
|
+
hs[:reset_task] = hs.delete(:condition_next_tasks).first
|
36
|
+
hs[:else_tasks] = hs.delete(:else_tasks)
|
37
|
+
hs
|
38
|
+
end
|
39
|
+
|
40
|
+
def each_child(task, &blk)
|
41
|
+
super(task) do |child|
|
42
|
+
next unless task.task_spec.else_tasks.include?(child.task_spec)
|
43
|
+
|
44
|
+
blk.call(child)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# private-ish
|
49
|
+
def connects_with(tasks)
|
50
|
+
return unless @connects_to
|
51
|
+
|
52
|
+
cond, id = @connects_to.fetch(:reset_task)
|
53
|
+
cond = Operators.from_hash(cond)
|
54
|
+
reset_task = tasks[id]
|
55
|
+
reset_task.connects_with(tasks)
|
56
|
+
connect(cond, reset_task)
|
57
|
+
connect_else(*@connects_to.fetch(:else_tasks).map(&tasks.method(:[])))
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def connect_cond(condition, task_specs)
|
63
|
+
@condition_next_tasks << [condition, task_specs]
|
64
|
+
end
|
65
|
+
|
66
|
+
def predict(task, next_tasks); end
|
67
|
+
|
68
|
+
def reset_task(task)
|
69
|
+
task.reset!
|
70
|
+
|
71
|
+
task.task_spec.each_child(task) do |child_task|
|
72
|
+
next unless child_of?(child_task.task_spec)
|
73
|
+
|
74
|
+
next if self == child_task.task_spec
|
75
|
+
|
76
|
+
reset_task(child_task)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class << self
|
81
|
+
def from_hash(hash)
|
82
|
+
case hash
|
83
|
+
in {
|
84
|
+
reset_task: [{ type: String, **}, TaskSpec],
|
85
|
+
else_tasks: [*, TaskSpec, *] => else_tasks,
|
86
|
+
**args
|
87
|
+
}
|
88
|
+
super
|
89
|
+
in {
|
90
|
+
reset_task: [{ type: String, **}, String] => reset_task,
|
91
|
+
else_tasks: [*, String, *] => else_tasks,
|
92
|
+
**args
|
93
|
+
}
|
94
|
+
spec = super(**args)
|
95
|
+
spec.connects_to = {
|
96
|
+
reset_task: reset_task,
|
97
|
+
else_tasks: else_tasks
|
98
|
+
}
|
99
|
+
spec
|
100
|
+
else # rubocop:disable Lint/DuplicateBranch
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class StructuredSynchronizedMerge < Simple
|
5
|
+
def join(*task_specs)
|
6
|
+
multi_choices = task_specs.map do |task_spec|
|
7
|
+
task_spec.each_ancestor.find do |spec|
|
8
|
+
spec.is_a?(MultiChoice)
|
9
|
+
end or raise(TaskSpecError, "#{task_spec.id} is not from a multi-choice branch")
|
10
|
+
end
|
11
|
+
|
12
|
+
raise TaskSpecError, "not branches from the same multi-choice" unless multi_choices.uniq.size == 1
|
13
|
+
|
14
|
+
# @type var multi_choice: MultiChoice
|
15
|
+
multi_choice = multi_choices.first
|
16
|
+
|
17
|
+
unless multi_choice.next_tasks.size == multi_choices.size
|
18
|
+
raise TaskSpecError,
|
19
|
+
"not joining all branches from the common multi-choice"
|
20
|
+
end
|
21
|
+
|
22
|
+
task_specs.each { |spec| spec.connect(self) }
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_complete(task)
|
28
|
+
parents_with_multi_choice = task.parents.select(&:completed?).filter_map do |parent|
|
29
|
+
multi_choice = parent.each_parent.find { |t| t.task_spec.is_a?(MultiChoice) }
|
30
|
+
[parent, multi_choice] if multi_choice
|
31
|
+
end
|
32
|
+
|
33
|
+
multi_choices = parents_with_multi_choice.map(&:last).uniq
|
34
|
+
|
35
|
+
return unless multi_choices.size == 1
|
36
|
+
|
37
|
+
# @type var multi_choice: Task
|
38
|
+
multi_choice = multi_choices.first
|
39
|
+
|
40
|
+
return unless multi_choice.children.size == parents_with_multi_choice.size
|
41
|
+
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class Synchronization < Simple
|
5
|
+
def join(*task_specs)
|
6
|
+
task_specs.each { |spec| spec.connect(self) }
|
7
|
+
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_complete(task)
|
12
|
+
return unless task.parents.all?(&:completed?)
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/wolflow/task.rb
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class Task
|
5
|
+
attr_reader :id, :task_spec, :root, :workflow, :parents, :children, :data
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
id: object_id.to_s,
|
9
|
+
task_spec: nil,
|
10
|
+
workflow: nil,
|
11
|
+
completed: false,
|
12
|
+
root: nil,
|
13
|
+
parents: [],
|
14
|
+
children: [],
|
15
|
+
data: {}
|
16
|
+
)
|
17
|
+
@id = id
|
18
|
+
@task_spec = task_spec
|
19
|
+
@completed = completed
|
20
|
+
@workflow = workflow
|
21
|
+
@root = root
|
22
|
+
@parents = parents
|
23
|
+
@children = children
|
24
|
+
@data = data
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize_dup(other)
|
28
|
+
super
|
29
|
+
@parents = other.instance_variable_get(:@parents).dup
|
30
|
+
@children = other.instance_variable_get(:@children).dup
|
31
|
+
@data = other.instance_variable_get(:@data).dup
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize_clone(other, **kwargs)
|
35
|
+
super
|
36
|
+
@parents = other.instance_variable_get(:@parents).clone(**kwargs)
|
37
|
+
@children = other.instance_variable_get(:@children).clone(**kwargs)
|
38
|
+
@data = other.instance_variable_get(:@data).clone(**kwargs)
|
39
|
+
end
|
40
|
+
|
41
|
+
def completed?
|
42
|
+
@completed
|
43
|
+
end
|
44
|
+
|
45
|
+
def complete!
|
46
|
+
@task_spec.on_complete(self) if @task_spec
|
47
|
+
end
|
48
|
+
|
49
|
+
def mark_as_complete!
|
50
|
+
@completed = true
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset!
|
54
|
+
@completed = false
|
55
|
+
end
|
56
|
+
|
57
|
+
def connect(*tasks)
|
58
|
+
tasks.each do |task|
|
59
|
+
connect_one(task)
|
60
|
+
end
|
61
|
+
|
62
|
+
if block_given?
|
63
|
+
yield(*tasks)
|
64
|
+
return self
|
65
|
+
end
|
66
|
+
|
67
|
+
return tasks.first if tasks.size <= 1
|
68
|
+
|
69
|
+
tasks
|
70
|
+
end
|
71
|
+
|
72
|
+
def disconnect(task)
|
73
|
+
unless @children.include?(task) && task.parents.include?(self)
|
74
|
+
raise TaskError, "#{self} and #{task} are not connected"
|
75
|
+
end
|
76
|
+
|
77
|
+
task.parents.delete(self)
|
78
|
+
@children.delete(task)
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_task_tree
|
82
|
+
raise TaskError, "there's already a task tree" unless @children.empty?
|
83
|
+
|
84
|
+
@task_spec.build_next_tasks(self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def copy_task_tree(i)
|
88
|
+
@task_spec.build_next_tasks(self, i: i)
|
89
|
+
end
|
90
|
+
|
91
|
+
def each(visited = [], &blk)
|
92
|
+
return enum_for(__method__, visited) unless blk
|
93
|
+
|
94
|
+
blk.call(self) unless @task_spec.id.start_with?("__")
|
95
|
+
|
96
|
+
visited << self
|
97
|
+
|
98
|
+
@task_spec.each_child(self) do |child|
|
99
|
+
# do not allow multiple visits on the same node
|
100
|
+
next if visited.include?(child)
|
101
|
+
|
102
|
+
if child.task_spec.is_a?(Synchronization) && !child.parents.all? { |parent| visited.include?(parent) }
|
103
|
+
# only jump in after all parents are consumed
|
104
|
+
next
|
105
|
+
end
|
106
|
+
|
107
|
+
child.each(visited, &blk)
|
108
|
+
end
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
def each_parent(visited = [self], &blk)
|
113
|
+
return enum_for(__method__, visited) unless blk
|
114
|
+
|
115
|
+
@task_spec.each_parent(self) do |parent|
|
116
|
+
# do not allow multiple visits on the same node
|
117
|
+
next if visited.include?(parent)
|
118
|
+
|
119
|
+
visited << parent
|
120
|
+
|
121
|
+
blk.call(parent)
|
122
|
+
|
123
|
+
parent.each_parent(visited, &blk)
|
124
|
+
end
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
def inspect
|
129
|
+
"#<#{self.class}:#{hash} " \
|
130
|
+
"@id=#{@id} " \
|
131
|
+
"@parents=#{@parents.map(&:id)} " \
|
132
|
+
"@children=#{@children.map(&:id)} " \
|
133
|
+
"@completed=#{@completed}>"
|
134
|
+
end
|
135
|
+
|
136
|
+
# private-ish
|
137
|
+
def connects_with(tasks)
|
138
|
+
@parents = @parents.map do |parent|
|
139
|
+
parent.is_a?(Task) ? parent : tasks.fetch(parent)
|
140
|
+
end
|
141
|
+
@children = @children.map do |child|
|
142
|
+
child.is_a?(Task) ? child : tasks.fetch(child)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def to_hash
|
147
|
+
{
|
148
|
+
id: @id,
|
149
|
+
parents: @parents.map(&:id),
|
150
|
+
children: @children.map(&:id),
|
151
|
+
completed: @completed,
|
152
|
+
data: (@data.to_hash unless @data.empty?),
|
153
|
+
task_spec_id: (@task_spec.id if @task_spec)
|
154
|
+
}.compact
|
155
|
+
end
|
156
|
+
|
157
|
+
class << self
|
158
|
+
def from_hash(hash)
|
159
|
+
if hash.key?(:task_spec) && !hash[:task_spec].is_a?(TaskSpec)
|
160
|
+
task_spec = TaskSpec.spec_types[hash[:task_spec]]
|
161
|
+
|
162
|
+
raise Error, "no task spec registered for #{hash[:task_spec]}" unless task_spec
|
163
|
+
|
164
|
+
hash[:task_spec] = task_spec.new
|
165
|
+
end
|
166
|
+
|
167
|
+
new(**hash)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
def root=(rt)
|
174
|
+
@root = rt
|
175
|
+
@children.each do |task|
|
176
|
+
task.root = rt
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def connect_one(task)
|
183
|
+
raise TaskError, "#{task} is from a different workflow" if @workflow && @workflow != task.workflow
|
184
|
+
|
185
|
+
unless @task_spec.nil? || @task_spec.precedes?(task.task_spec)
|
186
|
+
raise TaskError,
|
187
|
+
"#{task} can't connect from task spec"
|
188
|
+
end
|
189
|
+
|
190
|
+
task.parents << self
|
191
|
+
task.root = @root || self
|
192
|
+
@children << task
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class TaskSpec
|
5
|
+
using StringExtensions unless ::String.method_defined?(:underscore)
|
6
|
+
|
7
|
+
attr_reader :id, :name, :workflow_spec, :prev_tasks
|
8
|
+
|
9
|
+
attr_writer :connects_to
|
10
|
+
|
11
|
+
def initialize(id: object_id.to_s, name: self.class.spec_type, workflow_spec: nil, prev_tasks: [])
|
12
|
+
@id = id
|
13
|
+
@name = name
|
14
|
+
@workflow_spec = workflow_spec
|
15
|
+
@prev_tasks = prev_tasks
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_child(task, &)
|
19
|
+
task.children.each(&)
|
20
|
+
end
|
21
|
+
|
22
|
+
def each_parent(task, &)
|
23
|
+
task.parents.each(&)
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_successor(visited = [self], &blk)
|
27
|
+
return enum_for(__method__, visited) unless blk
|
28
|
+
|
29
|
+
next_tasks.each do |spec|
|
30
|
+
next if visited.include?(spec)
|
31
|
+
|
32
|
+
blk.call(spec)
|
33
|
+
|
34
|
+
visited << spec
|
35
|
+
|
36
|
+
spec.each_successor(visited, &blk)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def each_ancestor(visited = [self], &blk)
|
41
|
+
return enum_for(__method__, visited) unless blk
|
42
|
+
|
43
|
+
@prev_tasks.each do |spec|
|
44
|
+
next if visited.include?(spec)
|
45
|
+
|
46
|
+
blk.call(spec)
|
47
|
+
|
48
|
+
visited << spec
|
49
|
+
|
50
|
+
spec.each_ancestor(visited, &blk)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def workflow_spec=(workflow_spec)
|
55
|
+
if @workflow_spec
|
56
|
+
return if workflow_spec == @workflow_spec
|
57
|
+
|
58
|
+
raise TaskSpecError, "#{self} already defined in a workflow spec"
|
59
|
+
end
|
60
|
+
|
61
|
+
@workflow_spec = workflow_spec
|
62
|
+
end
|
63
|
+
|
64
|
+
def child_of?(spec)
|
65
|
+
spec.next_tasks.include?(self) || spec.next_tasks.one? { |child_spec| child_of?(child_spec) }
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def predict(task, next_tasks)
|
71
|
+
return unless task.children.empty?
|
72
|
+
|
73
|
+
next_tasks.each do |task_spec|
|
74
|
+
task.connect(Task.new(task_spec: task_spec, workflow: task.workflow))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
@spec_types = {}
|
79
|
+
|
80
|
+
class << self
|
81
|
+
attr_reader :spec_types, :spec_type
|
82
|
+
|
83
|
+
def inherited(subclass)
|
84
|
+
super
|
85
|
+
|
86
|
+
return superclass.inherited(subclass) unless self == TaskSpec
|
87
|
+
|
88
|
+
name = subclass.name
|
89
|
+
|
90
|
+
return unless name # anon class
|
91
|
+
|
92
|
+
tag = name.demodulize.underscore
|
93
|
+
|
94
|
+
raise Error, "spec type for #{tag} already exists" if @spec_types.key?(tag)
|
95
|
+
|
96
|
+
subclass.instance_variable_set(:@spec_type, tag)
|
97
|
+
subclass.instance_variable_get(:@spec_type).freeze
|
98
|
+
|
99
|
+
@spec_types[tag] = subclass
|
100
|
+
end
|
101
|
+
|
102
|
+
def from_hash(hash)
|
103
|
+
case hash
|
104
|
+
in { id: String => id, name: String => name, ** }
|
105
|
+
TaskSpec.spec_types[name].new(**hash)
|
106
|
+
in { id: String => id }
|
107
|
+
new(id: id)
|
108
|
+
else
|
109
|
+
raise TaskSpecError, "can't deserialize #{hash} to a TaskSpec"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|