matchers 0.1.0.pre.1

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.
Files changed (127) hide show
  1. checksums.yaml +7 -0
  2. data/lib/matcher/assertions.rb +19 -0
  3. data/lib/matcher/autoload.rb +5 -0
  4. data/lib/matcher/base.rb +183 -0
  5. data/lib/matcher/compatibility.rb +34 -0
  6. data/lib/matcher/debug.rb +62 -0
  7. data/lib/matcher/dsl/builder.rb +99 -0
  8. data/lib/matcher/dsl/chain.rb +84 -0
  9. data/lib/matcher/dsl/expression_dsl.rb +306 -0
  10. data/lib/matcher/dsl/matcher_dsl.rb +5 -0
  11. data/lib/matcher/dsl/optional.rb +82 -0
  12. data/lib/matcher/dsl/optional_chain.rb +24 -0
  13. data/lib/matcher/dsl/others.rb +28 -0
  14. data/lib/matcher/errors/and_error.rb +88 -0
  15. data/lib/matcher/errors/boolean_collector.rb +51 -0
  16. data/lib/matcher/errors/element_error.rb +24 -0
  17. data/lib/matcher/errors/empty_error.rb +23 -0
  18. data/lib/matcher/errors/error.rb +39 -0
  19. data/lib/matcher/errors/error_collector.rb +100 -0
  20. data/lib/matcher/errors/nested_error.rb +98 -0
  21. data/lib/matcher/errors/or_error.rb +88 -0
  22. data/lib/matcher/expression_cache.rb +57 -0
  23. data/lib/matcher/expression_labeler.rb +96 -0
  24. data/lib/matcher/expressions/array_expression.rb +45 -0
  25. data/lib/matcher/expressions/block.rb +189 -0
  26. data/lib/matcher/expressions/call.rb +307 -0
  27. data/lib/matcher/expressions/call_error.rb +45 -0
  28. data/lib/matcher/expressions/constant.rb +53 -0
  29. data/lib/matcher/expressions/expression.rb +237 -0
  30. data/lib/matcher/expressions/expression_walker.rb +77 -0
  31. data/lib/matcher/expressions/hash_expression.rb +59 -0
  32. data/lib/matcher/expressions/proc_expression.rb +96 -0
  33. data/lib/matcher/expressions/range_expression.rb +65 -0
  34. data/lib/matcher/expressions/recorder.rb +136 -0
  35. data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -0
  36. data/lib/matcher/expressions/set_expression.rb +45 -0
  37. data/lib/matcher/expressions/string_expression.rb +53 -0
  38. data/lib/matcher/expressions/symbol_proc.rb +53 -0
  39. data/lib/matcher/expressions/variable.rb +87 -0
  40. data/lib/matcher/hash_stack.rb +52 -0
  41. data/lib/matcher/list.rb +102 -0
  42. data/lib/matcher/markers.rb +7 -0
  43. data/lib/matcher/matcher_cache.rb +18 -0
  44. data/lib/matcher/matchers/all_matcher.rb +60 -0
  45. data/lib/matcher/matchers/always_matcher.rb +34 -0
  46. data/lib/matcher/matchers/any_matcher.rb +70 -0
  47. data/lib/matcher/matchers/array_matcher.rb +72 -0
  48. data/lib/matcher/matchers/block_matcher.rb +61 -0
  49. data/lib/matcher/matchers/boolean_matcher.rb +37 -0
  50. data/lib/matcher/matchers/dig_matcher.rb +149 -0
  51. data/lib/matcher/matchers/each_matcher.rb +85 -0
  52. data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
  53. data/lib/matcher/matchers/equal_matcher.rb +198 -0
  54. data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
  55. data/lib/matcher/matchers/expression_matcher.rb +69 -0
  56. data/lib/matcher/matchers/filter_matcher.rb +115 -0
  57. data/lib/matcher/matchers/hash_matcher.rb +315 -0
  58. data/lib/matcher/matchers/imply_matcher.rb +83 -0
  59. data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
  60. data/lib/matcher/matchers/index_by_matcher.rb +177 -0
  61. data/lib/matcher/matchers/inline_matcher.rb +101 -0
  62. data/lib/matcher/matchers/keys_matcher.rb +131 -0
  63. data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
  64. data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
  65. data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
  66. data/lib/matcher/matchers/let_matcher.rb +73 -0
  67. data/lib/matcher/matchers/map_matcher.rb +148 -0
  68. data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
  69. data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
  70. data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
  71. data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
  72. data/lib/matcher/matchers/negated_matcher.rb +25 -0
  73. data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
  74. data/lib/matcher/matchers/never_matcher.rb +35 -0
  75. data/lib/matcher/matchers/one_matcher.rb +68 -0
  76. data/lib/matcher/matchers/optional_matcher.rb +38 -0
  77. data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
  78. data/lib/matcher/matchers/parse_integer_matcher.rb +101 -0
  79. data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
  80. data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
  81. data/lib/matcher/matchers/parse_json_helper.rb +43 -0
  82. data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
  83. data/lib/matcher/matchers/project_matcher.rb +72 -0
  84. data/lib/matcher/matchers/raises_matcher.rb +131 -0
  85. data/lib/matcher/matchers/range_matcher.rb +50 -0
  86. data/lib/matcher/matchers/reference_matcher.rb +213 -0
  87. data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
  88. data/lib/matcher/matchers/regexp_matcher.rb +86 -0
  89. data/lib/matcher/messages/expected_phrasing.rb +355 -0
  90. data/lib/matcher/messages/message.rb +104 -0
  91. data/lib/matcher/messages/message_builder.rb +35 -0
  92. data/lib/matcher/messages/message_rules.rb +240 -0
  93. data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
  94. data/lib/matcher/messages/phrasing.rb +59 -0
  95. data/lib/matcher/messages/standard_message_builder.rb +105 -0
  96. data/lib/matcher/patterns/ast_mapping.rb +42 -0
  97. data/lib/matcher/patterns/capture_hole.rb +33 -0
  98. data/lib/matcher/patterns/constant_hole.rb +14 -0
  99. data/lib/matcher/patterns/hole.rb +30 -0
  100. data/lib/matcher/patterns/method_hole.rb +62 -0
  101. data/lib/matcher/patterns/pattern.rb +104 -0
  102. data/lib/matcher/patterns/pattern_building.rb +39 -0
  103. data/lib/matcher/patterns/pattern_capture.rb +11 -0
  104. data/lib/matcher/patterns/pattern_match.rb +29 -0
  105. data/lib/matcher/patterns/variable_hole.rb +14 -0
  106. data/lib/matcher/reporter.rb +103 -0
  107. data/lib/matcher/rules/message_factory.rb +26 -0
  108. data/lib/matcher/rules/message_rule.rb +18 -0
  109. data/lib/matcher/rules/message_rule_context.rb +26 -0
  110. data/lib/matcher/rules/rule_builder.rb +29 -0
  111. data/lib/matcher/rules/rule_set.rb +57 -0
  112. data/lib/matcher/rules/transform_builder.rb +24 -0
  113. data/lib/matcher/rules/transform_mapping.rb +5 -0
  114. data/lib/matcher/rules/transform_rule.rb +21 -0
  115. data/lib/matcher/state.rb +40 -0
  116. data/lib/matcher/testing/error_builder.rb +62 -0
  117. data/lib/matcher/testing/error_checker.rb +514 -0
  118. data/lib/matcher/testing/error_testing.rb +37 -0
  119. data/lib/matcher/testing/pattern_testing.rb +11 -0
  120. data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
  121. data/lib/matcher/testing.rb +107 -0
  122. data/lib/matcher/undefined.rb +10 -0
  123. data/lib/matcher/utils/mapping_utils.rb +61 -0
  124. data/lib/matcher/utils.rb +72 -0
  125. data/lib/matcher/version.rb +5 -0
  126. data/lib/matcher.rb +346 -0
  127. metadata +174 -0
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module PatternBuilding
5
+ include ExpressionDsl
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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Matcher
6
+ class Reporter
7
+ ##
8
+ # Formats an error tree as a human-readable string
9
+ # @example
10
+ # errors = Matcher.build { Integer }.match("foo")
11
+ # puts Reporter.report(errors)
12
+ # # > root: expected a kind of Integer but got "foo"
13
+ # @param error [Error] the error tree from {Base#match}
14
+ # @return [String]
15
+ def self.report(error)
16
+ new.report(error)
17
+ end
18
+
19
+ def initialize
20
+ @level = 0
21
+ @continue_line = false
22
+ @path_stack = ["root"]
23
+ @phrasing = ExpectedPhrasing.phrasing
24
+ end
25
+
26
+ def report(error)
27
+ @io = StringIO.new
28
+
29
+ report_error(error)
30
+
31
+ string = @io.string
32
+ @io.close
33
+ @io = nil
34
+
35
+ string
36
+ end
37
+
38
+ private
39
+
40
+ def report_error(error)
41
+ case error
42
+ when EmptyError
43
+ report_empty
44
+ when ElementError
45
+ report_element(error)
46
+ when NestedError
47
+ report_nested(error)
48
+ when AndError
49
+ report_and(error)
50
+ when OrError
51
+ report_or(error)
52
+ else
53
+ raise "Illegal error: #{error.inspect}"
54
+ end
55
+ end
56
+
57
+ def report_empty
58
+ "no error"
59
+ end
60
+
61
+ def report_element(element)
62
+ message = element.message
63
+
64
+ if message.is_a?(Message)
65
+ message = @phrasing.call(@path_stack.last, message)
66
+ end
67
+
68
+ line("#{@path_stack.last}: #{message}")
69
+ end
70
+
71
+ def report_nested(nested)
72
+ path = NestedError.key_to_s(nested.key, @path_stack.last)
73
+
74
+ @path_stack.push(path)
75
+ report_error(nested.child)
76
+ @path_stack.pop
77
+ end
78
+
79
+ def report_and(error)
80
+ error.children.each { report_error(_1) }
81
+ end
82
+
83
+ def report_or(error)
84
+ line("expected at least one error to be absent:")
85
+
86
+ error.children.each do |n|
87
+ line("- ", newline: false)
88
+
89
+ @level += 1
90
+ report_error(n)
91
+ @level -= 1
92
+ end
93
+ end
94
+
95
+ def line(message, newline: true)
96
+ message = " " * @level + message unless @continue_line
97
+ message += "\n" if newline
98
+
99
+ @continue_line = !newline
100
+ @io.print(message)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MessageFactory
5
+ def initialize(value_paths, expressions, block)
6
+ @value_paths = value_paths
7
+ @expressions = expressions
8
+ @negate = false
9
+ @block = block
10
+ end
11
+
12
+ def negate!
13
+ @negate = !@negate
14
+ end
15
+
16
+ def create(context, value_tree)
17
+ # Using reduce instead of dig so it will fail if an unexpected leaf is
18
+ # encountered.
19
+ values = @value_paths.transform_values { _1.reduce(value_tree, :[]) }
20
+ message = context.instance_exec(values, @expressions, &@block)
21
+ message.negate! if @negate
22
+
23
+ message
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MessageRule
5
+ def initialize(patterns, block)
6
+ @patterns = patterns
7
+ @block = block
8
+ end
9
+
10
+ attr_reader :patterns
11
+
12
+ def apply(match)
13
+ expressions = @block.arity >= 2 ? match.expressions : nil
14
+
15
+ MessageFactory.new(match.value_paths, expressions, @block)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class MessageRuleContext
5
+ extend Forwardable
6
+
7
+ def initialize(matcher, state)
8
+ @matcher = matcher
9
+ @state = state
10
+ end
11
+
12
+ def standard_message
13
+ StandardMessageBuilder.new(!@matcher.negated, @state.actual)
14
+ end
15
+
16
+ def expression_message
17
+ NamespacedMessageBuilder.new(
18
+ !@matcher.negated, @state.actual, :expression
19
+ )
20
+ end
21
+
22
+ def given
23
+ @matcher.expression.given_for(@state.values)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class RuleBuilder
5
+ include PatternBuilding
6
+
7
+ def initialize(rules = [], build_session: Matcher.build_session)
8
+ ExpressionDsl.init(self, build_session)
9
+
10
+ @rules = rules
11
+ end
12
+
13
+ attr_reader :rules
14
+
15
+ def transform(*patterns, negate: false, &block)
16
+ patterns.map! { pattern_of(_1) }
17
+ @rules << TransformRule.new(patterns, negate, block)
18
+
19
+ nil
20
+ end
21
+
22
+ def message(*patterns, &block)
23
+ patterns.map! { pattern_of(_1) }
24
+ @rules << MessageRule.new(patterns, block)
25
+
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class RuleSet
5
+ def initialize(rules = [], &)
6
+ @rules = rules
7
+
8
+ configure(&) if block_given?
9
+ end
10
+
11
+ def configure(&)
12
+ Matcher.with_build_session do |build_session|
13
+ builder = RuleBuilder.new(@rules, build_session:)
14
+ builder.instance_exec(&)
15
+ end
16
+
17
+ self
18
+ end
19
+
20
+ def apply(expression)
21
+ cur = expression
22
+ mapping = AstMapping.new
23
+ result = nil
24
+ negate = false
25
+
26
+ while (rule, match = find_rule(cur, mapping))
27
+ result = rule.apply(match)
28
+
29
+ if rule.is_a?(MessageRule)
30
+ result.negate! if negate
31
+
32
+ return result
33
+ end
34
+
35
+ cur = result.expression
36
+ mapping = result.mapping
37
+ negate = !negate if rule.negate?
38
+ end
39
+
40
+ result
41
+ end
42
+
43
+ private
44
+
45
+ def find_rule(expression, mapping)
46
+ @rules.each do |rule|
47
+ rule.patterns.each do |pattern|
48
+ match = pattern.match(expression, mapping)
49
+
50
+ return [rule, match] if match
51
+ end
52
+ end
53
+
54
+ nil
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class TransformBuilder
5
+ include Singleton
6
+
7
+ def call(match, receiver, method, *args, **kwargs)
8
+ expression = Call.new(
9
+ receiver.expression,
10
+ method,
11
+ args.map(&:expression),
12
+ kwargs.transform_values(&:expression),
13
+ )
14
+
15
+ mapping = TransformMapping.new
16
+ mapping.path = match.mapping.path
17
+ mapping.receiver = receiver.mapping
18
+ mapping.args = args.map(&:mapping)
19
+ mapping.kwargs = kwargs.transform_values(&:mapping)
20
+
21
+ PatternCapture.new(expression, mapping)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ TransformMapping = Struct.new(:path, :receiver, :args, :kwargs)
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class TransformRule
5
+ def initialize(patterns, negate, block)
6
+ @patterns = patterns
7
+ @negate = negate
8
+ @block = block
9
+ end
10
+
11
+ attr_reader :patterns
12
+
13
+ def negate?
14
+ @negate
15
+ end
16
+
17
+ def apply(match)
18
+ TransformBuilder.instance.instance_exec(match, &@block)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class State
5
+ def initialize(values, boolean: false)
6
+ @values = values
7
+ @boolean = boolean
8
+ end
9
+
10
+ attr_reader :values
11
+
12
+ def boolean?
13
+ @boolean
14
+ end
15
+
16
+ def actual
17
+ @values[:actual]
18
+ end
19
+
20
+ def errors
21
+ @errors ||= new_collector
22
+ end
23
+
24
+ def new_collector
25
+ @boolean ? BooleanCollector.new : ErrorCollector.new
26
+ end
27
+
28
+ def result
29
+ @errors&.error || EmptyError.instance
30
+ end
31
+
32
+ def report(actual = self.actual)
33
+ StandardMessageBuilder.new(false, actual)
34
+ end
35
+
36
+ def expected(actual = self.actual)
37
+ StandardMessageBuilder.new(true, actual)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class ErrorBuilder
5
+ def self.build(use_or: false, &)
6
+ errors = build_errors(&)
7
+
8
+ if use_or
9
+ OrError.from(errors)
10
+ else
11
+ AndError.from(errors)
12
+ end
13
+ end
14
+
15
+ def self.build_errors(&)
16
+ builder = ErrorBuilder.new
17
+ builder.instance_exec(&) if block_given?
18
+ builder.errors
19
+ end
20
+
21
+ attr_reader :errors
22
+
23
+ def initialize
24
+ @errors = []
25
+ end
26
+
27
+ def _or(*path, &)
28
+ errors = ErrorBuilder.build_errors(&)
29
+ error = OrError.from(errors)
30
+
31
+ @errors << nest(path, error)
32
+ end
33
+
34
+ def _and(*path, &)
35
+ errors = ErrorBuilder.build_errors(&)
36
+ error = AndError.from(errors)
37
+
38
+ @errors << nest(path, error)
39
+ end
40
+
41
+ def error(path_or_message, message = nil)
42
+ if message.nil?
43
+ @errors << ElementError.new(path_or_message)
44
+ else
45
+ path = Array(path_or_message)
46
+ element = ElementError.new(message)
47
+
48
+ @errors << nest(path, element)
49
+ end
50
+ end
51
+
52
+ def msg(actual)
53
+ StandardMessageBuilder.new(false, actual)
54
+ end
55
+
56
+ private
57
+
58
+ def nest(path, error)
59
+ path.reverse_each.reduce(error) { NestedError.from(_2, _1) }
60
+ end
61
+ end
62
+ end