diff --git a/src/spectator/dsl/matcher_dsl.cr b/src/spectator/dsl/matcher_dsl.cr index 4136c27..665e44f 100644 --- a/src/spectator/dsl/matcher_dsl.cr +++ b/src/spectator/dsl/matcher_dsl.cr @@ -579,9 +579,11 @@ module Spectator::DSL {% if call.name.starts_with?("be_") %} # Remove `be_` prefix. {% method_name = call.name[3..-1] %} + {% matcher = "PredicateMatcher" %} {% elsif call.name.starts_with?("have_") %} - # Swap `have_` with `has_`. - {% method_name = ("has_" + call.name[5..-1].stringify).id %} + # Remove `have_` prefix. + {% method_name = call.name[5..-1] %} + {% matcher = "HavePredicateMatcher" %} {% else %} {% raise "Undefined local variable or method '#{call}'" %} {% end %} @@ -598,7 +600,7 @@ module Spectator::DSL {% end %} label << ')' {% end %} - ::Spectator::Matchers::PredicateMatcher.new(descriptor, label.to_s) + ::Spectator::Matchers::{{matcher.id}}.new(descriptor, label.to_s) end end end diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr new file mode 100644 index 0000000..bba32ae --- /dev/null +++ b/src/spectator/matchers/have_predicate_matcher.cr @@ -0,0 +1,64 @@ +require "./value_matcher" + +module Spectator::Matchers + # Matcher that tests one or more "has" predicates + # (methods ending in '?' and starting with 'has_'). + # The `ExpectedType` type param should be a `NamedTuple`. + # Each key in the tuple is a predicate (without the '?' and 'has_' prefix) to test. + # Each value is a a `Tuple` of arguments to pass to the predicate method. + struct HavePredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType) + # Determines whether the matcher is satisfied with the value given to it. + private def match?(values) + # Test each predicate and immediately return false if one is false. + {% for attribute in ExpectedType.keys %} + return false unless values[{{attribute.symbolize}}] + {% end %} + + # All checks passed if this point is reached. + true + end + + # Determines whether the matcher is satisfied with the partial given to it. + # `MatchData` is returned that contains information about the match. + def match(partial) : MatchData + values = snapshot_values(partial.actual) + MatchData.new(match?(values), values, partial.label, label) + end + + # Captures all of the actual values. + # A `NamedTuple` is returned, + # with each key being the attribute. + private def snapshot_values(actual) + {% begin %} + { + {% for attribute in ExpectedType.keys %} + {{attribute}}: actual.has_{{attribute}}?(*@expected[{{attribute.symbolize}}]), + {% end %} + } + {% end %} + end + + # Match data specific to this matcher. + private struct MatchData(ActualType) < MatchData + # Creates the match data. + def initialize(matched, @named_tuple : ActualType, @actual_label : String, @expected_label : String) + super(matched) + end + + # Information about the match. + getter named_tuple + + # Describes the condition that satisfies the matcher. + # This is informational and displayed to the end-user. + def message + "#{@actual_label} has #{@expected_label}" + end + + # Describes the condition that won't satsify the matcher. + # This is informational and displayed to the end-user. + def negated_message + "#{@actual_label} does not have #{@expected_label}" + end + end + end +end