respond_matcher.cr 2.45 KB
Newer Older
Mike Miller's avatar
Mike Miller committed
1
2
3
4
5
6
7
8
require "./value_matcher"

module Spectator::Matchers
  # Matcher that tests that a type responds to a method call.
  # The instance is tested with the `responds_to?` method.
  # The `ExpectedType` type param should be a `NamedTuple`,
  # with each key being the method to check and the value is ignored.
  struct RespondMatcher(ExpectedType) < Matcher
Mike Miller's avatar
Mike Miller committed
9
10
11
    # Short text about the matcher's purpose.
    # This explains what condition satisfies the matcher.
    # The description is used when the one-liner syntax is used.
Mike Miller's avatar
Mike Miller committed
12
    def description : String
13
14
15
      "responds to #{label}"
    end

Mike Miller's avatar
Mike Miller committed
16
    # Actually performs the test against the expression.
17
    def match(actual : Expression(T)) : MatchData forall T
18
      snapshot = snapshot_values(actual.value)
19
      if snapshot.values.all?
20
        SuccessfulMatchData.new(match_data_description(actual))
21
      else
22
        FailedMatchData.new(match_data_description(actual), "#{actual.label} does not respond to #{label}", values(snapshot).to_a)
23
24
25
      end
    end

Mike Miller's avatar
Mike Miller committed
26
27
    # Performs the test against the expression, but inverted.
    # A successful match with `#match` should normally fail for this method, and vice-versa.
28
    def negated_match(actual : Expression(T)) : MatchData forall T
29
      snapshot = snapshot_values(actual.value)
Mike Miller's avatar
Mike Miller committed
30
31
      # Intentionally check truthiness of each value.
      if snapshot.values.any? # ameba:disable Performance/AnyInsteadOfEmpty
32
        FailedMatchData.new(match_data_description(actual), "#{actual.label} responds to #{label}", values(snapshot).to_a)
33
      else
34
        SuccessfulMatchData.new(match_data_description(actual))
35
      end
Mike Miller's avatar
Mike Miller committed
36
37
38
    end

    # Captures all of the actual values.
39
40
    # A `NamedTuple` is returned, with each key being the attribute.
    private def snapshot_values(object)
Mike Miller's avatar
Mike Miller committed
41
42
      {% begin %}
      {
43
44
        {% for attribute in ExpectedType.keys %}
        {{attribute}}: object.responds_to?({{attribute.symbolize}}),
Mike Miller's avatar
Mike Miller committed
45
46
47
48
49
        {% end %}
      }
      {% end %}
    end

Mike Miller's avatar
Mike Miller committed
50
    # Produces the tuple for the failed match data from a snapshot of the results.
Mike Miller's avatar
Mike Miller committed
51
52
53
54
55
56
57
58
59
60
    private def values(snapshot)
      {% begin %}
      {
        {% for attribute in ExpectedType.keys %}
        {{attribute}}: snapshot[{{attribute.symbolize}}].inspect,
        {% end %}
      }
      {% end %}
    end

61
62
63
64
    # Generated, user-friendly, string for the expected value.
    private def label
      # Prefix every method name with # and join them with commas.
      {{ExpectedType.keys.map { |e| "##{e}".id }.splat.stringify}}
Mike Miller's avatar
Mike Miller committed
65
66
67
    end
  end
end