diff --git a/lib/floe/workflow/choice_rule/data.rb b/lib/floe/workflow/choice_rule/data.rb index ef67f623..422fb11b 100644 --- a/lib/floe/workflow/choice_rule/data.rb +++ b/lib/floe/workflow/choice_rule/data.rb @@ -4,12 +4,12 @@ module Floe class Workflow class ChoiceRule class Data < Floe::Workflow::ChoiceRule - TYPES = ["String", "Numeric", "Boolean", "Timestamp", "Present", "Null"].freeze + TYPES = {"String" => :is_string?, "Numeric" => :is_numeric?, "Boolean" => :is_boolean?, "Timestamp" => :is_timestamp?, "Present" => :is_present?, "Null" => :is_null?}.freeze COMPARES = ["Equals", "LessThan", "GreaterThan", "LessThanEquals", "GreaterThanEquals", "Matches"].freeze # e.g.: (Is)(String), (Is)(Present) - TYPE_CHECK = /^(Is)(#{TYPES.join("|")})$/.freeze + TYPE_CHECK = /^(Is)(#{TYPES.keys.join("|")})$/.freeze # e.g.: (String)(LessThan)(Path), (Numeric)(GreaterThanEquals)() - OPERATION = /^(#{(TYPES - %w[Null Present]).join("|")})(#{COMPARES.join("|")})(Path)?$/.freeze + OPERATION = /^(#{(TYPES.keys - %w[Null Present]).join("|")})(#{COMPARES.join("|")})(Path)?$/.freeze attr_reader :variable, :compare_key, :type, :compare_predicate, :path @@ -135,18 +135,18 @@ def parse_compare_key(payload) # parse predicate at initilization time # @return the right predicate attached to the compare key def parse_predicate(payload) - path ? parse_path(compare_key, payload) : payload[compare_key] + path ? parse_path(compare_key, payload) : parse_value(compare_key, payload) end # @return right hand predicate - input path or static payload value) def compare_value(context, input) - path ? compare_predicate.value(context, input) : compare_predicate + path ? fetch_path(compare_key, compare_predicate, context, input) : compare_predicate end # feth the variable value at runtime # @return variable value (left hand side ) def variable_value(context, input) - variable.value(context, input) + fetch_path("Variable", variable, context, input) end # parse path at initilization time @@ -156,6 +156,24 @@ def parse_path(field_name, payload) missing_field_error!(field_name) unless value wrap_parser_error(field_name, value) { Path.new(value) } end + + def parse_value(field_name, payload) + value = payload[field_name] + invalid_field_error!(field_name, value, "required to be a #{type || "Boolean"}") unless correct_type?(value) + value + end + + # fetch a path at runtime + # @ the value at a path + def fetch_path(field_name, field_path, context, input) + ret_value = field_path.value(context, input) + runtime_field_error!(field_name, field_path.to_s, "required to point to a #{type}") if type && !correct_type?(ret_value) + ret_value + end + + def correct_type?(val) + send(TYPES[type || "Boolean"], val) + end end end end diff --git a/spec/workflow/choice_rule_spec.rb b/spec/workflow/choice_rule_spec.rb index edcbf037..b879e9c7 100644 --- a/spec/workflow/choice_rule_spec.rb +++ b/spec/workflow/choice_rule_spec.rb @@ -155,6 +155,13 @@ end end + context "with an invalid predicate" do + let(:choices) { [{"Variable" => "$.foo", "IsNull" => 5, "Next" => "FirstMatchState"}] } + it "fails" do + expect { subject }.to raise_exception(Floe::InvalidWorkflowError, "States.Choice1.Choices.0.Data field \"IsNull\" value \"5\" required to be a Boolean") + end + end + context "with IsNull" do let(:predicate) { true } let(:choices) { [{"Variable" => "$.foo", "IsNull" => predicate, "Next" => "FirstMatchState"}] } @@ -384,6 +391,16 @@ end end + context "with wrong match type on the left" do + let(:input) { {"foo" => "abc", "bar" => 2} } + it { expect { subject }.to raise_exception(Floe::ExecutionError, "States.Choice1.Choices.0.Data field \"Variable\" value \"$.foo\" required to point to a Numeric") } + end + + context "with wrong match type on the right" do + let(:input) { {"foo" => 2, "bar" => "xyz"} } + it { expect { subject }.to raise_exception(Floe::ExecutionError, "States.Choice1.Choices.0.Data field \"NumericEqualsPath\" value \"$.bar\" required to point to a Numeric") } + end + context "with path not found" do let(:input) { {"foo" => 2} } it { expect { subject }.to raise_error(Floe::PathError, "Path [$.bar] references an invalid value") } @@ -468,6 +485,12 @@ expect(subject).to eq(false) end end + + context "with invalid Path" do + let(:choices) { [{"Variable" => "$.foo", "NumericGreaterThanPath" => "bogus", "Next" => "FirstMatchState"}] } + let(:input) { {} } + it { expect { subject }.to raise_exception(Floe::InvalidWorkflowError, "States.Choice1.Choices.0.Data field \"NumericGreaterThanPath\" value \"bogus\" Path [bogus] must start with \"$\"") } + end end context "with a NumericLessThanEquals" do @@ -555,18 +578,22 @@ context "that is true" do let(:input) { {"foo" => "audit.log"} } - - it "returns true" do - expect(subject).to eq(true) - end + it { expect(subject).to eq(true) } end context "that is false" do let(:input) { {"foo" => "audit"} } + it { expect(subject).to eq(false) } + end - it "returns false" do - expect(subject).to eq(false) - end + context "that does not exist" do + let(:input) { {} } + it { expect { subject }.to raise_exception(Floe::PathError, "Path [$.foo] references an invalid value") } + end + + context "that references a number" do + let(:input) { {"foo" => 5} } + it { expect { subject }.to raise_exception(Floe::ExecutionError, "States.Choice1.Choices.0.Data field \"Variable\" value \"$.foo\" required to point to a String") } end end end