floe 0.11.3 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- results.count < 2 ? results.first : results
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
- value.start_with?("$") ? Path.new(value) : value
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 then interpolate_value_array(value, context, inputs)
51
- when Hash then interpolate_value_hash(value, context, inputs)
52
- when Path then value.value(context, inputs)
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
- attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate
6
+ include ErrorMatcherMixin
7
+ include ValidationMixin
7
8
 
8
- def initialize(payload)
9
- @payload = payload
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
@@ -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
- raise Floe::InvalidWorkflowError, "Missing \"Type\" field in state [#{name}]" if payload["Type"].nil?
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
- raise Floe::InvalidWorkflowError, "Invalid state type: [#{state_type}]"
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(workflow, name, payload)
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.input}]...")
66
+ logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
58
67
  end
59
68
 
60
- def finish(context)
61
- finished_time = Time.now.utc
62
- entered_time = Time.parse(context.state["EnteredTime"])
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.output&.[]("Error") ? :error : :info
68
- 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}]")
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
- "#{@type}:#{name}"
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.parse(context.state["EnteredTime"]) + seconds).iso8601
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
- output = output_path.value(context, context.input)
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
- raise Floe::InvalidWorkflowError, "Choice state must have \"Choices\"" unless payload.key?("Choices")
47
- raise Floe::InvalidWorkflowError, "\"Choices\" must be a non-empty array" unless payload["Choices"].kind_of?(Array) && !payload["Choices"].empty?
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
- raise Floe::InvalidWorkflowError, "\"Default\" not in \"States\"" unless workflow.payload["States"].include?(payload["Default"])
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
@@ -12,8 +12,8 @@ module Floe
12
12
  end
13
13
 
14
14
  def validate_state_next!(workflow)
15
- raise Floe::InvalidWorkflowError, "Missing \"Next\" field in state [#{name}]" if @next.nil? && !@end
16
- raise Floe::InvalidWorkflowError, "\"Next\" [#{@next}] not in \"States\" for state [#{name}]" if @next && !workflow.payload["States"].key?(@next)
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
@@ -25,7 +25,8 @@ module Floe
25
25
  end
26
26
 
27
27
  def finish(context)
28
- context.output = process_output(context, result)
28
+ input = result.nil? ? process_input(context) : result
29
+ context.output = process_output(context, input)
29
30
  super
30
31
  end
31
32
 
@@ -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
- context.output = context.input
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
- @runner = Floe::Runner.for_resource(@resource)
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| (r.error_equals & [error, "States.ALL"]).any? }
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| (c.error_equals & [error, "States.ALL"]).any? }
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.input}] got error[#{context.output}]...Retry - delay: #{wait_until(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.input}]...CatchError - next state: [#{context.next_state}] output: [#{context.output}]")
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.input}]...Complete workflow - output: [#{context.output}]")
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)