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.
- checksums.yaml +7 -0
- data/lib/matcher/assertions.rb +19 -0
- data/lib/matcher/autoload.rb +5 -0
- data/lib/matcher/base.rb +183 -0
- data/lib/matcher/compatibility.rb +34 -0
- data/lib/matcher/debug.rb +62 -0
- data/lib/matcher/dsl/builder.rb +99 -0
- data/lib/matcher/dsl/chain.rb +84 -0
- data/lib/matcher/dsl/expression_dsl.rb +306 -0
- data/lib/matcher/dsl/matcher_dsl.rb +5 -0
- data/lib/matcher/dsl/optional.rb +82 -0
- data/lib/matcher/dsl/optional_chain.rb +24 -0
- data/lib/matcher/dsl/others.rb +28 -0
- data/lib/matcher/errors/and_error.rb +88 -0
- data/lib/matcher/errors/boolean_collector.rb +51 -0
- data/lib/matcher/errors/element_error.rb +24 -0
- data/lib/matcher/errors/empty_error.rb +23 -0
- data/lib/matcher/errors/error.rb +39 -0
- data/lib/matcher/errors/error_collector.rb +100 -0
- data/lib/matcher/errors/nested_error.rb +98 -0
- data/lib/matcher/errors/or_error.rb +88 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +96 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +189 -0
- data/lib/matcher/expressions/call.rb +307 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +237 -0
- data/lib/matcher/expressions/expression_walker.rb +77 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +96 -0
- data/lib/matcher/expressions/range_expression.rb +65 -0
- data/lib/matcher/expressions/recorder.rb +136 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +49 -0
- data/lib/matcher/expressions/set_expression.rb +45 -0
- data/lib/matcher/expressions/string_expression.rb +53 -0
- data/lib/matcher/expressions/symbol_proc.rb +53 -0
- data/lib/matcher/expressions/variable.rb +87 -0
- data/lib/matcher/hash_stack.rb +52 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers.rb +7 -0
- data/lib/matcher/matcher_cache.rb +18 -0
- data/lib/matcher/matchers/all_matcher.rb +60 -0
- data/lib/matcher/matchers/always_matcher.rb +34 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +72 -0
- data/lib/matcher/matchers/block_matcher.rb +61 -0
- data/lib/matcher/matchers/boolean_matcher.rb +37 -0
- data/lib/matcher/matchers/dig_matcher.rb +149 -0
- data/lib/matcher/matchers/each_matcher.rb +85 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +198 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +112 -0
- data/lib/matcher/matchers/expression_matcher.rb +69 -0
- data/lib/matcher/matchers/filter_matcher.rb +115 -0
- data/lib/matcher/matchers/hash_matcher.rb +315 -0
- data/lib/matcher/matchers/imply_matcher.rb +83 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +116 -0
- data/lib/matcher/matchers/index_by_matcher.rb +177 -0
- data/lib/matcher/matchers/inline_matcher.rb +101 -0
- data/lib/matcher/matchers/keys_matcher.rb +131 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +69 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +69 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +148 -0
- data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
- data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
- data/lib/matcher/matchers/negated_matcher.rb +25 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +35 -0
- data/lib/matcher/matchers/one_matcher.rb +68 -0
- data/lib/matcher/matchers/optional_matcher.rb +38 -0
- data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
- data/lib/matcher/matchers/parse_integer_matcher.rb +101 -0
- data/lib/matcher/matchers/parse_iso8601_helper.rb +41 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +52 -0
- data/lib/matcher/matchers/parse_json_helper.rb +43 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +59 -0
- data/lib/matcher/matchers/project_matcher.rb +72 -0
- data/lib/matcher/matchers/raises_matcher.rb +131 -0
- data/lib/matcher/matchers/range_matcher.rb +50 -0
- data/lib/matcher/matchers/reference_matcher.rb +213 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +86 -0
- data/lib/matcher/messages/expected_phrasing.rb +355 -0
- data/lib/matcher/messages/message.rb +104 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +240 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +59 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -0
- data/lib/matcher/patterns/ast_mapping.rb +42 -0
- data/lib/matcher/patterns/capture_hole.rb +33 -0
- data/lib/matcher/patterns/constant_hole.rb +14 -0
- data/lib/matcher/patterns/hole.rb +30 -0
- data/lib/matcher/patterns/method_hole.rb +62 -0
- data/lib/matcher/patterns/pattern.rb +104 -0
- data/lib/matcher/patterns/pattern_building.rb +39 -0
- data/lib/matcher/patterns/pattern_capture.rb +11 -0
- data/lib/matcher/patterns/pattern_match.rb +29 -0
- data/lib/matcher/patterns/variable_hole.rb +14 -0
- data/lib/matcher/reporter.rb +103 -0
- data/lib/matcher/rules/message_factory.rb +26 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +26 -0
- data/lib/matcher/rules/rule_builder.rb +29 -0
- data/lib/matcher/rules/rule_set.rb +57 -0
- data/lib/matcher/rules/transform_builder.rb +24 -0
- data/lib/matcher/rules/transform_mapping.rb +5 -0
- data/lib/matcher/rules/transform_rule.rb +21 -0
- data/lib/matcher/state.rb +40 -0
- data/lib/matcher/testing/error_builder.rb +62 -0
- data/lib/matcher/testing/error_checker.rb +514 -0
- data/lib/matcher/testing/error_testing.rb +37 -0
- data/lib/matcher/testing/pattern_testing.rb +11 -0
- data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
- data/lib/matcher/testing.rb +107 -0
- data/lib/matcher/undefined.rb +10 -0
- data/lib/matcher/utils/mapping_utils.rb +61 -0
- data/lib/matcher/utils.rb +72 -0
- data/lib/matcher/version.rb +5 -0
- data/lib/matcher.rb +346 -0
- 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
|
data/lib/matcher/base.rb
ADDED
|
@@ -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
|