floe 0.11.2 → 0.12.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/.codeclimate.yml +16 -0
- data/.yamllint +1 -3
- data/CHANGELOG.md +51 -1
- data/Gemfile +1 -1
- data/LICENSE.txt +202 -0
- data/README.md +5 -1
- data/exe/floe +3 -72
- data/floe.gemspec +5 -4
- data/lib/floe/cli.rb +86 -0
- data/lib/floe/container_runner/docker_mixin.rb +3 -0
- data/lib/floe/runner.rb +9 -4
- data/lib/floe/validation_mixin.rb +49 -0
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/catcher.rb +17 -3
- data/lib/floe/workflow/choice_rule.rb +14 -9
- data/lib/floe/workflow/context.rb +27 -5
- data/lib/floe/workflow/error_matcher_mixin.rb +17 -0
- data/lib/floe/workflow/intrinsic_function/parser.rb +100 -0
- data/lib/floe/workflow/intrinsic_function/transformer.rb +196 -0
- data/lib/floe/workflow/intrinsic_function.rb +34 -0
- data/lib/floe/workflow/path.rb +7 -1
- data/lib/floe/workflow/payload_template.rb +7 -4
- data/lib/floe/workflow/reference_path.rb +2 -5
- data/lib/floe/workflow/retrier.rb +11 -4
- data/lib/floe/workflow/state.rb +33 -46
- data/lib/floe/workflow/states/choice.rb +12 -11
- data/lib/floe/workflow/states/fail.rb +3 -3
- data/lib/floe/workflow/states/input_output_mixin.rb +8 -8
- data/lib/floe/workflow/states/non_terminal_mixin.rb +6 -6
- data/lib/floe/workflow/states/pass.rb +7 -6
- data/lib/floe/workflow/states/succeed.rb +12 -3
- data/lib/floe/workflow/states/task.rb +35 -30
- data/lib/floe/workflow/states/wait.rb +8 -7
- data/lib/floe/workflow.rb +75 -23
- data/lib/floe.rb +6 -0
- metadata +31 -22
data/lib/floe/workflow/state.rb
CHANGED
@@ -4,114 +4,101 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
class State
|
6
6
|
include Logging
|
7
|
+
include ValidationMixin
|
7
8
|
|
8
9
|
class << self
|
9
10
|
def build!(workflow, name, payload)
|
10
11
|
state_type = payload["Type"]
|
11
|
-
|
12
|
+
missing_field_error!(name, "Type") if payload["Type"].nil?
|
13
|
+
invalid_field_error!(name[0..-2], "Name", name.last, "must be less than or equal to 80 characters") if name.last.length > 80
|
12
14
|
|
13
15
|
begin
|
14
16
|
klass = Floe::Workflow::States.const_get(state_type)
|
15
17
|
rescue NameError
|
16
|
-
|
18
|
+
invalid_field_error!(name, "Type", state_type, "is not valid")
|
17
19
|
end
|
18
20
|
|
19
21
|
klass.new(workflow, name, payload)
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
|
-
attr_reader :
|
25
|
+
attr_reader :comment, :name, :type, :payload
|
24
26
|
|
25
|
-
def initialize(
|
26
|
-
@workflow = workflow
|
27
|
+
def initialize(_workflow, name, payload)
|
27
28
|
@name = name
|
28
29
|
@payload = payload
|
29
30
|
@type = payload["Type"]
|
30
31
|
@comment = payload["Comment"]
|
31
|
-
|
32
|
-
raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
|
33
|
-
raise Floe::InvalidWorkflowError, "State name [#{name}] must be less than or equal to 80 characters" if name.length > 80
|
34
32
|
end
|
35
33
|
|
36
|
-
def wait(timeout: nil)
|
34
|
+
def wait(context, timeout: nil)
|
37
35
|
start = Time.now.utc
|
38
36
|
|
39
37
|
loop do
|
40
|
-
return 0 if ready?
|
38
|
+
return 0 if ready?(context)
|
41
39
|
return Errno::EAGAIN if timeout && (timeout.zero? || Time.now.utc - start > timeout)
|
42
40
|
|
43
41
|
sleep(1)
|
44
42
|
end
|
45
43
|
end
|
46
44
|
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
# @return for incomplete Errno::EAGAIN, for completed 0
|
46
|
+
def run_nonblock!(context)
|
47
|
+
start(context) unless context.state_started?
|
48
|
+
return Errno::EAGAIN unless ready?(context)
|
50
49
|
|
51
|
-
finish
|
50
|
+
finish(context)
|
52
51
|
end
|
53
52
|
|
54
|
-
def start(
|
55
|
-
|
56
|
-
|
57
|
-
context.execution["StartTime"] ||= start_time
|
58
|
-
context.state["Guid"] = SecureRandom.uuid
|
59
|
-
context.state["EnteredTime"] = start_time
|
53
|
+
def start(context)
|
54
|
+
context.state["EnteredTime"] = Time.now.utc.iso8601
|
60
55
|
|
61
|
-
logger.info("Running state: [#{long_name}] with input [#{context.
|
56
|
+
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
|
62
57
|
end
|
63
58
|
|
64
|
-
def finish
|
59
|
+
def finish(context)
|
65
60
|
finished_time = Time.now.utc
|
66
|
-
finished_time_iso = finished_time.iso8601
|
67
61
|
entered_time = Time.parse(context.state["EnteredTime"])
|
68
62
|
|
69
|
-
context.state["FinishedTime"] ||=
|
63
|
+
context.state["FinishedTime"] ||= finished_time.iso8601
|
70
64
|
context.state["Duration"] = finished_time - entered_time
|
71
|
-
context.execution["EndTime"] = finished_time_iso if context.next_state.nil?
|
72
|
-
|
73
|
-
level = context.output&.[]("Error") ? :error : :info
|
74
|
-
logger.public_send(level, "Running state: [#{long_name}] with input [#{context.input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.output}]")
|
75
65
|
|
76
|
-
context.
|
66
|
+
level = context.failed? ? :error : :info
|
67
|
+
logger.public_send(level, "Running state: [#{long_name}] with input [#{context.json_input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.json_output}]")
|
77
68
|
|
78
69
|
0
|
79
70
|
end
|
80
71
|
|
81
|
-
def context
|
82
|
-
|
83
|
-
end
|
84
|
-
|
85
|
-
def started?
|
86
|
-
context.state.key?("EnteredTime")
|
72
|
+
def ready?(context)
|
73
|
+
!context.state_started? || !running?(context)
|
87
74
|
end
|
88
75
|
|
89
|
-
def
|
90
|
-
|
76
|
+
def running?(context)
|
77
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
91
78
|
end
|
92
79
|
|
93
|
-
def
|
94
|
-
context.state.key?("FinishedTime")
|
95
|
-
end
|
96
|
-
|
97
|
-
def waiting?
|
80
|
+
def waiting?(context)
|
98
81
|
context.state["WaitUntil"] && Time.now.utc <= Time.parse(context.state["WaitUntil"])
|
99
82
|
end
|
100
83
|
|
101
|
-
def wait_until
|
84
|
+
def wait_until(context)
|
102
85
|
context.state["WaitUntil"] && Time.parse(context.state["WaitUntil"])
|
103
86
|
end
|
104
87
|
|
88
|
+
def short_name
|
89
|
+
name.last
|
90
|
+
end
|
91
|
+
|
105
92
|
def long_name
|
106
|
-
"#{
|
93
|
+
"#{type}:#{short_name}"
|
107
94
|
end
|
108
95
|
|
109
96
|
private
|
110
97
|
|
111
|
-
def wait_until!(seconds: nil, time: nil)
|
98
|
+
def wait_until!(context, seconds: nil, time: nil)
|
112
99
|
context.state["WaitUntil"] =
|
113
100
|
if seconds
|
114
|
-
(Time.
|
101
|
+
(Time.now + seconds).iso8601
|
115
102
|
elsif time.kind_of?(String)
|
116
103
|
time
|
117
104
|
else
|
@@ -9,17 +9,18 @@ module Floe
|
|
9
9
|
def initialize(workflow, name, payload)
|
10
10
|
super
|
11
11
|
|
12
|
-
validate_state!
|
12
|
+
validate_state!(workflow)
|
13
13
|
|
14
|
-
@choices = payload["Choices"].map { |choice| ChoiceRule.build(choice) }
|
14
|
+
@choices = payload["Choices"].map.with_index { |choice, i| ChoiceRule.build(workflow, name + ["Choices", i.to_s], choice) }
|
15
15
|
@default = payload["Default"]
|
16
16
|
|
17
17
|
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
18
18
|
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
19
19
|
end
|
20
20
|
|
21
|
-
def finish
|
22
|
-
|
21
|
+
def finish(context)
|
22
|
+
input = input_path.value(context, context.input)
|
23
|
+
output = output_path.value(context, input)
|
23
24
|
next_state = choices.detect { |choice| choice.true?(context, output) }&.next || default
|
24
25
|
|
25
26
|
context.next_state = next_state
|
@@ -27,7 +28,7 @@ module Floe
|
|
27
28
|
super
|
28
29
|
end
|
29
30
|
|
30
|
-
def running?
|
31
|
+
def running?(_)
|
31
32
|
false
|
32
33
|
end
|
33
34
|
|
@@ -37,18 +38,18 @@ module Floe
|
|
37
38
|
|
38
39
|
private
|
39
40
|
|
40
|
-
def validate_state!
|
41
|
+
def validate_state!(workflow)
|
41
42
|
validate_state_choices!
|
42
|
-
validate_state_default!
|
43
|
+
validate_state_default!(workflow)
|
43
44
|
end
|
44
45
|
|
45
46
|
def validate_state_choices!
|
46
|
-
|
47
|
-
|
47
|
+
missing_field_error!("Choices") unless payload.key?("Choices")
|
48
|
+
invalid_field_error!("Choices", nil, "must be a non-empty array") unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
|
48
49
|
end
|
49
50
|
|
50
|
-
def validate_state_default!
|
51
|
-
|
51
|
+
def validate_state_default!(workflow)
|
52
|
+
invalid_field_error!("Default", payload["Default"], "is not found in \"States\"") if payload["Default"] && !workflow_state?(payload["Default"], workflow)
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|
@@ -4,7 +4,7 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
module States
|
6
6
|
class Fail < Floe::Workflow::State
|
7
|
-
attr_reader :cause, :error
|
7
|
+
attr_reader :cause, :error, :cause_path, :error_path
|
8
8
|
|
9
9
|
def initialize(workflow, name, payload)
|
10
10
|
super
|
@@ -15,7 +15,7 @@ module Floe
|
|
15
15
|
@error_path = Path.new(payload["ErrorPath"]) if payload["ErrorPath"]
|
16
16
|
end
|
17
17
|
|
18
|
-
def finish
|
18
|
+
def finish(context)
|
19
19
|
context.next_state = nil
|
20
20
|
# TODO: support intrinsic functions here
|
21
21
|
# see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html
|
@@ -27,7 +27,7 @@ module Floe
|
|
27
27
|
super
|
28
28
|
end
|
29
29
|
|
30
|
-
def running?
|
30
|
+
def running?(_)
|
31
31
|
false
|
32
32
|
end
|
33
33
|
|
@@ -4,23 +4,23 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
module States
|
6
6
|
module InputOutputMixin
|
7
|
-
def process_input(
|
8
|
-
input = input_path.value(context, input)
|
7
|
+
def process_input(context)
|
8
|
+
input = input_path.value(context, context.input)
|
9
9
|
input = parameters.value(context, input) if parameters
|
10
10
|
input
|
11
11
|
end
|
12
12
|
|
13
|
-
def process_output(
|
14
|
-
return input if results.nil?
|
13
|
+
def process_output(context, results)
|
14
|
+
return context.input.dup if results.nil?
|
15
15
|
return if output_path.nil?
|
16
16
|
|
17
17
|
results = result_selector.value(context, results) if @result_selector
|
18
18
|
if result_path.payload.start_with?("$.Credentials")
|
19
|
-
credentials = result_path.set(
|
20
|
-
|
21
|
-
output = input
|
19
|
+
credentials = result_path.set(context.credentials, results)["Credentials"]
|
20
|
+
context.credentials.merge!(credentials)
|
21
|
+
output = context.input.dup
|
22
22
|
else
|
23
|
-
output = result_path.set(input, results)
|
23
|
+
output = result_path.set(context.input.dup, results)
|
24
24
|
end
|
25
25
|
|
26
26
|
output_path.value(context, output)
|
@@ -4,16 +4,16 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
module States
|
6
6
|
module NonTerminalMixin
|
7
|
-
def finish
|
8
|
-
# If this state is failed or
|
9
|
-
context.next_state
|
7
|
+
def finish(context)
|
8
|
+
# If this state is failed or this is an end state, next_state to nil
|
9
|
+
context.next_state ||= end? || context.failed? ? nil : @next
|
10
10
|
|
11
11
|
super
|
12
12
|
end
|
13
13
|
|
14
|
-
def validate_state_next!
|
15
|
-
|
16
|
-
|
14
|
+
def validate_state_next!(workflow)
|
15
|
+
missing_field_error!("Next") if @next.nil? && !@end
|
16
|
+
invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow_state?(@next, workflow)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -21,15 +21,16 @@ module Floe
|
|
21
21
|
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
22
22
|
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
|
23
23
|
|
24
|
-
validate_state!
|
24
|
+
validate_state!(workflow)
|
25
25
|
end
|
26
26
|
|
27
|
-
def finish
|
28
|
-
|
27
|
+
def finish(context)
|
28
|
+
input = result.nil? ? process_input(context) : result
|
29
|
+
context.output = process_output(context, input)
|
29
30
|
super
|
30
31
|
end
|
31
32
|
|
32
|
-
def running?
|
33
|
+
def running?(_)
|
33
34
|
false
|
34
35
|
end
|
35
36
|
|
@@ -39,8 +40,8 @@ module Floe
|
|
39
40
|
|
40
41
|
private
|
41
42
|
|
42
|
-
def validate_state!
|
43
|
-
validate_state_next!
|
43
|
+
def validate_state!(workflow)
|
44
|
+
validate_state_next!(workflow)
|
44
45
|
end
|
45
46
|
end
|
46
47
|
end
|
@@ -6,13 +6,22 @@ module Floe
|
|
6
6
|
class Succeed < Floe::Workflow::State
|
7
7
|
attr_reader :input_path, :output_path
|
8
8
|
|
9
|
-
def
|
9
|
+
def initialize(workflow, name, payload)
|
10
|
+
super
|
11
|
+
|
12
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
13
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
14
|
+
end
|
15
|
+
|
16
|
+
def finish(context)
|
17
|
+
input = input_path.value(context, context.input)
|
18
|
+
context.output = output_path.value(context, input)
|
10
19
|
context.next_state = nil
|
11
|
-
|
20
|
+
|
12
21
|
super
|
13
22
|
end
|
14
23
|
|
15
|
-
def running?
|
24
|
+
def running?(_)
|
16
25
|
false
|
17
26
|
end
|
18
27
|
|
@@ -18,10 +18,13 @@ module Floe
|
|
18
18
|
@next = payload["Next"]
|
19
19
|
@end = !!payload["End"]
|
20
20
|
@resource = payload["Resource"]
|
21
|
-
|
21
|
+
|
22
|
+
missing_field_error!("Resource") unless @resource.kind_of?(String)
|
23
|
+
@runner = wrap_parser_error("Resource", @resource) { Floe::Runner.for_resource(@resource) }
|
24
|
+
|
22
25
|
@timeout_seconds = payload["TimeoutSeconds"]
|
23
|
-
@retry = payload["Retry"].to_a.map { |retrier| Retrier.new(retrier) }
|
24
|
-
@catch = payload["Catch"].to_a.map { |catcher| Catcher.new(catcher) }
|
26
|
+
@retry = payload["Retry"].to_a.map.with_index { |retrier, i| Retrier.new(workflow, name + ["Retry", i.to_s], retrier) }
|
27
|
+
@catch = payload["Catch"].to_a.map.with_index { |catcher, i| Catcher.new(workflow, name + ["Catch", i.to_s], catcher) }
|
25
28
|
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
26
29
|
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
27
30
|
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
|
@@ -29,36 +32,35 @@ module Floe
|
|
29
32
|
@result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
|
30
33
|
@credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
|
31
34
|
|
32
|
-
validate_state!
|
35
|
+
validate_state!(workflow)
|
33
36
|
end
|
34
37
|
|
35
|
-
def start(
|
38
|
+
def start(context)
|
36
39
|
super
|
37
40
|
|
38
|
-
input = process_input(
|
39
|
-
runner_context = runner.run_async!(resource, input, credentials&.value({},
|
41
|
+
input = process_input(context)
|
42
|
+
runner_context = runner.run_async!(resource, input, credentials&.value({}, context.credentials), context)
|
40
43
|
|
41
44
|
context.state["RunnerContext"] = runner_context
|
42
45
|
end
|
43
46
|
|
44
|
-
def finish
|
47
|
+
def finish(context)
|
45
48
|
output = runner.output(context.state["RunnerContext"])
|
46
49
|
|
47
|
-
if success?
|
50
|
+
if success?(context)
|
48
51
|
output = parse_output(output)
|
49
|
-
context.output = process_output(context
|
50
|
-
super
|
52
|
+
context.output = process_output(context, output)
|
51
53
|
else
|
52
|
-
|
53
|
-
|
54
|
-
retry_state!(error) || catch_error!(error) || fail_workflow!(error)
|
54
|
+
error = parse_error(output)
|
55
|
+
retry_state!(context, error) || catch_error!(context, error) || fail_workflow!(context, error)
|
55
56
|
end
|
57
|
+
super
|
56
58
|
ensure
|
57
59
|
runner.cleanup(context.state["RunnerContext"])
|
58
60
|
end
|
59
61
|
|
60
|
-
def running?
|
61
|
-
return true if waiting?
|
62
|
+
def running?(context)
|
63
|
+
return true if waiting?(context)
|
62
64
|
|
63
65
|
runner.status!(context.state["RunnerContext"])
|
64
66
|
runner.running?(context.state["RunnerContext"])
|
@@ -72,23 +74,23 @@ module Floe
|
|
72
74
|
|
73
75
|
attr_reader :runner
|
74
76
|
|
75
|
-
def validate_state!
|
76
|
-
validate_state_next!
|
77
|
+
def validate_state!(workflow)
|
78
|
+
validate_state_next!(workflow)
|
77
79
|
end
|
78
80
|
|
79
|
-
def success?
|
81
|
+
def success?(context)
|
80
82
|
runner.success?(context.state["RunnerContext"])
|
81
83
|
end
|
82
84
|
|
83
85
|
def find_retrier(error)
|
84
|
-
self.retry.detect { |r|
|
86
|
+
self.retry.detect { |r| r.match_error?(error) }
|
85
87
|
end
|
86
88
|
|
87
89
|
def find_catcher(error)
|
88
|
-
self.catch.detect { |c|
|
90
|
+
self.catch.detect { |c| c.match_error?(error) }
|
89
91
|
end
|
90
92
|
|
91
|
-
def retry_state!(error)
|
93
|
+
def retry_state!(context, error)
|
92
94
|
retrier = find_retrier(error["Error"]) if error
|
93
95
|
return if retrier.nil?
|
94
96
|
|
@@ -102,27 +104,30 @@ module Floe
|
|
102
104
|
|
103
105
|
return if context["State"]["RetryCount"] > retrier.max_attempts
|
104
106
|
|
105
|
-
wait_until!(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
|
107
|
+
wait_until!(context, :seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
|
106
108
|
context.next_state = context.state_name
|
107
|
-
|
109
|
+
context.output = error
|
110
|
+
logger.info("Running state: [#{long_name}] with input [#{context.json_input}] got error[#{context.json_output}]...Retry - delay: #{wait_until(context)}")
|
108
111
|
true
|
109
112
|
end
|
110
113
|
|
111
|
-
def catch_error!(error)
|
114
|
+
def catch_error!(context, error)
|
112
115
|
catcher = find_catcher(error["Error"]) if error
|
113
116
|
return if catcher.nil?
|
114
117
|
|
115
118
|
context.next_state = catcher.next
|
116
119
|
context.output = catcher.result_path.set(context.input, error)
|
117
|
-
logger.info("Running state: [#{long_name}] with input [#{context.
|
120
|
+
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...CatchError - next state: [#{context.next_state}] output: [#{context.json_output}]")
|
118
121
|
|
119
122
|
true
|
120
123
|
end
|
121
124
|
|
122
|
-
def fail_workflow!(error)
|
123
|
-
|
124
|
-
|
125
|
-
|
125
|
+
def fail_workflow!(context, error)
|
126
|
+
# next_state is nil, and will be set to nil again in super
|
127
|
+
# keeping in here for completeness
|
128
|
+
context.next_state = nil
|
129
|
+
context.output = error
|
130
|
+
logger.error("Running state: [#{long_name}] with input [#{context.json_input}]...Complete workflow - output: [#{context.json_output}]")
|
126
131
|
end
|
127
132
|
|
128
133
|
def parse_error(output)
|
@@ -23,28 +23,29 @@ module Floe
|
|
23
23
|
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
24
24
|
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
25
25
|
|
26
|
-
validate_state!
|
26
|
+
validate_state!(workflow)
|
27
27
|
end
|
28
28
|
|
29
|
-
def start(
|
29
|
+
def start(context)
|
30
30
|
super
|
31
31
|
|
32
32
|
input = input_path.value(context, context.input)
|
33
33
|
|
34
34
|
wait_until!(
|
35
|
+
context,
|
35
36
|
:seconds => seconds_path ? seconds_path.value(context, input).to_i : seconds,
|
36
37
|
:time => timestamp_path ? timestamp_path.value(context, input) : timestamp
|
37
38
|
)
|
38
39
|
end
|
39
40
|
|
40
|
-
def finish
|
41
|
+
def finish(context)
|
41
42
|
input = input_path.value(context, context.input)
|
42
43
|
context.output = output_path.value(context, input)
|
43
44
|
super
|
44
45
|
end
|
45
46
|
|
46
|
-
def running?
|
47
|
-
waiting?
|
47
|
+
def running?(context)
|
48
|
+
waiting?(context)
|
48
49
|
end
|
49
50
|
|
50
51
|
def end?
|
@@ -53,8 +54,8 @@ module Floe
|
|
53
54
|
|
54
55
|
private
|
55
56
|
|
56
|
-
def validate_state!
|
57
|
-
validate_state_next!
|
57
|
+
def validate_state!(workflow)
|
58
|
+
validate_state_next!(workflow)
|
58
59
|
end
|
59
60
|
end
|
60
61
|
end
|