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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 92ba01bc8b3f334276b254cb4b78efcd86165a7410cad65fc21e0b319d532df0
4
+ data.tar.gz: 3626679374928bd5adf361c3d9d1fa357033e003ae702d225f4c3a9b4e028b82
5
+ SHA512:
6
+ metadata.gz: 32fb3f33b2d210d46f81ebaa0f3c4aec455a4543806b3b38e7eefd42f81718f98a2690a600140b12702d3c9ee3232f63c9d4a55b8f58840597779cff40b95276
7
+ data.tar.gz: 556079ba3a6e26bdc35217e651740d4f1298bc7886165dca2e4715ee0c270955baabb28fd66c6df4031acc6e42af774e5d19ae6bf70b76a10935ffcdeeea3758
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module Assertions
5
+ def assert_structure(actual, &)
6
+ errors = Matcher.build(&).match(actual)
7
+
8
+ assert(false, <<~TEXT.chomp) unless errors.valid?
9
+ For object:
10
+
11
+ #{actual.inspect}
12
+
13
+ The following conditions were not satisfied:
14
+
15
+ #{Reporter.report(errors)}
16
+ TEXT
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ autoload :Reporter, "matcher/reporter"
5
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Base
5
+ include NoExpression
6
+ include NoKey
7
+
8
+ ##
9
+ # Negates this matcher
10
+ def ~
11
+ matcher_cache = MatcherCache.current
12
+
13
+ return negate unless matcher_cache
14
+
15
+ cache = (matcher_cache.negated_matchers ||= {}.compare_by_identity)
16
+ negated = cache[self]
17
+
18
+ unless negated
19
+ negated = negate
20
+ cache[self] = negated
21
+ cache[negated] = self
22
+ end
23
+
24
+ negated
25
+ end
26
+
27
+ def negate
28
+ NegatedMatcher.new(self)
29
+ end
30
+ protected :negate
31
+
32
+ ##
33
+ # Combines with other to AnyMatcher
34
+ # @param other
35
+ # @return [AnyMatcher]
36
+ # @see MatcherDsl#any
37
+ def +(other)
38
+ other = Matcher.cache(other)
39
+
40
+ matchers = if other.is_a?(AnyMatcher)
41
+ [self].concat(other.matchers)
42
+ else
43
+ [self, other]
44
+ end
45
+
46
+ AnyMatcher.new(matchers)
47
+ end
48
+
49
+ ##
50
+ # Combines with other to AllMatcher
51
+ # @param other
52
+ # @return [AllMatcher]
53
+ # @see MatcherDsl#all
54
+ def *(other)
55
+ other = Matcher.cache(other)
56
+
57
+ matchers = if other.is_a?(AllMatcher)
58
+ [self].concat(other.matchers)
59
+ else
60
+ [self, other]
61
+ end
62
+
63
+ AllMatcher.new(matchers)
64
+ end
65
+
66
+ ##
67
+ # Combines with other to LazyAnyMatcher
68
+ # @param other
69
+ # @return [LazyAnyMatcher]
70
+ # @see MatcherDsl#lazy_any
71
+ def |(other)
72
+ other = Matcher.cache(other)
73
+
74
+ matchers = if other.is_a?(LazyAnyMatcher)
75
+ [self].concat(other.matchers)
76
+ else
77
+ [self, other]
78
+ end
79
+
80
+ LazyAnyMatcher.new(matchers)
81
+ end
82
+
83
+ ##
84
+ # Combines with other to LazyAllMatcher
85
+ # @param other
86
+ # @return [LazyAllMatcher]
87
+ # @see MatcherDsl#lazy_all
88
+ def &(other)
89
+ other = Matcher.cache(other)
90
+
91
+ matchers = if other.is_a?(LazyAllMatcher)
92
+ [self].concat(other.matchers)
93
+ else
94
+ [self, other]
95
+ end
96
+
97
+ LazyAllMatcher.new(matchers)
98
+ end
99
+
100
+ ##
101
+ # Implies another matcher
102
+ # @param other
103
+ # @return [ImplyMatcher]
104
+ # @see MatcherDsl#imply
105
+ def >>(other)
106
+ ImplyMatcher.new(self, Matcher.cache(other))
107
+ end
108
+
109
+ StackData = Struct.new(:actual, :vals, :errors)
110
+
111
+ ##
112
+ # Returns +true+ if actual matches, +false+ otherwise
113
+ # @example
114
+ # Matcher.build { Integer }.match?(42) # => true
115
+ # @param actual the value to match against
116
+ # @param ** values
117
+ # @return [Boolean]
118
+ def match?(actual, **)
119
+ match_helper(true, actual:, **).valid?
120
+ end
121
+ alias === match?
122
+
123
+ ##
124
+ # Returns an error tree describing all mismatches
125
+ # @example
126
+ # errors = Matcher.build { Integer }.match("foo")
127
+ # puts Matcher::Reporter.report(errors)
128
+ # # > root: expected a kind of Integer but got "foo"
129
+ # @param actual the value to match against
130
+ # @param ** values
131
+ # @return [Error]
132
+ def match(actual, **)
133
+ match_helper(false, actual:, **)
134
+ end
135
+
136
+ def match_helper(boolean, **)
137
+ hash_stack = HashStack.new
138
+
139
+ invoke = lambda do |matcher, act = UNDEFINED, **kwargs|
140
+ state = State.new(hash_stack, boolean:)
141
+ kwargs[:actual] = act unless Matcher.undefined?(act)
142
+
143
+ hash_stack.push(kwargs)
144
+
145
+ catch(:mismatch) do
146
+ matcher.validate(state, &invoke)
147
+ end
148
+
149
+ hash_stack.pop(kwargs)
150
+
151
+ state.result
152
+ end
153
+
154
+ Matcher.with_session do
155
+ invoke.call(self, **)
156
+ end
157
+ end
158
+ private :match_helper
159
+
160
+ def validate(state)
161
+ raise NotImplementedError
162
+ end
163
+ protected :validate
164
+
165
+ def inspect
166
+ to_s
167
+ end
168
+
169
+ protected
170
+
171
+ ##
172
+ # Stores information for this matcher instance during match time.
173
+ def session(key = object_id)
174
+ Matcher.session[key] ||= {}
175
+ end
176
+
177
+ ##
178
+ # Stores information for this matcher's class during match time.
179
+ def class_session
180
+ Matcher.session[self.class] ||= {}
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module Compatibility
5
+ extend self
6
+
7
+ # rubocop:disable Style/ClassVars
8
+
9
+ @@method_quote_delimiter = caller[0].include?("`") ? "`" : "'"
10
+
11
+ # rubocop:enable Style/ClassVars
12
+
13
+ def quote_method(method)
14
+ "#{@@method_quote_delimiter}#{method}'"
15
+ end
16
+
17
+ def nil_kwargs?
18
+ test_nil_kwargs(**nil)
19
+ rescue TypeError
20
+ false
21
+ end
22
+
23
+ # rubocop:disable Naming/PredicateMethod
24
+
25
+ def test_nil_kwargs(**)
26
+ true
27
+ end
28
+ private :test_nil_kwargs
29
+
30
+ # rubocop:enable Naming/PredicateMethod
31
+
32
+ NULL_KWARGS = nil_kwargs? ? nil : {}.freeze
33
+ end
34
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ module Debug
5
+ class << self
6
+ DEBUGGERS = %w[/bin/irb: ruby-debug-ide].freeze
7
+
8
+ def enable
9
+ init(force: true)
10
+ end
11
+
12
+ def init(force: false)
13
+ return if @initialized
14
+
15
+ main_caller = caller[-1]
16
+
17
+ return if !force && DEBUGGERS.none? { main_caller.include?(_1) }
18
+
19
+ Recorder.prepend(ExpressionRecorderDebug)
20
+ @initialized = true
21
+ end
22
+
23
+ def debugging?(trace)
24
+ last_trace_item = trace[0]
25
+
26
+ %w[puts p].any? { call_from?(last_trace_item, _1) } ||
27
+ last_trace_item.include?("ruby-debug-ide") ||
28
+ trace.any? { call_from?(_1, "output_value") }
29
+ end
30
+
31
+ # rubocop:disable Style/ClassVars
32
+
33
+ @@method_quote_delimiter = caller[0].include?("`") ? "`" : "#"
34
+
35
+ # rubocop:enable Style/ClassVars
36
+
37
+ def call_from?(trace_item, method)
38
+ trace_item.end_with?("#{@@method_quote_delimiter}#{method}'")
39
+ end
40
+ end
41
+ end
42
+
43
+ module ExpressionRecorderDebug
44
+ private
45
+
46
+ def method_missing(method, ...)
47
+ if Debug.debugging?(caller)
48
+ return @expression.to_s if %i[to_s inspect].include?(method)
49
+
50
+ Object.instance_method(method).bind_call(self, ...)
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def respond_to_missing?(method, _include_private = false)
57
+ return Object.method_defined?(method) if Debug.debugging?(caller)
58
+
59
+ super
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ class Builder
5
+ include ExpressionDsl
6
+ include MatcherDsl
7
+
8
+ def initialize(outside, build_session: Matcher.build_session)
9
+ ExpressionDsl.init(self, build_session)
10
+
11
+ @outside = outside
12
+ @matcher_cache = MatcherCache.current(build_session)
13
+ end
14
+
15
+ ##
16
+ # Turns a matchable value into a matcher
17
+ # @example
18
+ # of(String).class # => Matcher::KindOfMatcher
19
+ # of(String) & !_.empty? # matches non-empty strings
20
+ # @param value
21
+ # @return [Base]
22
+ def matcher_of(value)
23
+ Matcher.of(
24
+ value,
25
+ matcher_cache: @matcher_cache,
26
+ expression_cache: @expression_cache,
27
+ )
28
+ end
29
+ alias of matcher_of
30
+
31
+ ##
32
+ # Accesses the outside context within a build block
33
+ # @example
34
+ # class MyClass
35
+ # def initialize
36
+ # @ivar = 42
37
+ # end
38
+ #
39
+ # def my_method
40
+ # "my string"
41
+ # end
42
+ #
43
+ # def my_matcher
44
+ # Matcher.build do
45
+ # # @ivar and self.my_method not accessible from here
46
+ # [outside { @ivar }, outside.my_method]
47
+ # end
48
+ # end
49
+ # end
50
+ # @return [Object] the outside context
51
+ def outside(&)
52
+ if block_given?
53
+ @outside.instance_eval(&)
54
+ else
55
+ @outside
56
+ end
57
+ end
58
+
59
+ ##
60
+ # Negates a matcher
61
+ # @example
62
+ # # matches anything except 1
63
+ # neg(1)
64
+ # # alternatively:
65
+ # ~equal(1)
66
+ # @param matcher
67
+ # @return [Base]
68
+ # @see Base#~
69
+ def neg(matcher)
70
+ ~matcher_of(matcher)
71
+ end
72
+
73
+ ##
74
+ # Matches given matcher but not +nil+
75
+ #
76
+ # This is useful to prevent accidentally matching against +nil+:
77
+ # def definitely_not_nil
78
+ # nil # shoot
79
+ # end
80
+ #
81
+ # my_value = definitely_not_nil
82
+ # m = Matcher.build { present(my_value) }
83
+ # m.match?(nil) # => false
84
+ #
85
+ # my_value = "fixed"
86
+ # m = Matcher.build { present(my_value) }
87
+ # m.match?("fixed") # => true
88
+ # @example
89
+ # present(my_value)
90
+ # @param matcher
91
+ # @return [AllMatcher]
92
+ def present(matcher)
93
+ AllMatcher.new([
94
+ ~equal(nil),
95
+ matcher_of(matcher),
96
+ ])
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Matcher
4
+ ##
5
+ # Matcher helpers like +each+ or +map+ can be chained with the +^+ operator or
6
+ # +chain+ helper. If a helper has the form <tt>my_helper(..., matcher)</tt>
7
+ # then it usually also supports this form <tt>my_helper(...) ^ matcher</tt>.
8
+ # This helps to reduce nested parenthesis:
9
+ #
10
+ # # before
11
+ # let({ limit: 10 }, map(_.compact, filter(_.even?, _ < vars[:limit])))
12
+ #
13
+ # # with ^
14
+ # let(limit: 10) ^
15
+ # map(_.compact) ^
16
+ # filter(_.even?) ^
17
+ # (_ < vars[:limit])
18
+ #
19
+ # # with chain
20
+ # chain(
21
+ # let(limit: 10),
22
+ # map(_.compact),
23
+ # filter(_.even?),
24
+ # _ < vars[:limit],
25
+ # )
26
+ #
27
+ # Keep operator precedence in mind when working with expressions.
28
+ class Chain
29
+ include NoMatcher
30
+ include NoExpression
31
+ include NoKey
32
+
33
+ def initialize(negated: false, &block)
34
+ @block = block
35
+ @negated = negated
36
+ end
37
+
38
+ def ~
39
+ Chain.new(negated: !@negated, &@block)
40
+ end
41
+
42
+ ##
43
+ # Chains this with a matcher or another chain
44
+ #
45
+ # Many helpers return a Chain that accepts a child matcher via +^+.
46
+ # Chains can also be composed: +each ^ map(_.to_i) ^ (_ > 0)+.
47
+ # @example
48
+ # each ^ Integer
49
+ # map(_.to_i) ^ [1, 2]
50
+ # filter(_.odd?) ^ [1, 3, 5]
51
+ # @param other matcher or chain
52
+ # @return [Base, Chain]
53
+ def ^(other)
54
+ if !Recorder.recorder?(other) && other.is_a?(Chain)
55
+ Chain.new { @block.call(other ^ _1) }
56
+ else
57
+ matcher = Matcher.cache(other)
58
+ result = @block.call(matcher)
59
+ result = ~result if @negated
60
+ result
61
+ end
62
+ end
63
+
64
+ def optional(fallback = AlwaysMatcher.instance)
65
+ OptionalChain.new(self, fallback)
66
+ end
67
+ end
68
+
69
+ module MatcherDsl
70
+ ##
71
+ # Reduces multiple chains to one
72
+ # @example
73
+ # chain(let(limit: 10), map(_.compact), filter(_.even?), _ < vars[:limit])
74
+ # # instead of
75
+ # let(limit: 10) ^ map(_.compact) ^ filter(_.even?) ^ (_ < vars[:limit])
76
+ # # which is equivalent to
77
+ # let({ limit: 10 }, map(_.compact, filter(_.even?, _ < vars[:limit])))
78
+ # @param *chains
79
+ # @return [Base, Chain]
80
+ def chain(*chains)
81
+ chains.reduce(:^)
82
+ end
83
+ end
84
+ end