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,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
|