Skip to content

Commit b187689

Browse files
committed
Better Runtime error detection for bad paths
1 parent 50bc558 commit b187689

File tree

17 files changed

+205
-52
lines changed

17 files changed

+205
-52
lines changed

lib/floe.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ module Floe
4343
class Error < StandardError; end
4444
class InvalidWorkflowError < Error; end
4545
class InvalidExecutionInput < Error; end
46+
class PathError < Error; end
47+
class ExecutionError < Error; end
4648

4749
def self.logger
4850
@logger ||= NullLogger.new

lib/floe/validation_mixin.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ def self.included(base)
77
end
88

99
def parser_error!(comment)
10-
self.class.parser_error!(name, comment)
10+
raise Floe::InvalidWorkflowError, "#{self.class.location_name_str(name)} #{comment}"
1111
end
1212

1313
def missing_field_error!(field_name)
@@ -18,6 +18,16 @@ def invalid_field_error!(field_name, field_value = nil, comment = nil)
1818
self.class.invalid_field_error!(name, field_name, field_value, comment)
1919
end
2020

21+
def runtime_field_error!(field_name, field_value, comment)
22+
raise Floe::ExecutionError, self.class.field_error_text(name, field_name, field_value, comment)
23+
end
24+
25+
def wrap_runtime_error(field_name, field_value)
26+
yield
27+
rescue Floe::PathError => e
28+
runtime_field_error!(field_name, field_value, e.message)
29+
end
30+
2131
def workflow_state?(field_value, workflow)
2232
workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
2333
end
@@ -29,20 +39,25 @@ def wrap_parser_error(field_name, field_value)
2939
end
3040

3141
module ClassMethods
32-
def parser_error!(name, comment)
33-
name = name.join(".") if name.kind_of?(Array)
34-
raise Floe::InvalidWorkflowError, "#{name} #{comment}"
42+
def location_name_str(name)
43+
name.kind_of?(Array) ? name.join(".") : name
3544
end
3645

46+
# these are here for State.build!
47+
3748
def missing_field_error!(name, field_name)
38-
parser_error!(name, "does not have required field \"#{field_name}\"")
49+
raise Floe::InvalidWorkflowError, "#{location_name_str(name)} does not have required field \"#{field_name}\""
3950
end
4051

4152
def invalid_field_error!(name, field_name, field_value, comment)
53+
raise Floe::InvalidWorkflowError, field_error_text(name, field_name, field_value, comment)
54+
end
55+
56+
def field_error_text(name, field_name, field_value, comment = nil)
4257
# instead of displaying a large hash or array, just displaying the word Hash or Array
4358
field_value = field_value.class if field_value.kind_of?(Hash) || field_value.kind_of?(Array)
4459

45-
parser_error!(name, "field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}")
60+
"#{location_name_str(name)} field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}"
4661
end
4762
end
4863
end

lib/floe/workflow/choice_rule/data.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ class Workflow
55
class ChoiceRule
66
class Data < Floe::Workflow::ChoiceRule
77
def true?(context, input)
8+
return presence_check(context, input) if compare_key == "IsPresent"
9+
810
lhs = variable_value(context, input)
911
rhs = compare_value(context, input)
1012

11-
validate!(lhs)
12-
1313
case compare_key
1414
when "IsNull" then is_null?(lhs)
15-
when "IsPresent" then is_present?(lhs)
1615
when "IsNumeric" then is_numeric?(lhs)
1716
when "IsString" then is_string?(lhs)
1817
when "IsBoolean" then is_boolean?(lhs)
@@ -47,8 +46,17 @@ def true?(context, input)
4746

4847
private
4948

50-
def validate!(value)
51-
raise "No such variable [#{variable}]" if value.nil? && !%w[IsNull IsPresent].include?(compare_key)
49+
def presence_check(context, input)
50+
rhs = compare_value(context, input)
51+
# don't need the value, just need to see if the path finds the value
52+
variable_value(context, input)
53+
54+
# path found the variable_value, (so if they said true, return true)
55+
rhs
56+
rescue Floe::ExecutionError
57+
# variable_value (path) threw an error
58+
# it was not found (so if they said false, return true)
59+
!rhs
5260
end
5361

5462
def is_null?(value) # rubocop:disable Naming/PredicateName

lib/floe/workflow/path.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ def value(context, input = {})
3434
return obj if path == "$"
3535

3636
results = JsonPath.on(obj, path)
37-
results.count < 2 ? results.first : results
37+
case results.count
38+
when 0
39+
raise Floe::PathError, "references an invalid value"
40+
when 1
41+
results.first
42+
else
43+
results
44+
end
3845
end
3946

4047
def to_s

lib/floe/workflow/state.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,35 @@ def wait(context, timeout: nil)
4545
# @return for incomplete Errno::EAGAIN, for completed 0
4646
def run_nonblock!(context)
4747
start(context) unless context.state_started?
48+
4849
return Errno::EAGAIN unless ready?(context)
4950

5051
finish(context)
52+
rescue Floe::ExecutionError => e
53+
# input / output paths were bad
54+
context.next_state = nil
55+
context.output = {"Error" => "States.RuntimeError", "Cause" => e.message}
56+
# finish.super was never called
57+
mark_finished(context)
5158
end
5259

5360
def start(context)
61+
mark_started(context)
62+
end
63+
64+
def finish(context)
65+
mark_finished(context)
66+
end
67+
68+
def mark_started(context)
5469
context.state["EnteredTime"] = Time.now.utc.iso8601
5570

5671
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...")
5772
end
5873

59-
def finish(context)
60-
finished_time = Time.now.utc
61-
entered_time = Time.parse(context.state["EnteredTime"])
74+
def mark_finished(context)
75+
finished_time = Time.now.utc
76+
entered_time = Time.parse(context.state["EnteredTime"])
6277

6378
context.state["FinishedTime"] ||= finished_time.iso8601
6479
context.state["Duration"] = finished_time - entered_time

lib/floe/workflow/states/choice.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ def initialize(workflow, name, payload)
1919
end
2020

2121
def finish(context)
22-
input = input_path.value(context, context.input)
23-
output = output_path.value(context, input)
22+
input = wrap_runtime_error("InputPath", input_path.to_s) { input_path.value(context, context.input) }
23+
output = wrap_runtime_error("OutputPath", output_path.to_s) { output_path.value(context, input) }
24+
# For a bad path, throws an ExecutionError
2425
next_state = choices.detect { |choice| choice.true?(context, output) }&.next || default
2526

2627
context.next_state = next_state
2728
context.output = output
29+
2830
super
2931
end
3032

lib/floe/workflow/states/fail.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Floe
44
class Workflow
55
module States
66
class Fail < Floe::Workflow::State
7-
attr_reader :cause, :error
7+
attr_reader :cause, :error, :cause_path, :error_path
88

99
def initialize(workflow, name, payload)
1010
super
@@ -21,8 +21,8 @@ def finish(context)
2121
# see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html
2222
# https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html#asl-intrsc-func-generic
2323
context.output = {
24-
"Error" => @error_path ? @error_path.value(context, context.input) : error,
25-
"Cause" => @cause_path ? @cause_path.value(context, context.input) : cause
24+
"Error" => error_path ? wrap_runtime_error("ErrorPath", error_path.to_s) { @error_path.value(context, context.input) } : error,
25+
"Cause" => cause_path ? wrap_runtime_error("CausePath", cause_path.to_s) { @cause_path.value(context, context.input) } : cause
2626
}.compact
2727
super
2828
end

lib/floe/workflow/states/input_output_mixin.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@ class Workflow
55
module States
66
module InputOutputMixin
77
def process_input(context)
8-
input = input_path.value(context, context.input)
9-
input = parameters.value(context, input) if parameters
8+
input = wrap_runtime_error("InputPath", input_path.to_s) { input_path.value(context, context.input) }
9+
input = wrap_runtime_error("Parameters", parameters.to_s) { parameters.value(context, input) } if parameters
1010
input
1111
end
1212

1313
def process_output(context, results)
1414
return context.input.dup if results.nil?
1515
return if output_path.nil?
1616

17-
results = result_selector.value(context, results) if @result_selector
17+
results = wrap_runtime_error("ResultSelector", @result_selector.to_s) { result_selector.value(context, results) } if @result_selector
1818
if result_path.payload.start_with?("$.Credentials")
19-
credentials = result_path.set(context.credentials, results)["Credentials"]
19+
credentials = wrap_runtime_error("ResultPath", result_path.to_s) { result_path.set(context.credentials, results)["Credentials"] }
2020
context.credentials.merge!(credentials)
2121
output = context.input.dup
2222
else
2323
output = result_path.set(context.input.dup, results)
2424
end
2525

26-
output_path.value(context, output)
26+
wrap_runtime_error("OutputPath", output_path.to_s) { output_path.value(context, output) }
2727
end
2828
end
2929
end

lib/floe/workflow/states/succeed.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def initialize(workflow, name, payload)
1414
end
1515

1616
def finish(context)
17-
input = input_path.value(context, context.input)
18-
context.output = output_path.value(context, input)
17+
input = wrap_runtime_error("InputPath", input_path.to_s) { input_path.value(context, context.input) }
18+
context.output = wrap_runtime_error("OutputPath", output_path.to_s) { output_path.value(context, input) }
1919
context.next_state = nil
2020

2121
super

lib/floe/workflow/states/task.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def catch_error!(context, error)
116116
return if catcher.nil?
117117

118118
context.next_state = catcher.next
119-
context.output = catcher.result_path.set(context.input, error)
119+
context.output = wrap_runtime_error("ResultPath", result_path.to_s) { catcher.result_path.set(context.input, error) }
120120
logger.info("Running state: [#{long_name}] with input [#{context.json_input}]...CatchError - next state: [#{context.next_state}] output: [#{context.json_output}]")
121121

122122
true

0 commit comments

Comments
 (0)