matchi 4.1.1 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +148 -219
- data/lib/matchi/be.rb +58 -16
- data/lib/matchi/be_a_kind_of.rb +87 -34
- data/lib/matchi/be_an_instance_of.rb +74 -62
- data/lib/matchi/be_within.rb +125 -14
- data/lib/matchi/change/by.rb +99 -22
- data/lib/matchi/change/by_at_least.rb +118 -21
- data/lib/matchi/change/by_at_most.rb +132 -22
- data/lib/matchi/change/from/to.rb +92 -25
- data/lib/matchi/change/from.rb +31 -1
- data/lib/matchi/change/to.rb +72 -23
- data/lib/matchi/change.rb +119 -45
- data/lib/matchi/eq.rb +58 -16
- data/lib/matchi/match.rb +56 -16
- data/lib/matchi/predicate.rb +122 -33
- data/lib/matchi/raise_exception.rb +89 -22
- data/lib/matchi/satisfy.rb +87 -16
- data/lib/matchi.rb +51 -297
- metadata +12 -5
- data/lib/matchi/be_within/of.rb +0 -51
data/lib/matchi/change.rb
CHANGED
@@ -12,97 +12,171 @@ module Matchi
|
|
12
12
|
# Initialize a wrapper of the change matcher with an object and the name of
|
13
13
|
# one of its methods.
|
14
14
|
#
|
15
|
-
# @
|
16
|
-
#
|
15
|
+
# @api public
|
16
|
+
#
|
17
|
+
# @param object [#object_id] The object whose state will be monitored
|
18
|
+
# @param method [Symbol] The name of the method to track
|
19
|
+
# @param args [Array] Additional positional arguments to pass to the method
|
20
|
+
# @param kwargs [Hash] Additional keyword arguments to pass to the method
|
21
|
+
# @param block [Proc] Optional block to pass to the method
|
22
|
+
#
|
23
|
+
# @raise [ArgumentError] if method is not a Symbol
|
24
|
+
# @raise [ArgumentError] if object doesn't respond to method
|
25
|
+
#
|
26
|
+
# @return [Change] a new instance of the change wrapper
|
27
|
+
#
|
28
|
+
# @example Basic initialization
|
29
|
+
# array = []
|
30
|
+
# Change.new(array, :length) # Track array length changes
|
17
31
|
#
|
18
|
-
#
|
32
|
+
# @example With positional arguments
|
33
|
+
# hash = { key: "value" }
|
34
|
+
# Change.new(hash, :fetch, :key) # Track specific key value
|
19
35
|
#
|
20
|
-
# @
|
21
|
-
#
|
22
|
-
|
36
|
+
# @example With keyword arguments
|
37
|
+
# hash = { a: 1, b: 2 }
|
38
|
+
# Change.new(hash, :fetch, default: 0) # Track with default value
|
39
|
+
#
|
40
|
+
# @example With block
|
41
|
+
# hash = { a: 1 }
|
42
|
+
# Change.new(hash, :fetch, :b) { |k| k.to_s } # Track with block default
|
43
|
+
def initialize(object, method, *args, **kwargs, &block)
|
23
44
|
raise ::ArgumentError, "method must be a Symbol" unless method.is_a?(::Symbol)
|
24
45
|
raise ::ArgumentError, "object must respond to method" unless object.respond_to?(method)
|
25
46
|
|
26
|
-
@state = -> { object.send(method,
|
47
|
+
@state = -> { object.send(method, *args, **kwargs, &block) }
|
27
48
|
end
|
28
49
|
|
29
|
-
#
|
50
|
+
# Checks if the tracked method's return value changes when executing the block.
|
51
|
+
#
|
52
|
+
# This method verifies that the value changes in any way between the start and end
|
53
|
+
# of the block execution. It doesn't care about the type or magnitude of the change,
|
54
|
+
# only that it's different.
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
#
|
58
|
+
# @yield [] Block during which the change should occur
|
59
|
+
# @yieldreturn [Object] Result of the block execution (not used)
|
60
|
+
#
|
61
|
+
# @return [Boolean] true if the value changed, false otherwise
|
62
|
+
#
|
63
|
+
# @raise [ArgumentError] if no block is provided
|
64
|
+
#
|
65
|
+
# @example Basic usage with array length
|
66
|
+
# array = []
|
67
|
+
# matcher = Change.new(array, :length)
|
68
|
+
# matcher.match? { array << "item" } # => true
|
69
|
+
# matcher.match? { array.clear } # => true (from 1 to 0)
|
70
|
+
# matcher.match? { array.dup } # => false (no change)
|
71
|
+
#
|
72
|
+
# @example With method parameters
|
73
|
+
# hash = { key: "old" }
|
74
|
+
# matcher = Change.new(hash, :fetch, :key)
|
75
|
+
# matcher.match? { hash[:key] = "new" } # => true
|
76
|
+
# matcher.match? { hash[:key] = "new" } # => false (same value)
|
77
|
+
#
|
78
|
+
# @example With computed values
|
79
|
+
# text = "hello"
|
80
|
+
# matcher = Change.new(text, :upcase)
|
81
|
+
# matcher.match? { text.upcase! } # => true
|
82
|
+
# matcher.match? { text.upcase! } # => false (already uppercase)
|
83
|
+
def match?
|
84
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
85
|
+
|
86
|
+
value_before = @state.call
|
87
|
+
yield
|
88
|
+
value_after = @state.call
|
89
|
+
|
90
|
+
!value_before.eql?(value_after)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns a human-readable description of the matcher.
|
94
|
+
#
|
95
|
+
# @api public
|
96
|
+
#
|
97
|
+
# @return [String] A string describing what this matcher verifies
|
30
98
|
#
|
31
99
|
# @example
|
32
|
-
#
|
100
|
+
# Change.new("test", :upcase).to_s # => 'eq "test"'
|
101
|
+
def to_s
|
102
|
+
"change #{@state.inspect}"
|
103
|
+
end
|
104
|
+
|
105
|
+
# Specifies a minimum delta of the expected change.
|
33
106
|
#
|
34
|
-
#
|
107
|
+
# @api public
|
35
108
|
#
|
36
|
-
#
|
37
|
-
# change_wrapper.by_at_least(1)
|
109
|
+
# @param minimum_delta [#object_id] The minimum expected change amount
|
38
110
|
#
|
39
|
-
# @
|
111
|
+
# @return [#match?] A matcher that verifies the minimum change
|
40
112
|
#
|
41
|
-
# @
|
113
|
+
# @example
|
114
|
+
# counter = 0
|
115
|
+
# matcher = Change.new(counter, :to_i).by_at_least(5)
|
116
|
+
# matcher.match? { counter += 6 } # => true
|
42
117
|
def by_at_least(minimum_delta)
|
43
118
|
ByAtLeast.new(minimum_delta, &@state)
|
44
119
|
end
|
45
120
|
|
46
121
|
# Specifies a maximum delta of the expected change.
|
47
122
|
#
|
48
|
-
# @
|
49
|
-
# require "matchi/change"
|
50
|
-
#
|
51
|
-
# object = []
|
123
|
+
# @api public
|
52
124
|
#
|
53
|
-
#
|
54
|
-
# change_wrapper.by_at_most(1)
|
125
|
+
# @param maximum_delta [#object_id] The maximum allowed change amount
|
55
126
|
#
|
56
|
-
# @
|
127
|
+
# @return [#match?] A matcher that verifies the maximum change
|
57
128
|
#
|
58
|
-
# @
|
129
|
+
# @example
|
130
|
+
# counter = 0
|
131
|
+
# matcher = Change.new(counter, :to_i).by_at_most(5)
|
132
|
+
# matcher.match? { counter += 3 } # => true
|
59
133
|
def by_at_most(maximum_delta)
|
60
134
|
ByAtMost.new(maximum_delta, &@state)
|
61
135
|
end
|
62
136
|
|
63
|
-
# Specifies the delta of the expected change.
|
64
|
-
#
|
65
|
-
# @example
|
66
|
-
# require "matchi/change"
|
137
|
+
# Specifies the exact delta of the expected change.
|
67
138
|
#
|
68
|
-
#
|
139
|
+
# @api public
|
69
140
|
#
|
70
|
-
#
|
71
|
-
# change_wrapper.by(1)
|
141
|
+
# @param delta [#object_id] The exact expected change amount
|
72
142
|
#
|
73
|
-
# @
|
143
|
+
# @return [#match?] A matcher that verifies the exact change
|
74
144
|
#
|
75
|
-
# @
|
145
|
+
# @example
|
146
|
+
# counter = 0
|
147
|
+
# matcher = Change.new(counter, :to_i).by(5)
|
148
|
+
# matcher.match? { counter += 5 } # => true
|
76
149
|
def by(delta)
|
77
150
|
By.new(delta, &@state)
|
78
151
|
end
|
79
152
|
|
80
|
-
# Specifies the original value.
|
153
|
+
# Specifies the original value in a value transition check.
|
81
154
|
#
|
82
|
-
# @
|
83
|
-
# require "matchi/change"
|
155
|
+
# @api public
|
84
156
|
#
|
85
|
-
#
|
86
|
-
# change_wrapper.from("foo")
|
157
|
+
# @param old_value [#object_id] The expected initial value
|
87
158
|
#
|
88
|
-
# @
|
159
|
+
# @return [#to] A wrapper for creating a from/to matcher
|
89
160
|
#
|
90
|
-
# @
|
161
|
+
# @example
|
162
|
+
# string = "foo"
|
163
|
+
# Change.new(string, :to_s).from("foo").to("FOO")
|
91
164
|
def from(old_value)
|
92
165
|
From.new(old_value, &@state)
|
93
166
|
end
|
94
167
|
|
95
|
-
# Specifies the
|
168
|
+
# Specifies the final value to expect.
|
96
169
|
#
|
97
|
-
# @
|
98
|
-
# require "matchi/change"
|
170
|
+
# @api public
|
99
171
|
#
|
100
|
-
#
|
101
|
-
# change_wrapper.to("FOO")
|
172
|
+
# @param new_value [#object_id] The expected final value
|
102
173
|
#
|
103
|
-
# @
|
174
|
+
# @return [#match?] A matcher that verifies the final state
|
104
175
|
#
|
105
|
-
# @
|
176
|
+
# @example
|
177
|
+
# string = "foo"
|
178
|
+
# matcher = Change.new(string, :to_s).to("FOO")
|
179
|
+
# matcher.match? { string.upcase! } # => true
|
106
180
|
def to(new_value)
|
107
181
|
To.new(new_value, &@state)
|
108
182
|
end
|
data/lib/matchi/eq.rb
CHANGED
@@ -1,41 +1,83 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Value equivalence matcher that checks if two objects have identical values.
|
5
|
+
#
|
6
|
+
# This matcher verifies value equality using Ruby's Object#eql? method, which
|
7
|
+
# compares the values of objects rather than their identity. This is different
|
8
|
+
# from identity comparison (equal?) which checks if objects are the same instance.
|
9
|
+
#
|
10
|
+
# @example Basic usage with strings
|
11
|
+
# matcher = Matchi::Eq.new("test")
|
12
|
+
# matcher.match? { "test" } # => true
|
13
|
+
# matcher.match? { "test".dup } # => true
|
14
|
+
# matcher.match? { "other" } # => false
|
15
|
+
#
|
16
|
+
# @example With numbers
|
17
|
+
# matcher = Matchi::Eq.new(42)
|
18
|
+
# matcher.match? { 42 } # => true
|
19
|
+
# matcher.match? { 42.0 } # => false # Different types
|
20
|
+
# matcher.match? { 43 } # => false
|
21
|
+
#
|
22
|
+
# @example With collections
|
23
|
+
# array = [1, 2, 3]
|
24
|
+
# matcher = Matchi::Eq.new(array)
|
25
|
+
# matcher.match? { array.dup } # => true # Same values
|
26
|
+
# matcher.match? { array } # => true # Same object
|
27
|
+
# matcher.match? { [1, 2, 3] } # => true # Same values
|
28
|
+
# matcher.match? { [1, 2] } # => false # Different values
|
29
|
+
#
|
30
|
+
# @see https://ruby-doc.org/core/Object.html#method-i-eql-3F
|
31
|
+
# @see Matchi::Be
|
5
32
|
class Eq
|
6
|
-
# Initialize the matcher with
|
33
|
+
# Initialize the matcher with a reference value.
|
7
34
|
#
|
8
|
-
# @
|
9
|
-
#
|
35
|
+
# @api public
|
36
|
+
#
|
37
|
+
# @param expected [#eql?] The expected equivalent value
|
10
38
|
#
|
11
|
-
#
|
39
|
+
# @return [Eq] a new instance of the matcher
|
12
40
|
#
|
13
|
-
# @
|
41
|
+
# @example
|
42
|
+
# Eq.new("test") # Match strings with same value
|
43
|
+
# Eq.new([1, 2, 3]) # Match arrays with same elements
|
14
44
|
def initialize(expected)
|
15
45
|
@expected = expected
|
16
46
|
end
|
17
47
|
|
18
|
-
#
|
48
|
+
# Checks if the yielded object has a value equivalent to the expected object.
|
19
49
|
#
|
20
|
-
#
|
21
|
-
#
|
50
|
+
# This method uses Ruby's Object#eql? method, which performs value comparison.
|
51
|
+
# Two objects are considered equivalent if they have the same value, even if
|
52
|
+
# they are different instances.
|
53
|
+
#
|
54
|
+
# @api public
|
22
55
|
#
|
23
|
-
#
|
24
|
-
#
|
56
|
+
# @yield [] Block that returns the object to check
|
57
|
+
# @yieldreturn [Object] The object to verify equivalence with
|
25
58
|
#
|
26
|
-
# @
|
27
|
-
# one.
|
59
|
+
# @return [Boolean] true if both objects have equivalent values
|
28
60
|
#
|
29
|
-
# @
|
61
|
+
# @raise [ArgumentError] if no block is provided
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# matcher = Eq.new([1, 2, 3])
|
65
|
+
# matcher.match? { [1, 2, 3] } # => true
|
66
|
+
# matcher.match? { [1, 2, 3].dup } # => true
|
30
67
|
def match?
|
31
68
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
32
69
|
|
33
70
|
@expected.eql?(yield)
|
34
71
|
end
|
35
72
|
|
36
|
-
# Returns a
|
73
|
+
# Returns a human-readable description of the matcher.
|
74
|
+
#
|
75
|
+
# @api public
|
37
76
|
#
|
38
|
-
# @return [String]
|
77
|
+
# @return [String] A string describing what this matcher verifies
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# Eq.new("test").to_s # => 'eq "test"'
|
39
81
|
def to_s
|
40
82
|
"eq #{@expected.inspect}"
|
41
83
|
end
|
data/lib/matchi/match.rb
CHANGED
@@ -1,43 +1,83 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Pattern matching matcher that checks if a value matches a regular expression.
|
5
|
+
#
|
6
|
+
# This matcher verifies that a value matches a pattern using Ruby's Regexp#match? method.
|
7
|
+
# It's particularly useful for string validation, pattern matching, and text analysis.
|
8
|
+
# The matcher ensures secure pattern matching by requiring the pattern to respond to match?.
|
9
|
+
#
|
10
|
+
# @example Basic usage
|
11
|
+
# matcher = Matchi::Match.new(/^test/)
|
12
|
+
# matcher.match? { "test_string" } # => true
|
13
|
+
# matcher.match? { "other_string" } # => false
|
14
|
+
#
|
15
|
+
# @example Case sensitivity
|
16
|
+
# matcher = Matchi::Match.new(/^test$/i)
|
17
|
+
# matcher.match? { "TEST" } # => true
|
18
|
+
# matcher.match? { "Test" } # => true
|
19
|
+
# matcher.match? { "testing" } # => false
|
20
|
+
#
|
21
|
+
# @example Multiline patterns
|
22
|
+
# matcher = Matchi::Match.new(/\A\d+\Z/m)
|
23
|
+
# matcher.match? { "123" } # => true
|
24
|
+
# matcher.match? { "12.3" } # => false
|
25
|
+
#
|
26
|
+
# @see https://ruby-doc.org/core/Regexp.html#method-i-match-3F
|
5
27
|
class Match
|
6
|
-
# Initialize the matcher with
|
28
|
+
# Initialize the matcher with a pattern.
|
7
29
|
#
|
8
|
-
# @
|
9
|
-
#
|
30
|
+
# @api public
|
31
|
+
#
|
32
|
+
# @param expected [#match?] A pattern that responds to match?
|
33
|
+
#
|
34
|
+
# @raise [ArgumentError] if the pattern doesn't respond to match?
|
10
35
|
#
|
11
|
-
#
|
36
|
+
# @return [Match] a new instance of the matcher
|
12
37
|
#
|
13
|
-
# @
|
38
|
+
# @example
|
39
|
+
# Match.new(/\d+/) # Match digits
|
40
|
+
# Match.new(/^test$/i) # Case-insensitive match
|
41
|
+
# Match.new(/\A\w+\Z/) # Full string word characters
|
14
42
|
def initialize(expected)
|
15
43
|
raise ::ArgumentError, "expected must respond to match?" unless expected.respond_to?(:match?)
|
16
44
|
|
17
45
|
@expected = expected
|
18
46
|
end
|
19
47
|
|
20
|
-
#
|
48
|
+
# Checks if the yielded value matches the expected pattern.
|
21
49
|
#
|
22
|
-
#
|
23
|
-
#
|
50
|
+
# This method uses the pattern's match? method to perform the comparison.
|
51
|
+
# The match is performed on the entire string unless the pattern specifically
|
52
|
+
# allows partial matches.
|
53
|
+
#
|
54
|
+
# @api public
|
24
55
|
#
|
25
|
-
#
|
26
|
-
#
|
56
|
+
# @yield [] Block that returns the value to check
|
57
|
+
# @yieldreturn [#to_s] The value to match against the pattern
|
27
58
|
#
|
28
|
-
# @
|
29
|
-
# one.
|
59
|
+
# @return [Boolean] true if the value matches the pattern
|
30
60
|
#
|
31
|
-
# @
|
61
|
+
# @raise [ArgumentError] if no block is provided
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# matcher = Match.new(/^\d{3}-\d{2}-\d{4}$/)
|
65
|
+
# matcher.match? { "123-45-6789" } # => true
|
66
|
+
# matcher.match? { "123456789" } # => false
|
32
67
|
def match?
|
33
68
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
34
69
|
|
35
70
|
@expected.match?(yield)
|
36
71
|
end
|
37
72
|
|
38
|
-
# Returns a
|
73
|
+
# Returns a human-readable description of the matcher.
|
74
|
+
#
|
75
|
+
# @api public
|
39
76
|
#
|
40
|
-
# @return [String]
|
77
|
+
# @return [String] A string describing what this matcher verifies
|
78
|
+
#
|
79
|
+
# @example
|
80
|
+
# Match.new(/^test/).to_s # => "match /^test/"
|
41
81
|
def to_s
|
42
82
|
"match #{@expected.inspect}"
|
43
83
|
end
|
data/lib/matchi/predicate.rb
CHANGED
@@ -1,45 +1,109 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Matchi
|
4
|
-
#
|
4
|
+
# Predicate matcher that checks if an object responds to a predicate method with a truthy value.
|
5
|
+
#
|
6
|
+
# This matcher converts a predicate name (starting with 'be_' or 'have_') into a method call
|
7
|
+
# ending with '?' and verifies that calling this method returns a boolean value. It's useful
|
8
|
+
# for testing state-checking methods and collection properties. The matcher supports two types
|
9
|
+
# of predicate formats: 'be_*' which converts to '*?' and 'have_*' which converts to 'has_*?'.
|
10
|
+
#
|
11
|
+
# @example Basic empty check
|
12
|
+
# matcher = Matchi::Predicate.new(:be_empty)
|
13
|
+
# matcher.match? { [] } # => true
|
14
|
+
# matcher.match? { [1, 2] } # => false
|
15
|
+
#
|
16
|
+
# @example Object property check with arguments
|
17
|
+
# matcher = Matchi::Predicate.new(:have_key, :name)
|
18
|
+
# matcher.match? { { name: "Alice" } } # => true
|
19
|
+
# matcher.match? { { age: 30 } } # => false
|
20
|
+
#
|
21
|
+
# @example Using keyword arguments
|
22
|
+
# class Record
|
23
|
+
# def complete?(status: nil)
|
24
|
+
# status.nil? || status == :validated
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# matcher = Matchi::Predicate.new(:be_complete, status: :validated)
|
29
|
+
# matcher.match? { Record.new } # => true
|
30
|
+
#
|
31
|
+
# @example With block arguments
|
32
|
+
# class List
|
33
|
+
# def all?(&block)
|
34
|
+
# block ? super : empty?
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# matcher = Matchi::Predicate.new(:be_all) { |x| x.positive? }
|
39
|
+
# matcher.match? { [1, 2, 3] } # => true
|
40
|
+
# matcher.match? { [-1, 2, 3] } # => false
|
41
|
+
#
|
42
|
+
# @see https://ruby-doc.org/core/Object.html#method-i-respond_to-3F
|
5
43
|
class Predicate
|
6
|
-
#
|
44
|
+
# Mapping of predicate prefixes to their method name transformations.
|
45
|
+
# Each entry defines how a prefix should be converted to its method form.
|
7
46
|
#
|
8
|
-
# @
|
9
|
-
|
47
|
+
# @api private
|
48
|
+
PREFIXES = {
|
49
|
+
"be_" => ->(name) { "#{name.gsub(/\A(?:be_)/, "")}?" },
|
50
|
+
"have_" => ->(name) { "#{name.gsub(/\A(?:have_)/, "has_")}?" }
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
# Initialize the matcher with a predicate name and optional arguments.
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
#
|
57
|
+
# @param name [#to_s] A matcher name starting with 'be_' or 'have_'
|
58
|
+
# @param args [Array] Optional positional arguments to pass to the predicate method
|
59
|
+
# @param kwargs [Hash] Optional keyword arguments to pass to the predicate method
|
60
|
+
# @param block [Proc] Optional block to pass to the predicate method
|
61
|
+
#
|
62
|
+
# @raise [ArgumentError] if the predicate name format is invalid
|
63
|
+
#
|
64
|
+
# @return [Predicate] a new instance of the matcher
|
10
65
|
#
|
11
|
-
#
|
66
|
+
# @example With simple predicate
|
67
|
+
# Predicate.new(:be_empty) # Empty check
|
12
68
|
#
|
13
|
-
# @
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# @
|
69
|
+
# @example With arguments
|
70
|
+
# Predicate.new(:have_key, :id) # Key presence check
|
71
|
+
#
|
72
|
+
# @example With keyword arguments
|
73
|
+
# Predicate.new(:be_valid, status: true) # Conditional validation
|
17
74
|
def initialize(name, *args, **kwargs, &block)
|
18
75
|
@name = String(name)
|
19
76
|
raise ::ArgumentError, "invalid predicate name format" unless valid_name?
|
20
77
|
|
21
|
-
@args
|
78
|
+
@args = args
|
22
79
|
@kwargs = kwargs
|
23
|
-
@block
|
80
|
+
@block = block
|
24
81
|
end
|
25
82
|
|
26
|
-
#
|
83
|
+
# Checks if the yielded object responds to and returns true for the predicate.
|
27
84
|
#
|
28
|
-
#
|
29
|
-
#
|
85
|
+
# This method converts the predicate name into a method name according to the prefix
|
86
|
+
# mapping and calls it on the yielded object with any provided arguments. The method
|
87
|
+
# must return a boolean value, or a TypeError will be raised.
|
30
88
|
#
|
31
|
-
#
|
32
|
-
# matcher.match? { [] } # => true
|
89
|
+
# @api public
|
33
90
|
#
|
34
|
-
# @
|
35
|
-
#
|
91
|
+
# @yield [] Block that returns the object to check
|
92
|
+
# @yieldreturn [Object] The object to call the predicate method on
|
36
93
|
#
|
37
|
-
#
|
38
|
-
# matcher.match? { { foo: 42 } } # => true
|
94
|
+
# @return [Boolean] true if the predicate method returns true
|
39
95
|
#
|
40
|
-
# @
|
96
|
+
# @raise [ArgumentError] if no block is provided
|
97
|
+
# @raise [TypeError] if predicate method returns non-boolean value
|
41
98
|
#
|
42
|
-
# @
|
99
|
+
# @example Basic usage
|
100
|
+
# matcher = Predicate.new(:be_empty)
|
101
|
+
# matcher.match? { [] } # => true
|
102
|
+
# matcher.match? { [1] } # => false
|
103
|
+
#
|
104
|
+
# @example With arguments
|
105
|
+
# matcher = Predicate.new(:have_key, :id)
|
106
|
+
# matcher.match? { { id: 1 } } # => true
|
43
107
|
def match?
|
44
108
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
45
109
|
|
@@ -49,9 +113,20 @@ module Matchi
|
|
49
113
|
raise ::TypeError, "Boolean expected, but #{value.class} instance returned."
|
50
114
|
end
|
51
115
|
|
52
|
-
# Returns a
|
116
|
+
# Returns a human-readable description of the matcher.
|
117
|
+
#
|
118
|
+
# @api public
|
53
119
|
#
|
54
|
-
# @return [String]
|
120
|
+
# @return [String] A string describing what this matcher verifies
|
121
|
+
#
|
122
|
+
# @example Simple predicate
|
123
|
+
# Predicate.new(:be_empty).to_s # => "be empty"
|
124
|
+
#
|
125
|
+
# @example With arguments
|
126
|
+
# Predicate.new(:have_key, :id).to_s # => "have key :id"
|
127
|
+
#
|
128
|
+
# @example With keyword arguments
|
129
|
+
# Predicate.new(:be_valid, active: true).to_s # => "be valid active: true"
|
55
130
|
def to_s
|
56
131
|
(
|
57
132
|
"#{@name.tr("_", " ")} " + [
|
@@ -64,20 +139,34 @@ module Matchi
|
|
64
139
|
|
65
140
|
private
|
66
141
|
|
67
|
-
#
|
142
|
+
# Converts the predicate name into the actual method name to call.
|
143
|
+
#
|
144
|
+
# @api private
|
145
|
+
#
|
146
|
+
# @return [Symbol] The method name to call on the object
|
147
|
+
# @raise [ArgumentError] if the predicate prefix is unknown
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# # With be_ prefix
|
151
|
+
# method_name # => :empty? (from be_empty)
|
152
|
+
# # With have_ prefix
|
153
|
+
# method_name # => :has_key? (from have_key)
|
68
154
|
def method_name
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
155
|
+
_, transform = PREFIXES.find { |prefix, _| @name.start_with?(prefix) }
|
156
|
+
return transform.call(@name) if transform
|
157
|
+
|
158
|
+
raise ::ArgumentError, "unknown prefix in predicate name: #{@name}"
|
74
159
|
end
|
75
160
|
|
76
|
-
#
|
161
|
+
# Verifies that the predicate name follows the required format.
|
162
|
+
#
|
163
|
+
# @api private
|
164
|
+
#
|
165
|
+
# @return [Boolean] true if the name follows the required format
|
77
166
|
def valid_name?
|
78
|
-
return false if @name.
|
167
|
+
return false if @name.match?(/[?!]\z/)
|
79
168
|
|
80
|
-
@name.start_with?(
|
169
|
+
PREFIXES.keys.any? { |prefix| @name.start_with?(prefix) }
|
81
170
|
end
|
82
171
|
end
|
83
172
|
end
|