floe 0.11.3 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yamllint +1 -3
- data/CHANGELOG.md +37 -1
- data/floe.gemspec +2 -2
- data/lib/floe/cli.rb +4 -1
- data/lib/floe/container_runner/docker_mixin.rb +3 -0
- 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/data.rb +17 -5
- data/lib/floe/workflow/choice_rule.rb +14 -9
- data/lib/floe/workflow/context.rb +11 -5
- data/lib/floe/workflow/error_matcher_mixin.rb +17 -0
- data/lib/floe/workflow/intrinsic_function/parser.rb +108 -0
- data/lib/floe/workflow/intrinsic_function/transformer.rb +266 -0
- data/lib/floe/workflow/intrinsic_function.rb +34 -0
- data/lib/floe/workflow/path.rb +18 -1
- data/lib/floe/workflow/payload_template.rb +7 -4
- data/lib/floe/workflow/retrier.rb +9 -3
- data/lib/floe/workflow/state.rb +35 -14
- data/lib/floe/workflow/states/choice.rb +6 -5
- data/lib/floe/workflow/states/fail.rb +1 -1
- data/lib/floe/workflow/states/non_terminal_mixin.rb +2 -2
- data/lib/floe/workflow/states/pass.rb +2 -1
- data/lib/floe/workflow/states/succeed.rb +10 -1
- data/lib/floe/workflow/states/task.rb +11 -10
- data/lib/floe/workflow.rb +19 -9
- data/lib/floe.rb +7 -0
- metadata +23 -18
@@ -0,0 +1,266 @@
|
|
1
|
+
# Disable rubocops against the `match` method, since this is a Parslet specific
|
2
|
+
# match method and not the typical `Object#match`.
|
3
|
+
# rubocop:disable Performance/RegexpMatch, Performance/RedundantMatch
|
4
|
+
|
5
|
+
require "parslet"
|
6
|
+
|
7
|
+
module Floe
|
8
|
+
class Workflow
|
9
|
+
class IntrinsicFunction
|
10
|
+
class Transformer < Parslet::Transform
|
11
|
+
OptionalArg = Struct.new(:type)
|
12
|
+
VariadicArgs = Struct.new(:type)
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def process_args(args, function, signature = nil)
|
16
|
+
args = resolve_args(args)
|
17
|
+
if signature
|
18
|
+
check_arity(args, function, signature)
|
19
|
+
check_types(args, function, signature)
|
20
|
+
end
|
21
|
+
args
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def resolve_args(args)
|
27
|
+
if args.nil?
|
28
|
+
# 0 args
|
29
|
+
[]
|
30
|
+
elsif args.kind_of?(Array)
|
31
|
+
# >1 arg
|
32
|
+
args.map { |a| a[:arg] }
|
33
|
+
else
|
34
|
+
# 1 arg
|
35
|
+
[args[:arg]]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_arity(args, function, signature)
|
40
|
+
if signature.any?(OptionalArg)
|
41
|
+
signature_required = signature.reject { |a| a.kind_of?(OptionalArg) }
|
42
|
+
signature_size = (signature_required.size..signature.size)
|
43
|
+
|
44
|
+
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature_size})" unless signature_size.include?(args.size)
|
45
|
+
elsif signature.any?(VariadicArgs)
|
46
|
+
signature_required = signature.reject { |a| a.kind_of?(VariadicArgs) }
|
47
|
+
|
48
|
+
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected at least #{signature_required.size})" unless args.size >= signature_required.size
|
49
|
+
else
|
50
|
+
raise ArgumentError, "wrong number of arguments to #{function} (given #{args.size}, expected #{signature.size})" unless signature.size == args.size
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def check_types(args, function, signature)
|
55
|
+
# Adjust the signature for VariadicArgs to create a copy of the expected type for each given arg
|
56
|
+
if signature.last.kind_of?(VariadicArgs)
|
57
|
+
signature = signature[0..-2] + Array.new(args.size - signature.size + 1, signature.last.type)
|
58
|
+
end
|
59
|
+
|
60
|
+
args.zip(signature).each_with_index do |(arg, type), index|
|
61
|
+
type = type.type if type.kind_of?(OptionalArg)
|
62
|
+
|
63
|
+
if type.kind_of?(Array)
|
64
|
+
raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected one of #{type.join(", ")})" unless type.any? { |t| arg.kind_of?(t) }
|
65
|
+
else
|
66
|
+
raise ArgumentError, "wrong type for argument #{index + 1} to #{function} (given #{arg.class}, expected #{type})" unless arg.kind_of?(type)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
rule(:null_literal => simple(:v)) { nil }
|
73
|
+
rule(:true_literal => simple(:v)) { true }
|
74
|
+
rule(:false_literal => simple(:v)) { false }
|
75
|
+
|
76
|
+
rule(:string => simple(:v)) { v.to_s[1..-2] }
|
77
|
+
rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
|
78
|
+
rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
|
79
|
+
|
80
|
+
STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze
|
81
|
+
|
82
|
+
rule(:states_format => {:args => subtree(:args)}) do
|
83
|
+
args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Integer, Float, NilClass]]])
|
84
|
+
str, *rest = *args
|
85
|
+
|
86
|
+
# TODO: Handle templates with escaped characters, including invalid templates
|
87
|
+
# See https://states-language.net/#intrinsic-functions (number 6)
|
88
|
+
|
89
|
+
expected_args = str.scan(STATES_FORMAT_PLACEHOLDER).size
|
90
|
+
actual_args = rest.size
|
91
|
+
if expected_args != actual_args
|
92
|
+
raise ArgumentError, "number of arguments to States.Format do not match the occurrences of {} (given #{actual_args}, expected #{expected_args})"
|
93
|
+
end
|
94
|
+
|
95
|
+
rest.each do |arg|
|
96
|
+
str = str.sub(STATES_FORMAT_PLACEHOLDER, arg.nil? ? "null" : arg.to_s)
|
97
|
+
end
|
98
|
+
|
99
|
+
# TODO: Handle arguments that have escape characters within them but are interpolated
|
100
|
+
str.gsub!("\\'", "'")
|
101
|
+
str.gsub!("\\{", "{")
|
102
|
+
str.gsub!("\\}", "}")
|
103
|
+
str.gsub!("\\\\", "\\")
|
104
|
+
|
105
|
+
str
|
106
|
+
end
|
107
|
+
|
108
|
+
rule(:states_json_to_string => {:args => subtree(:args)}) do
|
109
|
+
args = Transformer.process_args(args(), "States.JsonToString", [Object])
|
110
|
+
json = args.first
|
111
|
+
|
112
|
+
JSON.generate(json)
|
113
|
+
end
|
114
|
+
|
115
|
+
rule(:states_string_to_json => {:args => subtree(:args)}) do
|
116
|
+
args = Transformer.process_args(args(), "States.StringToJson", [String])
|
117
|
+
str = args.first
|
118
|
+
|
119
|
+
JSON.parse(str)
|
120
|
+
rescue JSON::ParserError => e
|
121
|
+
raise ArgumentError, "invalid value for argument 1 to States.StringToJson (invalid json: #{e.message})"
|
122
|
+
end
|
123
|
+
|
124
|
+
rule(:states_array => {:args => subtree(:args)}) do
|
125
|
+
Transformer.process_args(args, "States.Array")
|
126
|
+
end
|
127
|
+
|
128
|
+
rule(:states_array_partition => {:args => subtree(:args)}) do
|
129
|
+
args = Transformer.process_args(args(), "States.ArrayPartition", [Array, Integer])
|
130
|
+
array, chunk = *args
|
131
|
+
raise ArgumentError, "invalid value for argument 2 to States.ArrayPartition (given #{chunk}, expected a positive Integer)" unless chunk.positive?
|
132
|
+
|
133
|
+
array.each_slice(chunk).to_a
|
134
|
+
end
|
135
|
+
|
136
|
+
rule(:states_array_contains => {:args => subtree(:args)}) do
|
137
|
+
args = Transformer.process_args(args(), "States.ArrayContains", [Array, Object])
|
138
|
+
array, target = *args
|
139
|
+
|
140
|
+
array.include?(target)
|
141
|
+
end
|
142
|
+
|
143
|
+
rule(:states_array_range => {:args => subtree(:args)}) do
|
144
|
+
args = Transformer.process_args(args(), "States.ArrayRange", [Integer, Integer, Integer])
|
145
|
+
range_begin, range_end, increment = *args
|
146
|
+
raise ArgumentError, "invalid value for argument 3 to States.ArrayRange (given #{increment}, expected a non-zero Integer)" if increment.zero?
|
147
|
+
|
148
|
+
(range_begin..range_end).step(increment).to_a
|
149
|
+
end
|
150
|
+
|
151
|
+
rule(:states_array_get_item => {:args => subtree(:args)}) do
|
152
|
+
args = Transformer.process_args(args(), "States.ArrayGetItem", [Array, Integer])
|
153
|
+
array, index = *args
|
154
|
+
raise ArgumentError, "invalid value for argument 2 to States.ArrayGetItem (given #{index}, expected 0 or a positive Integer)" unless index >= 0
|
155
|
+
|
156
|
+
array[index]
|
157
|
+
end
|
158
|
+
|
159
|
+
rule(:states_array_length => {:args => subtree(:args)}) do
|
160
|
+
args = Transformer.process_args(args(), "States.ArrayLength", [Array])
|
161
|
+
array = args.first
|
162
|
+
|
163
|
+
array.size
|
164
|
+
end
|
165
|
+
|
166
|
+
rule(:states_array_unique => {:args => subtree(:args)}) do
|
167
|
+
args = Transformer.process_args(args(), "States.ArrayUnique", [Array])
|
168
|
+
array = args.first
|
169
|
+
|
170
|
+
array.uniq
|
171
|
+
end
|
172
|
+
|
173
|
+
rule(:states_base64_encode => {:args => subtree(:args)}) do
|
174
|
+
args = Transformer.process_args(args(), "States.Base64Encode", [String])
|
175
|
+
str = args.first
|
176
|
+
|
177
|
+
require "base64"
|
178
|
+
Base64.strict_encode64(str).force_encoding("UTF-8")
|
179
|
+
end
|
180
|
+
|
181
|
+
rule(:states_base64_decode => {:args => subtree(:args)}) do
|
182
|
+
args = Transformer.process_args(args(), "States.Base64Decode", [String])
|
183
|
+
str = args.first
|
184
|
+
|
185
|
+
require "base64"
|
186
|
+
begin
|
187
|
+
Base64.strict_decode64(str)
|
188
|
+
rescue ArgumentError => err
|
189
|
+
raise ArgumentError, "invalid value for argument 1 to States.Base64Decode (#{err})"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
rule(:states_hash => {:args => subtree(:args)}) do
|
194
|
+
args = Transformer.process_args(args(), "States.Hash", [Object, String])
|
195
|
+
data, algorithm = *args
|
196
|
+
|
197
|
+
if data.nil?
|
198
|
+
raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
|
199
|
+
end
|
200
|
+
|
201
|
+
algorithms = %w[MD5 SHA-1 SHA-256 SHA-384 SHA-512]
|
202
|
+
unless algorithms.include?(algorithm)
|
203
|
+
raise ArgumentError, "invalid value for argument 2 to States.Hash (given #{algorithm.inspect}, expected one of #{algorithms.map(&:inspect).join(", ")})"
|
204
|
+
end
|
205
|
+
|
206
|
+
require "openssl"
|
207
|
+
algorithm = algorithm.sub("-", "")
|
208
|
+
data = JSON.generate(data) unless data.kind_of?(String)
|
209
|
+
OpenSSL::Digest.hexdigest(algorithm, data).force_encoding("UTF-8")
|
210
|
+
end
|
211
|
+
|
212
|
+
rule(:states_json_merge => {:args => subtree(:args)}) do
|
213
|
+
args = Transformer.process_args(args(), "States.JsonMerge", [Hash, Hash, [TrueClass, FalseClass]])
|
214
|
+
left, right, deep = *args
|
215
|
+
|
216
|
+
if deep
|
217
|
+
# NOTE: not implemented by AWS Step Functions and nuances not defined in docs
|
218
|
+
left.merge(right) { |_key, l, r| l.kind_of?(Hash) && r.kind_of?(Hash) ? l.merge(r) : r }
|
219
|
+
else
|
220
|
+
left.merge(right)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
rule(:states_math_random => {:args => subtree(:args)}) do
|
225
|
+
args = Transformer.process_args(args(), "States.MathRandom", [Integer, Integer, OptionalArg[Integer]])
|
226
|
+
range_start, range_end, seed = *args
|
227
|
+
unless range_start < range_end
|
228
|
+
raise ArgumentError, "invalid values for arguments to States.MathRandom (start must be less than end)"
|
229
|
+
end
|
230
|
+
|
231
|
+
seed ||= Random.new_seed
|
232
|
+
Random.new(seed).rand(range_start..range_end)
|
233
|
+
end
|
234
|
+
|
235
|
+
rule(:states_math_add => {:args => subtree(:args)}) do
|
236
|
+
args = Transformer.process_args(args(), "States.MathAdd", [Integer, Integer])
|
237
|
+
|
238
|
+
args.sum
|
239
|
+
end
|
240
|
+
|
241
|
+
rule(:states_string_split => {:args => subtree(:args)}) do
|
242
|
+
args = Transformer.process_args(args(), "States.StringSplit", [String, String])
|
243
|
+
str, delimeter = *args
|
244
|
+
|
245
|
+
case delimeter.size
|
246
|
+
when 0
|
247
|
+
str.empty? ? [] : [str]
|
248
|
+
when 1
|
249
|
+
str.split(delimeter)
|
250
|
+
else
|
251
|
+
str.split(/[#{Regexp.escape(delimeter)}]+/)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
rule(:states_uuid => {:args => subtree(:args)}) do
|
256
|
+
Transformer.process_args(args, "States.UUID", [])
|
257
|
+
|
258
|
+
require "securerandom"
|
259
|
+
SecureRandom.uuid
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# rubocop:enable Performance/RegexpMatch, Performance/RedundantMatch
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "parslet"
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class IntrinsicFunction
|
6
|
+
def self.value(payload, context = {}, input = {})
|
7
|
+
new(payload).value(context, input)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.intrinsic_function?(payload)
|
11
|
+
payload.start_with?("States.")
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :payload
|
15
|
+
|
16
|
+
def initialize(payload)
|
17
|
+
@payload = payload
|
18
|
+
@tree = Parser.new.parse(payload)
|
19
|
+
|
20
|
+
Floe.logger.debug { "Parsed intrinsic function: #{payload.inspect} => #{tree.inspect}" }
|
21
|
+
rescue Parslet::ParseFailed => err
|
22
|
+
raise Floe::InvalidWorkflowError, err.message
|
23
|
+
end
|
24
|
+
|
25
|
+
def value(context = {}, input = {})
|
26
|
+
Transformer.new.apply(tree, :context => context, :input => input)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :tree
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/floe/workflow/path.rb
CHANGED
@@ -4,6 +4,10 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
class Path
|
6
6
|
class << self
|
7
|
+
def path?(payload)
|
8
|
+
payload.start_with?("$")
|
9
|
+
end
|
10
|
+
|
7
11
|
def value(payload, context, input = {})
|
8
12
|
new(payload).value(context, input)
|
9
13
|
end
|
@@ -26,9 +30,22 @@ module Floe
|
|
26
30
|
[input, payload]
|
27
31
|
end
|
28
32
|
|
33
|
+
# If path is $ then just return the entire input
|
34
|
+
return obj if path == "$"
|
35
|
+
|
29
36
|
results = JsonPath.on(obj, path)
|
37
|
+
case results.count
|
38
|
+
when 0
|
39
|
+
raise Floe::PathError, "Path [#{payload}] references an invalid value"
|
40
|
+
when 1
|
41
|
+
results.first
|
42
|
+
else
|
43
|
+
results
|
44
|
+
end
|
45
|
+
end
|
30
46
|
|
31
|
-
|
47
|
+
def to_s
|
48
|
+
payload
|
32
49
|
end
|
33
50
|
end
|
34
51
|
end
|
@@ -42,14 +42,17 @@ module Floe
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def parse_payload_string(value)
|
45
|
-
|
45
|
+
return Path.new(value) if Path.path?(value)
|
46
|
+
return IntrinsicFunction.new(value) if IntrinsicFunction.intrinsic_function?(value)
|
47
|
+
|
48
|
+
value
|
46
49
|
end
|
47
50
|
|
48
51
|
def interpolate_value(value, context, inputs)
|
49
52
|
case value
|
50
|
-
when Array
|
51
|
-
when Hash
|
52
|
-
when Path
|
53
|
+
when Array then interpolate_value_array(value, context, inputs)
|
54
|
+
when Hash then interpolate_value_hash(value, context, inputs)
|
55
|
+
when Path, IntrinsicFunction then value.value(context, inputs)
|
53
56
|
else
|
54
57
|
value
|
55
58
|
end
|
@@ -3,15 +3,21 @@
|
|
3
3
|
module Floe
|
4
4
|
class Workflow
|
5
5
|
class Retrier
|
6
|
-
|
6
|
+
include ErrorMatcherMixin
|
7
|
+
include ValidationMixin
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate, :name
|
10
|
+
|
11
|
+
def initialize(_workflow, name, payload)
|
12
|
+
@name = name
|
13
|
+
@payload = payload
|
10
14
|
|
11
15
|
@error_equals = payload["ErrorEquals"]
|
12
16
|
@interval_seconds = payload["IntervalSeconds"] || 1.0
|
13
17
|
@max_attempts = payload["MaxAttempts"] || 3
|
14
18
|
@backoff_rate = payload["BackoffRate"] || 2.0
|
19
|
+
|
20
|
+
missing_field_error!("ErrorEquals") if !@error_equals.kind_of?(Array) || @error_equals.empty?
|
15
21
|
end
|
16
22
|
|
17
23
|
# @param [Integer] attempt 1 for the first attempt
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -4,16 +4,18 @@ 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)
|
@@ -22,14 +24,11 @@ module Floe
|
|
22
24
|
|
23
25
|
attr_reader :comment, :name, :type, :payload
|
24
26
|
|
25
|
-
def initialize(
|
27
|
+
def initialize(_workflow, name, payload)
|
26
28
|
@name = name
|
27
29
|
@payload = payload
|
28
30
|
@type = payload["Type"]
|
29
31
|
@comment = payload["Comment"]
|
30
|
-
|
31
|
-
raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
|
32
|
-
raise Floe::InvalidWorkflowError, "State name [#{name[..79]}...] must be less than or equal to 80 characters" if name.length > 80
|
33
32
|
end
|
34
33
|
|
35
34
|
def wait(context, timeout: nil)
|
@@ -49,27 +48,45 @@ module Floe
|
|
49
48
|
return Errno::EAGAIN unless ready?(context)
|
50
49
|
|
51
50
|
finish(context)
|
51
|
+
rescue Floe::Error => e
|
52
|
+
mark_error(context, e)
|
52
53
|
end
|
53
54
|
|
54
55
|
def start(context)
|
56
|
+
mark_started(context)
|
57
|
+
end
|
58
|
+
|
59
|
+
def finish(context)
|
60
|
+
mark_finished(context)
|
61
|
+
end
|
62
|
+
|
63
|
+
def mark_started(context)
|
55
64
|
context.state["EnteredTime"] = Time.now.utc.iso8601
|
56
65
|
|
57
|
-
logger.info("Running state: [#{long_name}] with input [#{context.
|
66
|
+
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
|
58
67
|
end
|
59
68
|
|
60
|
-
def
|
61
|
-
finished_time
|
62
|
-
entered_time
|
69
|
+
def mark_finished(context)
|
70
|
+
finished_time = Time.now.utc
|
71
|
+
entered_time = Time.parse(context.state["EnteredTime"])
|
63
72
|
|
64
73
|
context.state["FinishedTime"] ||= finished_time.iso8601
|
65
74
|
context.state["Duration"] = finished_time - entered_time
|
66
75
|
|
67
|
-
level = context.
|
68
|
-
logger.public_send(level, "Running state: [#{long_name}] with input [#{context.
|
76
|
+
level = context.failed? ? :error : :info
|
77
|
+
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}]")
|
69
78
|
|
70
79
|
0
|
71
80
|
end
|
72
81
|
|
82
|
+
def mark_error(context, exception)
|
83
|
+
# InputPath or OutputPath were bad.
|
84
|
+
context.next_state = nil
|
85
|
+
context.output = {"Error" => "States.Runtime", "Cause" => exception.message}
|
86
|
+
# Since finish threw an exception, super was never called. Calling that now.
|
87
|
+
mark_finished(context)
|
88
|
+
end
|
89
|
+
|
73
90
|
def ready?(context)
|
74
91
|
!context.state_started? || !running?(context)
|
75
92
|
end
|
@@ -86,8 +103,12 @@ module Floe
|
|
86
103
|
context.state["WaitUntil"] && Time.parse(context.state["WaitUntil"])
|
87
104
|
end
|
88
105
|
|
106
|
+
def short_name
|
107
|
+
name.last
|
108
|
+
end
|
109
|
+
|
89
110
|
def long_name
|
90
|
-
"#{
|
111
|
+
"#{type}:#{short_name}"
|
91
112
|
end
|
92
113
|
|
93
114
|
private
|
@@ -95,7 +116,7 @@ module Floe
|
|
95
116
|
def wait_until!(context, seconds: nil, time: nil)
|
96
117
|
context.state["WaitUntil"] =
|
97
118
|
if seconds
|
98
|
-
(Time.
|
119
|
+
(Time.now + seconds).iso8601
|
99
120
|
elsif time.kind_of?(String)
|
100
121
|
time
|
101
122
|
else
|
@@ -11,7 +11,7 @@ module Floe
|
|
11
11
|
|
12
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", "$"))
|
@@ -19,7 +19,8 @@ module Floe
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def finish(context)
|
22
|
-
|
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
|
@@ -43,12 +44,12 @@ module Floe
|
|
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
51
|
def validate_state_default!(workflow)
|
51
|
-
|
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
|
@@ -12,8 +12,8 @@ module Floe
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def validate_state_next!(workflow)
|
15
|
-
|
16
|
-
|
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
|
@@ -6,9 +6,18 @@ module Floe
|
|
6
6
|
class Succeed < Floe::Workflow::State
|
7
7
|
attr_reader :input_path, :output_path
|
8
8
|
|
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
|
+
|
9
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
|
|
@@ -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", "$"))
|
@@ -30,8 +33,6 @@ module Floe
|
|
30
33
|
@credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
|
31
34
|
|
32
35
|
validate_state!(workflow)
|
33
|
-
rescue ArgumentError => err
|
34
|
-
raise Floe::InvalidWorkflowError, err.message
|
35
36
|
end
|
36
37
|
|
37
38
|
def start(context)
|
@@ -82,11 +83,11 @@ module Floe
|
|
82
83
|
end
|
83
84
|
|
84
85
|
def find_retrier(error)
|
85
|
-
self.retry.detect { |r|
|
86
|
+
self.retry.detect { |r| r.match_error?(error) }
|
86
87
|
end
|
87
88
|
|
88
89
|
def find_catcher(error)
|
89
|
-
self.catch.detect { |c|
|
90
|
+
self.catch.detect { |c| c.match_error?(error) }
|
90
91
|
end
|
91
92
|
|
92
93
|
def retry_state!(context, error)
|
@@ -106,7 +107,7 @@ module Floe
|
|
106
107
|
wait_until!(context, :seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
|
107
108
|
context.next_state = context.state_name
|
108
109
|
context.output = error
|
109
|
-
logger.info("Running state: [#{long_name}] with input [#{context.
|
110
|
+
logger.info("Running state: [#{long_name}] with input [#{context.json_input}] got error[#{context.json_output}]...Retry - delay: #{wait_until(context)}")
|
110
111
|
true
|
111
112
|
end
|
112
113
|
|
@@ -116,7 +117,7 @@ module Floe
|
|
116
117
|
|
117
118
|
context.next_state = catcher.next
|
118
119
|
context.output = catcher.result_path.set(context.input, error)
|
119
|
-
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}]")
|
120
121
|
|
121
122
|
true
|
122
123
|
end
|
@@ -126,7 +127,7 @@ module Floe
|
|
126
127
|
# keeping in here for completeness
|
127
128
|
context.next_state = nil
|
128
129
|
context.output = error
|
129
|
-
logger.error("Running state: [#{long_name}] with input [#{context.
|
130
|
+
logger.error("Running state: [#{long_name}] with input [#{context.json_input}]...Complete workflow - output: [#{context.json_output}]")
|
130
131
|
end
|
131
132
|
|
132
133
|
def parse_error(output)
|