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,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class Cycle < Simple
|
5
|
+
def connect(*specs)
|
6
|
+
raise TaskSpecError, "cycles can only connect to a single task spec" unless specs.size == 1
|
7
|
+
|
8
|
+
spec = specs.first
|
9
|
+
|
10
|
+
raise TaskSpecError, "cycles can only connect to previous task specs" unless child_of?(spec)
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_complete(task)
|
16
|
+
super
|
17
|
+
|
18
|
+
task.children.each do |child_task|
|
19
|
+
next if task == child_task
|
20
|
+
|
21
|
+
reset_task(child_task)
|
22
|
+
end
|
23
|
+
|
24
|
+
task.reset!
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_hash_tree
|
28
|
+
[to_hash]
|
29
|
+
end
|
30
|
+
|
31
|
+
def each_child(task); end
|
32
|
+
|
33
|
+
# def each_parent(task); end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def predict(task, next_tasks)
|
38
|
+
return unless task.children.empty?
|
39
|
+
|
40
|
+
workflow = task.workflow
|
41
|
+
|
42
|
+
return unless workflow
|
43
|
+
|
44
|
+
tasks = workflow.each.each_with_object([]) do |wtask, wtasks| # rubocop:disable Style/RedundantEach
|
45
|
+
next unless next_tasks.include?(wtask.task_spec)
|
46
|
+
|
47
|
+
wtasks << wtask
|
48
|
+
|
49
|
+
break if wtasks.size == next_tasks.size
|
50
|
+
end
|
51
|
+
|
52
|
+
task.connect(*tasks)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_task(task)
|
56
|
+
task.reset!
|
57
|
+
|
58
|
+
task.task_spec.each_child(task) do |child_task|
|
59
|
+
next unless child_of?(child_task.task_spec)
|
60
|
+
|
61
|
+
next if self == child_task.task_spec
|
62
|
+
|
63
|
+
reset_task(child_task)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/wolflow/dsl.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wolflow"
|
4
|
+
|
5
|
+
module Wolflow
|
6
|
+
module DSL
|
7
|
+
def self.load!
|
8
|
+
WorkflowSpec.prepend(WorkflowSpecDSL)
|
9
|
+
Simple.prepend(SimpleDSL)
|
10
|
+
MultiChoice.prepend(ChoiceDSL)
|
11
|
+
Wolflow.extend(WolflowDSL)
|
12
|
+
end
|
13
|
+
|
14
|
+
module WolflowDSL
|
15
|
+
def spec(id, &blk)
|
16
|
+
wf = WorkflowSpec.new(id: id)
|
17
|
+
|
18
|
+
blk.call(wf) if blk
|
19
|
+
|
20
|
+
wf
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module WorkflowSpecDSL
|
25
|
+
def connect(*ids, &)
|
26
|
+
return super unless ids.all?(String)
|
27
|
+
|
28
|
+
specs = ids.map { |id| Simple.new(id: id) }
|
29
|
+
|
30
|
+
super(*specs, &)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module SimpleDSL
|
35
|
+
def initialize(...)
|
36
|
+
@on_complete_callbacks = []
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_perform(&blk)
|
41
|
+
@on_complete_callbacks << blk
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def on_complete(task)
|
47
|
+
@on_complete_callbacks.each do |blk|
|
48
|
+
blk.call(task)
|
49
|
+
end
|
50
|
+
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def connect(*ids, &)
|
55
|
+
return super unless ids.all?(String)
|
56
|
+
|
57
|
+
specs = ids.map { |id| Simple.new(id: id, workflow_spec: @workflow_spec) }
|
58
|
+
|
59
|
+
super(*specs, &)
|
60
|
+
end
|
61
|
+
|
62
|
+
def choose(*ids, **, &)
|
63
|
+
return super unless ids.all?(String) && ids.size > 1
|
64
|
+
|
65
|
+
if_id, *else_ids = ids
|
66
|
+
|
67
|
+
else_specs = else_ids.map { |else_id| Simple.new(id: else_id, workflow_spec: @workflow_spec) }
|
68
|
+
|
69
|
+
super(*else_specs, id: if_id, &)
|
70
|
+
end
|
71
|
+
|
72
|
+
def join(id, *task_specs)
|
73
|
+
synch = Synchronization.new(id: id, workflow_spec: @workflow_spec)
|
74
|
+
|
75
|
+
synch.join(self, *task_specs)
|
76
|
+
|
77
|
+
synch
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
module ChoiceDSL
|
82
|
+
def connect(*ids, cond)
|
83
|
+
return super unless ids.all?(String)
|
84
|
+
|
85
|
+
specs = ids.map { |id| Simple.new(id: id, workflow_spec: @workflow_spec) }
|
86
|
+
|
87
|
+
super(cond, *specs)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
DSL.load!
|
93
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class ExclusiveChoice < MultiChoice
|
5
|
+
attr_reader :else_tasks
|
6
|
+
|
7
|
+
def initialize(else_tasks: [], **kwargs)
|
8
|
+
super(**kwargs)
|
9
|
+
@else_tasks = Array(else_tasks)
|
10
|
+
|
11
|
+
@else_tasks.each do |task_spec|
|
12
|
+
task_spec.prev_tasks << self
|
13
|
+
|
14
|
+
@workflow_spec ||= task_spec.workflow_spec
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def next_tasks
|
19
|
+
[*super, *@else_tasks]
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_complete(task)
|
23
|
+
task.mark_as_complete!
|
24
|
+
|
25
|
+
@condition_next_tasks.each do |cond, next_tasks|
|
26
|
+
next unless cond.call(task)
|
27
|
+
|
28
|
+
predict(task, next_tasks)
|
29
|
+
return # rubocop:disable Lint/NonLocalExitFromIterator
|
30
|
+
end
|
31
|
+
predict(task, @else_tasks)
|
32
|
+
end
|
33
|
+
|
34
|
+
def connect_else(*task_specs)
|
35
|
+
task_specs.each do |task_spec|
|
36
|
+
task_spec.prev_tasks << self
|
37
|
+
@else_tasks << task_spec
|
38
|
+
|
39
|
+
@workflow_spec ||= task_spec.workflow_spec
|
40
|
+
end
|
41
|
+
|
42
|
+
return task_specs.first if task_specs.size <= 1
|
43
|
+
|
44
|
+
task_specs
|
45
|
+
end
|
46
|
+
|
47
|
+
def precedes?(task_spec)
|
48
|
+
@else_tasks.include?(task_spec) || super
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_hash
|
52
|
+
super.merge(
|
53
|
+
else_tasks: @else_tasks.map(&:id)
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_hash_tree
|
58
|
+
[*super, *@else_tasks.flat_map(&:to_hash_tree)]
|
59
|
+
end
|
60
|
+
|
61
|
+
def inspect
|
62
|
+
"<#{self.class}:#{hash} " \
|
63
|
+
"@id=#{@id} " \
|
64
|
+
"@condition_next_tasks=#{@condition_next_tasks.map { |k, t| [k, t.map(&:id)] }} " \
|
65
|
+
"@else_tasks=#{@else_tasks.map(&:id)}>"
|
66
|
+
end
|
67
|
+
|
68
|
+
def workflow_spec=(workflow_spec)
|
69
|
+
return if workflow_spec == @workflow_spec
|
70
|
+
|
71
|
+
super
|
72
|
+
|
73
|
+
@else_tasks.each do |task_spec|
|
74
|
+
task_spec.workflow_spec = workflow_spec
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# private-ish
|
79
|
+
def connects_with(tasks)
|
80
|
+
return unless @connects_to
|
81
|
+
|
82
|
+
connect_else(*@connects_to.fetch(:else_tasks).map { |id| tasks[id] })
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
class << self
|
87
|
+
def from_hash(hash)
|
88
|
+
case hash
|
89
|
+
in {
|
90
|
+
condition_next_tasks: [[{ type: String, **}, [*, String, *]]] => cond_tasks,
|
91
|
+
else_tasks: [*, String, *] => else_tasks,
|
92
|
+
**args
|
93
|
+
}
|
94
|
+
spec = super(**args)
|
95
|
+
spec.connects_to = {
|
96
|
+
condition_next_tasks: cond_tasks,
|
97
|
+
else_tasks: else_tasks
|
98
|
+
}
|
99
|
+
spec
|
100
|
+
else
|
101
|
+
super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
module Wolflow
|
6
|
+
module StringExtensions
|
7
|
+
refine ::String do
|
8
|
+
# https://github.com/jeremyevans/sequel/blob/58dd8d6e17d86cbf8809467c339be59aad54d5bf/lib/sequel/extensions/inflector.rb#L254
|
9
|
+
def underscore
|
10
|
+
gsub("::", "/").gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
11
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2').tr("-", "_").downcase
|
12
|
+
end
|
13
|
+
|
14
|
+
# https://github.com/jeremyevans/sequel/blob/58dd8d6e17d86cbf8809467c339be59aad54d5bf/lib/sequel/extensions/inflector.rb#L170
|
15
|
+
def demodulize
|
16
|
+
gsub(/^.*::/, "")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class MultiChoice < TaskSpec
|
5
|
+
attr_reader :condition_next_tasks
|
6
|
+
|
7
|
+
def initialize(condition_next_tasks: [], **kwargs)
|
8
|
+
super(**kwargs)
|
9
|
+
@condition_next_tasks = condition_next_tasks
|
10
|
+
end
|
11
|
+
|
12
|
+
def next_tasks
|
13
|
+
@condition_next_tasks.flat_map { |_, tasks| tasks }
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_complete(task)
|
17
|
+
task.mark_as_complete!
|
18
|
+
|
19
|
+
succ_next_tasks = @condition_next_tasks.filter_map do |cond, next_tasks|
|
20
|
+
next_tasks if cond.call(task)
|
21
|
+
end.flatten
|
22
|
+
predict(task, succ_next_tasks)
|
23
|
+
end
|
24
|
+
|
25
|
+
def connect(condition, *task_specs)
|
26
|
+
raise ArgumentError, "condition must response to #call" unless condition.respond_to?(:call)
|
27
|
+
|
28
|
+
connect_cond(condition, task_specs)
|
29
|
+
|
30
|
+
if block_given?
|
31
|
+
yield(*task_specs)
|
32
|
+
return self
|
33
|
+
end
|
34
|
+
|
35
|
+
return task_specs.first if task_specs.size <= 1
|
36
|
+
|
37
|
+
task_specs
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_next_tasks(task, **); end
|
41
|
+
|
42
|
+
def precedes?(task_spec)
|
43
|
+
@condition_next_tasks.one? { |_, tspecs| tspecs.include?(task_spec) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_hash
|
47
|
+
{
|
48
|
+
id: @id,
|
49
|
+
name: @name,
|
50
|
+
condition_next_tasks: @condition_next_tasks.map do |condition, task_specs|
|
51
|
+
condition_hash = Hash.try_convert(condition)
|
52
|
+
raise Error, "can't convert #{condition} to a hash" unless condition_hash
|
53
|
+
|
54
|
+
[condition_hash, task_specs.map(&:id)]
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_hash_tree
|
60
|
+
[to_hash, *@condition_next_tasks.flat_map { |_, ts| ts.flat_map(&:to_hash_tree) }]
|
61
|
+
end
|
62
|
+
|
63
|
+
def inspect
|
64
|
+
"#<#{self.class}:#{hash} " \
|
65
|
+
"@id=#{@id} " \
|
66
|
+
"@condition_next_tasks=#{@condition_next_tasks.map { |k, t| [k, t.map(&:id)] }}>"
|
67
|
+
end
|
68
|
+
|
69
|
+
def workflow_spec=(workflow_spec)
|
70
|
+
return if workflow_spec == @workflow_spec
|
71
|
+
|
72
|
+
super
|
73
|
+
|
74
|
+
@condition_next_tasks.each do |_, task_specs| # rubocop:disable Style/HashEachMethods
|
75
|
+
task_specs.each do |task_spec|
|
76
|
+
task_spec.workflow_spec = workflow_spec
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# private-ish
|
82
|
+
def connects_with(tasks)
|
83
|
+
return unless @connects_to
|
84
|
+
|
85
|
+
@connects_to.fetch(:condition_next_tasks).each do |cond, ids|
|
86
|
+
cond = Operators.from_hash(cond)
|
87
|
+
connect(cond, *ids.map { |id| tasks[id] })
|
88
|
+
end
|
89
|
+
|
90
|
+
@connects_to = nil
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def connect_cond(condition, task_specs)
|
96
|
+
@condition_next_tasks << [condition, task_specs]
|
97
|
+
|
98
|
+
task_specs.each { |task_spec| task_spec.prev_tasks << self }
|
99
|
+
end
|
100
|
+
|
101
|
+
def predict(task, next_tasks)
|
102
|
+
if task.children.empty?
|
103
|
+
super
|
104
|
+
else
|
105
|
+
task.children.delete_if { |child_task| !next_tasks.include?(child_task.task_spec) }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class << self
|
110
|
+
def from_hash(hash)
|
111
|
+
case hash
|
112
|
+
in {
|
113
|
+
condition_next_tasks: [[{ type: String, **}, [*, String, *]]] => cond_tasks,
|
114
|
+
**args
|
115
|
+
}
|
116
|
+
spec = super(**args)
|
117
|
+
spec.connects_to = {
|
118
|
+
condition_next_tasks: cond_tasks
|
119
|
+
}
|
120
|
+
spec
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class MultiMerge < 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 super if task.parents.all?(&:completed?)
|
13
|
+
|
14
|
+
# build repeat tree
|
15
|
+
task.copy_task_tree(task.parents.size)
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
module Operators
|
5
|
+
class Attribute < Base
|
6
|
+
def initialize(name:, **args)
|
7
|
+
@name = name
|
8
|
+
super(**args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(task)
|
12
|
+
task.data.fetch(@name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
hs = super
|
17
|
+
hs[:name] = @name
|
18
|
+
hs
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
module Operators
|
5
|
+
@op_types = {}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :op_types
|
9
|
+
|
10
|
+
def from_hash(hash)
|
11
|
+
case hash
|
12
|
+
in { type: String => type, **args }
|
13
|
+
operator_class = Operators.op_types.fetch(type)
|
14
|
+
args = operator_class.from_hash(args)
|
15
|
+
operator_class.new(**args)
|
16
|
+
else
|
17
|
+
raise TaskSpecError, "can't deserialize #{hash} to an operator"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Base
|
23
|
+
using StringExtensions unless ::String.method_defined?(:underscore)
|
24
|
+
|
25
|
+
def initialize(type: self.class.op_type)
|
26
|
+
@type = type
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_hash
|
30
|
+
{
|
31
|
+
type: @type
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
attr_reader :op_type
|
37
|
+
|
38
|
+
def inherited(subclass)
|
39
|
+
super
|
40
|
+
|
41
|
+
return unless self == Base
|
42
|
+
|
43
|
+
name = subclass.name
|
44
|
+
|
45
|
+
return unless name # anon class
|
46
|
+
|
47
|
+
tag = name.demodulize.underscore
|
48
|
+
|
49
|
+
raise Error, "operation type for #{tag} already exists" if Operators.op_types.key?(tag)
|
50
|
+
|
51
|
+
subclass.instance_variable_set(:@op_type, tag)
|
52
|
+
subclass.instance_variable_get(:@op_type).freeze
|
53
|
+
|
54
|
+
Operators.op_types[tag] = subclass
|
55
|
+
end
|
56
|
+
|
57
|
+
def from_hash(hash)
|
58
|
+
hash
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
require_relative "literal"
|
66
|
+
require_relative "attribute"
|
67
|
+
require_relative "operation"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
module Operators
|
5
|
+
class Literal < Base
|
6
|
+
def initialize(value:, **args)
|
7
|
+
@value = value
|
8
|
+
super(**args)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(*)
|
12
|
+
@value
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
hs = super
|
17
|
+
hs[:value] = @value
|
18
|
+
hs
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
module Operators
|
5
|
+
class Operation < Base
|
6
|
+
def initialize(op:, members:, **args)
|
7
|
+
@op = op
|
8
|
+
@members = Array(members)
|
9
|
+
super(**args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(task)
|
13
|
+
reducer, *members = @members
|
14
|
+
|
15
|
+
return unless reducer
|
16
|
+
|
17
|
+
reducer = reducer.call(task)
|
18
|
+
|
19
|
+
members.reduce(reducer) do |acc, member|
|
20
|
+
acc.__send__(@op, member.call(task))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_hash
|
25
|
+
hs = super
|
26
|
+
hs[:op] = @op
|
27
|
+
hs[:members] = @members.map(&:to_hash)
|
28
|
+
hs
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.from_hash(hash)
|
32
|
+
case hash
|
33
|
+
in { op: String, members: [*, Hash, *] => members }
|
34
|
+
members.map! { |m| Operators.from_hash(m) }
|
35
|
+
super(hash.merge(members: members))
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Wolflow
|
4
|
+
class Recursion < Simple
|
5
|
+
def on_complete(task)
|
6
|
+
return if task.completed?
|
7
|
+
|
8
|
+
return unless task.children.empty?
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def predict(task, next_tasks)
|
14
|
+
build_next_tasks(task, next_tasks: next_tasks)
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_next_tasks(task, **)
|
18
|
+
super if task.completed?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|