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.
@@ -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)