floe 0.11.3 → 0.13.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/.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)
|