wolflow 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|