matchers 0.1.0.pre.test

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.

Potentially problematic release.


This version of matchers might be problematic. Click here for more details.

Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +21 -0
  3. data/lib/matcher/base.rb +189 -0
  4. data/lib/matcher/builder.rb +74 -0
  5. data/lib/matcher/chain.rb +60 -0
  6. data/lib/matcher/debug.rb +48 -0
  7. data/lib/matcher/errors/and_error.rb +86 -0
  8. data/lib/matcher/errors/boolean_collector.rb +51 -0
  9. data/lib/matcher/errors/element_error.rb +24 -0
  10. data/lib/matcher/errors/empty_error.rb +23 -0
  11. data/lib/matcher/errors/error.rb +39 -0
  12. data/lib/matcher/errors/error_collector.rb +99 -0
  13. data/lib/matcher/errors/nested_error.rb +96 -0
  14. data/lib/matcher/errors/or_error.rb +86 -0
  15. data/lib/matcher/expression_cache.rb +57 -0
  16. data/lib/matcher/expression_labeler.rb +91 -0
  17. data/lib/matcher/expressions/array_expression.rb +45 -0
  18. data/lib/matcher/expressions/block.rb +153 -0
  19. data/lib/matcher/expressions/call.rb +338 -0
  20. data/lib/matcher/expressions/call_error.rb +45 -0
  21. data/lib/matcher/expressions/constant.rb +53 -0
  22. data/lib/matcher/expressions/expression.rb +147 -0
  23. data/lib/matcher/expressions/expression_building.rb +258 -0
  24. data/lib/matcher/expressions/expression_walker.rb +73 -0
  25. data/lib/matcher/expressions/hash_expression.rb +59 -0
  26. data/lib/matcher/expressions/proc_expression.rb +92 -0
  27. data/lib/matcher/expressions/range_expression.rb +58 -0
  28. data/lib/matcher/expressions/recorder.rb +86 -0
  29. data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
  30. data/lib/matcher/expressions/set_expression.rb +45 -0
  31. data/lib/matcher/expressions/string_expression.rb +53 -0
  32. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  33. data/lib/matcher/expressions/variable.rb +85 -0
  34. data/lib/matcher/hash_stack.rb +53 -0
  35. data/lib/matcher/list.rb +102 -0
  36. data/lib/matcher/markers/optional.rb +80 -0
  37. data/lib/matcher/markers/others.rb +28 -0
  38. data/lib/matcher/matcher_cache.rb +18 -0
  39. data/lib/matcher/matchers/all_matcher.rb +60 -0
  40. data/lib/matcher/matchers/always_matcher.rb +28 -0
  41. data/lib/matcher/matchers/any_matcher.rb +70 -0
  42. data/lib/matcher/matchers/array_matcher.rb +35 -0
  43. data/lib/matcher/matchers/block_matcher.rb +59 -0
  44. data/lib/matcher/matchers/boolean_matcher.rb +35 -0
  45. data/lib/matcher/matchers/dig_matcher.rb +146 -0
  46. data/lib/matcher/matchers/each_matcher.rb +52 -0
  47. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  48. data/lib/matcher/matchers/equal_matcher.rb +197 -0
  49. data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
  50. data/lib/matcher/matchers/expression_matcher.rb +73 -0
  51. data/lib/matcher/matchers/filter_matcher.rb +111 -0
  52. data/lib/matcher/matchers/hash_matcher.rb +223 -0
  53. data/lib/matcher/matchers/imply_matcher.rb +81 -0
  54. data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
  55. data/lib/matcher/matchers/index_by_matcher.rb +175 -0
  56. data/lib/matcher/matchers/inline_matcher.rb +99 -0
  57. data/lib/matcher/matchers/keys_matcher.rb +121 -0
  58. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  59. data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
  60. data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
  61. data/lib/matcher/matchers/let_matcher.rb +73 -0
  62. data/lib/matcher/matchers/map_matcher.rb +129 -0
  63. data/lib/matcher/matchers/matcher_building.rb +5 -0
  64. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  65. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  66. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  67. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  68. data/lib/matcher/matchers/negated_matcher.rb +23 -0
  69. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  70. data/lib/matcher/matchers/never_matcher.rb +29 -0
  71. data/lib/matcher/matchers/one_matcher.rb +70 -0
  72. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  73. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  74. data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
  75. data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
  76. data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
  77. data/lib/matcher/matchers/project_matcher.rb +68 -0
  78. data/lib/matcher/matchers/raises_matcher.rb +124 -0
  79. data/lib/matcher/matchers/range_matcher.rb +47 -0
  80. data/lib/matcher/matchers/reference_matcher.rb +111 -0
  81. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  82. data/lib/matcher/matchers/regexp_matcher.rb +84 -0
  83. data/lib/matcher/messages/expected_phrasing.rb +342 -0
  84. data/lib/matcher/messages/message.rb +102 -0
  85. data/lib/matcher/messages/message_builder.rb +35 -0
  86. data/lib/matcher/messages/message_rules.rb +223 -0
  87. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  88. data/lib/matcher/messages/phrasing.rb +57 -0
  89. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  90. data/lib/matcher/once_before.rb +18 -0
  91. data/lib/matcher/optional_chain.rb +24 -0
  92. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  93. data/lib/matcher/patterns/capture_hole.rb +33 -0
  94. data/lib/matcher/patterns/constant_hole.rb +14 -0
  95. data/lib/matcher/patterns/hole.rb +30 -0
  96. data/lib/matcher/patterns/method_hole.rb +58 -0
  97. data/lib/matcher/patterns/pattern.rb +92 -0
  98. data/lib/matcher/patterns/pattern_building.rb +39 -0
  99. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  100. data/lib/matcher/patterns/pattern_match.rb +29 -0
  101. data/lib/matcher/patterns/variable_hole.rb +14 -0
  102. data/lib/matcher/reporter.rb +98 -0
  103. data/lib/matcher/rules/message_factory.rb +25 -0
  104. data/lib/matcher/rules/message_rule.rb +18 -0
  105. data/lib/matcher/rules/message_rule_context.rb +24 -0
  106. data/lib/matcher/rules/rule_builder.rb +29 -0
  107. data/lib/matcher/rules/rule_set.rb +57 -0
  108. data/lib/matcher/rules/transform_builder.rb +24 -0
  109. data/lib/matcher/rules/transform_mapping.rb +5 -0
  110. data/lib/matcher/rules/transform_rule.rb +21 -0
  111. data/lib/matcher/state.rb +40 -0
  112. data/lib/matcher/testing/error_builder.rb +62 -0
  113. data/lib/matcher/testing/error_checker.rb +496 -0
  114. data/lib/matcher/testing/error_testing.rb +37 -0
  115. data/lib/matcher/testing/pattern_testing.rb +11 -0
  116. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  117. data/lib/matcher/testing.rb +102 -0
  118. data/lib/matcher/undefined.rb +10 -0
  119. data/lib/matcher/utils/mapping_utils.rb +61 -0
  120. data/lib/matcher/utils.rb +72 -0
  121. data/lib/matcher/version.rb +5 -0
  122. data/lib/matcher.rb +337 -0
  123. metadata +167 -0
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class NamespacedMessageBuilder < MessageBuilder
5
+ def initialize(negated, actual, namespace)
6
+ super(negated, actual)
7
+
8
+ @namespace = namespace
9
+ end
10
+
11
+ protected
12
+
13
+ def message(key, *, **)
14
+ key = [@namespace, key]
15
+
16
+ super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Phrasing
5
+ extend Forwardable
6
+
7
+ attr_reader :path, :message
8
+ def_delegator :@message, :actual
9
+ def_delegator :@message, :negated
10
+
11
+ def self.namespace(value)
12
+ @namespace = value
13
+ yield
14
+ ensure
15
+ @namespace = nil
16
+ end
17
+
18
+ def self.define(key, &block)
19
+ key = [@namespace, key] if @namespace
20
+
21
+ dict[key] = block
22
+ end
23
+
24
+ def self.dict
25
+ @dict ||= {}
26
+ end
27
+
28
+ def self.phrasing
29
+ ->(path, message) { new(path, message).apply }
30
+ end
31
+
32
+ def initialize(path, message)
33
+ @path = path
34
+ @message = message
35
+ end
36
+
37
+ def apply
38
+ phrase(@message.key, *@message.args, **@message.kwargs)
39
+ end
40
+
41
+ def phrase(key, *, **)
42
+ block = self.class.dict[key]
43
+
44
+ if block
45
+ instance_exec(*, **, &block)
46
+ else
47
+ "got #{actual.inspect} but found no message for #{'*NOT* ' if negated}#{key.inspect}"#{" (negated)" if negated}
48
+ end
49
+ end
50
+
51
+ def phrase_negated(key, *, **)
52
+ message = Message.new(key, !negated, actual, *, **)
53
+
54
+ self.class.new(@path, message).apply
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class StandardMessageBuilder < MessageBuilder
5
+ def namespace(namespace)
6
+ NamespacedMessageBuilder.new(@negated, @actual, namespace)
7
+ end
8
+
9
+ def truthy
10
+ message(:truthy)
11
+ end
12
+
13
+ def same(object)
14
+ message(:same, object)
15
+ end
16
+
17
+ def equal(value)
18
+ message(:equal, value)
19
+ end
20
+
21
+ def less_than(operand)
22
+ message(:less_than, operand)
23
+ end
24
+
25
+ def greater_than(operand)
26
+ message(:greater_than, operand)
27
+ end
28
+
29
+ def less_than_or_equal(operand)
30
+ message(:less_than_or_equal, operand)
31
+ end
32
+
33
+ def greater_than_or_equal(operand)
34
+ message(:greater_than_or_equal, operand)
35
+ end
36
+
37
+ def comparable_to(operand)
38
+ message(:comparable_to, operand)
39
+ end
40
+
41
+ def between(min, max, exclude_end: false)
42
+ message(:between, min, max, exclude_end:)
43
+ end
44
+
45
+ def length_of(exp, act)
46
+ message(:length_of, exp, act)
47
+ end
48
+
49
+ def having_key(key)
50
+ message(:having_key, key)
51
+ end
52
+
53
+ def having_index(index)
54
+ message(:having_index, index)
55
+ end
56
+
57
+ def exist
58
+ message(:exist)
59
+ end
60
+
61
+ def in(collection)
62
+ message(:in, collection)
63
+ end
64
+
65
+ def including(item)
66
+ message(:including, item)
67
+ end
68
+
69
+ def duplicate(original_index)
70
+ message(:duplicate, original_index)
71
+ end
72
+
73
+ def duplicate_by(expression, value, original_index)
74
+ message(:duplicate_by, expression, value, original_index)
75
+ end
76
+
77
+ def matching(pattern)
78
+ message(:matching, pattern)
79
+ end
80
+
81
+ def valid_format(format)
82
+ message(:valid_format, format)
83
+ end
84
+
85
+ def instance_of(klass)
86
+ message(:instance_of, klass)
87
+ end
88
+
89
+ def kind_of(klass)
90
+ message(:kind_of, klass)
91
+ end
92
+
93
+ def responding_to(method)
94
+ message(:responding_to, method)
95
+ end
96
+
97
+ def predicate(name)
98
+ message(:predicate, name)
99
+ end
100
+
101
+ def described_by(description)
102
+ message(:described_by, description)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module OnceBefore
5
+ def once_before(method, &)
6
+ original_method = "_before_once_#{method}"
7
+ alias_method(original_method, method)
8
+
9
+ define_method(method) do |*args, **kwargs, &block|
10
+ instance_exec(&)
11
+ send(original_method, *args, **kwargs, &block)
12
+ ensure
13
+ self.class.alias_method(method, original_method)
14
+ self.class.undef_method(original_method)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class OptionalChain
5
+ include NoExpression
6
+ include NoKey
7
+ extend Forwardable
8
+
9
+ def initialize(chain, fallback)
10
+ @chain = chain
11
+ @fallback = fallback
12
+ end
13
+
14
+ def_delegator :@chain, :^
15
+
16
+ def ~
17
+ OptionalChain.new(~@chain, @fallback)
18
+ end
19
+
20
+ def fallback
21
+ @chain ^ @fallback
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class AstMapping
5
+ RECEIVER = 0
6
+ ARGS = 1
7
+ KWARGS = 2
8
+
9
+ def initialize(path = List.empty)
10
+ @path = path
11
+ end
12
+
13
+ attr_reader :path
14
+
15
+ def receiver
16
+ @receiver ||= AstMapping.new(@path << RECEIVER)
17
+ end
18
+
19
+ def args
20
+ @args ||= Args.new(@path << ARGS, [])
21
+ end
22
+
23
+ def kwargs
24
+ @kwargs ||= Args.new(@path << KWARGS, {})
25
+ end
26
+
27
+ class Args
28
+ extend Forwardable
29
+
30
+ def initialize(path, data)
31
+ @path = path
32
+ @data = data
33
+ end
34
+
35
+ attr_reader :path, :data
36
+
37
+ def [](index)
38
+ @data[index] ||= AstMapping.new(@path << index)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class CaptureHole < Hole
5
+ def initialize(key, pattern)
6
+ super(key)
7
+
8
+ @pattern = pattern
9
+ end
10
+
11
+ attr_reader :pattern
12
+
13
+ def match?(_expression)
14
+ yield @pattern
15
+
16
+ true
17
+ end
18
+
19
+ def ==(other)
20
+ super && @pattern == other.pattern
21
+ end
22
+ alias eql? ==
23
+
24
+ def hash
25
+ @hash ||= [self.class, @key, @pattern].hash
26
+ end
27
+
28
+ def to_s
29
+ "capture(#{@key.inspect}, #{@pattern})"
30
+ end
31
+ alias inspect to_s
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ConstantHole < Hole
5
+ def match?(expression)
6
+ expression.is_a?(Constant)
7
+ end
8
+
9
+ def to_s
10
+ "const(#{@key.inspect})"
11
+ end
12
+ alias inspect to_s
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Hole
5
+ def initialize(key, filter = nil)
6
+ @key = key
7
+ @filter = filter
8
+ end
9
+
10
+ attr_reader :key
11
+
12
+ def match?(expression)
13
+ !@filter || @filter.call(expression)
14
+ end
15
+
16
+ def ==(other)
17
+ other.instance_of?(self.class) && key == other.key
18
+ end
19
+ alias eql? ==
20
+
21
+ def hash
22
+ [self.class, @key].hash
23
+ end
24
+
25
+ def to_s
26
+ "hole(#{@key.inspect})"
27
+ end
28
+ alias inspect to_s
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MethodHole < Hole
5
+ def initialize(key, receiver, method, args, kwargs)
6
+ super(key)
7
+
8
+ @receiver = receiver
9
+ @method = method
10
+ @args = args
11
+ @kwargs = kwargs
12
+ end
13
+
14
+ attr_reader :receiver, :method, :args, :kwargs
15
+
16
+ def match?(expression)
17
+ return false if !expression.is_a?(Call) || !match_method?(expression.method)
18
+
19
+ yield Call.new(@receiver, expression.method, @args, @kwargs)
20
+
21
+ true
22
+ end
23
+
24
+ def ==(other)
25
+ return true if other.equal?(self)
26
+
27
+ super &&
28
+ @receiver == other.receiver &&
29
+ @method == other.method &&
30
+ @args.eql?(other.receiver) &&
31
+ @kwargs.eql?(other.receiver)
32
+ end
33
+ alias eql? ==
34
+
35
+ def hash
36
+ @hash ||= [self.class, @key, @receiver, @method, @args, @kwargs].hash
37
+ end
38
+
39
+ def to_s
40
+ args = @args.map(&:inspect)
41
+ kwargs = @kwargs.map { "#{_1}: #{_2.inspect}" }
42
+ list = [@key.inspect, @receiver, @method.inspect].concat(args, kwargs)
43
+
44
+ "method_hole(#{list.join(', ')})"
45
+ end
46
+ alias inspect to_s
47
+
48
+ private
49
+
50
+ def match_method?(method)
51
+ if @method.is_a?(Array)
52
+ @method.include?(method)
53
+ else
54
+ @method === method
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Pattern
5
+ class PatternBuilder
6
+ include PatternBuilding
7
+
8
+ def initialize(build_session: Matcher.build_session)
9
+ ExpressionBuilding.init(self, build_session)
10
+ end
11
+ end
12
+
13
+ def self.build(&)
14
+ Matcher.with_build_session do |build_session|
15
+ builder = PatternBuilder.new(build_session:)
16
+ result = builder.instance_exec(&)
17
+ builder.pattern_of(result)
18
+ end
19
+ end
20
+
21
+ extend Forwardable
22
+
23
+ def initialize(expression)
24
+ @expression = expression
25
+ end
26
+
27
+ attr_reader :expression
28
+
29
+ def_delegator :@expression, :to_s
30
+ def_delegator :@expression, :inspect
31
+
32
+ def match(expression, mapping = AstMapping.new)
33
+ result = PatternMatch.new
34
+
35
+ catch(:mismatch) do
36
+ match_helper(expression, @expression, mapping, result)
37
+
38
+ result.capture(:root, expression, mapping) unless result.include?(:root)
39
+
40
+ return result
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def match_helper(expression, pattern, mapping, result)
49
+ if (hole = get_hole(pattern))
50
+ key = hole.key
51
+ capture = result[key]
52
+
53
+ if capture
54
+ throw(:mismatch) if capture.expression != expression
55
+ elsif !hole.match?(expression) { |p| match_helper(expression, p, mapping, result) }
56
+ throw(:mismatch)
57
+ else
58
+ result.capture(key, expression, mapping)
59
+ end
60
+ elsif expression.is_a?(Call)
61
+ throw(:mismatch) unless similar_call?(expression, pattern)
62
+
63
+ match_helper(expression.receiver, pattern.receiver, mapping.receiver, result)
64
+
65
+ expression.args.each_index do |i|
66
+ match_helper(expression.args[i], pattern.args[i], mapping.args[i], result)
67
+ end
68
+
69
+ expression.kwargs.each_key do |k|
70
+ match_helper(expression.kwargs[k], pattern.kwargs[k], mapping.kwargs[k], result)
71
+ end
72
+ elsif expression != pattern
73
+ throw(:mismatch)
74
+ end
75
+ end
76
+
77
+ def get_hole(expression)
78
+ return unless expression.is_a?(Constant)
79
+
80
+ constant = expression.value
81
+ constant if constant.is_a?(Hole)
82
+ end
83
+
84
+ def similar_call?(expression, pattern)
85
+ expression.class == pattern.class &&
86
+ expression.method == pattern.method &&
87
+ expression.args.length == pattern.args.length &&
88
+ expression.kwargs.size == pattern.kwargs.size &&
89
+ expression.kwargs.keys.sort == pattern.kwargs.keys.sort
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module PatternBuilding
5
+ include ExpressionBuilding
6
+
7
+ def pattern_of(value)
8
+ Pattern.new(expression_of(value))
9
+ end
10
+
11
+ def capture(key, pattern)
12
+ pattern = expression_of(pattern)
13
+ hole = CaptureHole.new(key, pattern)
14
+
15
+ expr(hole)
16
+ end
17
+
18
+ def hole(key, &filter)
19
+ expr(Hole.new(key, filter))
20
+ end
21
+
22
+ def var(key)
23
+ expr(VariableHole.new(key))
24
+ end
25
+
26
+ def const(key)
27
+ expr(ConstantHole.new(key))
28
+ end
29
+
30
+ def method_hole(key, receiver, method, *args, **kwargs)
31
+ receiver = expression_of(receiver)
32
+ args = args.map { expression_of(_1) }
33
+ kwargs = kwargs.transform_values { expression_of(_1) }
34
+ hole = MethodHole.new(key, receiver, method, args, kwargs)
35
+
36
+ expr(hole)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ PatternCapture = Struct.new(:expression, :mapping) do
5
+ def value_path
6
+ identifiers = mapping.path.to_a.reverse!
7
+ identifiers << -1
8
+ identifiers
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class PatternMatch
5
+ def initialize
6
+ @captures = {}
7
+ end
8
+
9
+ def [](key)
10
+ @captures[key]
11
+ end
12
+
13
+ def include?(key)
14
+ @captures.include?(key)
15
+ end
16
+
17
+ def capture(key, expression, mapping)
18
+ @captures[key] = PatternCapture.new(expression, mapping)
19
+ end
20
+
21
+ def value_paths
22
+ @captures.transform_values(&:value_path)
23
+ end
24
+
25
+ def expressions
26
+ @captures.transform_values(&:expression)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class VariableHole < Hole
5
+ def match?(expression)
6
+ expression.is_a?(Variable)
7
+ end
8
+
9
+ def to_s
10
+ "var(#{@key.inspect})"
11
+ end
12
+ alias inspect to_s
13
+ end
14
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Reporter
5
+ ##
6
+ # Formats an error tree as a human-readable string
7
+ # @example
8
+ # errors = Matcher.build { Integer }.match("foo")
9
+ # puts Reporter.report(errors)
10
+ # # > root: expected a kind of Integer but got "foo"
11
+ # @param error [Error] the error tree from {Base#match}
12
+ # @return [String]
13
+ def self.report(error)
14
+ new.report(error)
15
+ end
16
+
17
+ def initialize
18
+ @level = 0
19
+ @continue_line = false
20
+ @path_stack = ['root']
21
+ @phrasing = ExpectedPhrasing.phrasing
22
+ end
23
+
24
+ def report(error)
25
+ @io = StringIO.new
26
+
27
+ report_error(error)
28
+
29
+ string = @io.string
30
+ @io.close
31
+ @io = nil
32
+
33
+ string
34
+ end
35
+
36
+ private
37
+
38
+ def report_error(error)
39
+ case error
40
+ when EmptyError
41
+ report_empty
42
+ when ElementError
43
+ report_element(error)
44
+ when NestedError
45
+ report_nested(error)
46
+ when AndError
47
+ report_and(error)
48
+ when OrError
49
+ report_or(error)
50
+ else
51
+ raise "Illegal error: #{error.inspect}"
52
+ end
53
+ end
54
+
55
+ def report_empty
56
+ 'no error'
57
+ end
58
+
59
+ def report_element(element)
60
+ message = element.message
61
+ message = @phrasing.call(@path_stack.last, message) if message.is_a?(Message)
62
+
63
+ line("#{@path_stack.last}: #{message}")
64
+ end
65
+
66
+ def report_nested(nested)
67
+ path = NestedError.key_to_s(nested.key, @path_stack.last)
68
+
69
+ @path_stack.push(path)
70
+ report_error(nested.child)
71
+ @path_stack.pop
72
+ end
73
+
74
+ def report_and(error)
75
+ error.children.each { report_error(_1) }
76
+ end
77
+
78
+ def report_or(error)
79
+ line('expected at least one error to be absent:')
80
+
81
+ error.children.each do |n|
82
+ line('- ', newline: false)
83
+
84
+ @level += 1
85
+ report_error(n)
86
+ @level -= 1
87
+ end
88
+ end
89
+
90
+ def line(message, newline: true)
91
+ message = ' ' * @level + message unless @continue_line
92
+ message += "\n" if newline
93
+
94
+ @continue_line = !newline
95
+ @io.print(message)
96
+ end
97
+ end
98
+ end