floe 0.11.2 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|