matchi 4.1.0 → 4.2.0
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 +4 -4
- data/LICENSE.md +1 -1
- 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 +17 -7
- 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
|