matchi 4.1.1 → 5.0.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 +87 -294
- data/lib/matchi/be.rb +11 -24
- data/lib/matchi/be_a_kind_of.rb +21 -63
- data/lib/matchi/be_an_instance_of.rb +24 -101
- data/lib/matchi/be_within/of.rb +39 -33
- data/lib/matchi/be_within.rb +19 -25
- data/lib/matchi/change/delta.rb +56 -0
- data/lib/matchi/change/from.rb +22 -32
- data/lib/matchi/change/from_to.rb +46 -0
- data/lib/matchi/change/to.rb +19 -34
- data/lib/matchi/change.rb +85 -86
- data/lib/matchi/constant.rb +55 -0
- data/lib/matchi/eq.rb +26 -25
- data/lib/matchi/match.rb +30 -26
- data/lib/matchi/number.rb +37 -0
- data/lib/matchi/predicate.rb +46 -56
- data/lib/matchi/raise_exception.rb +75 -40
- data/lib/matchi/satisfy.rb +22 -26
- data/lib/matchi.rb +22 -306
- metadata +18 -11
- data/lib/matchi/change/by.rb +0 -60
- data/lib/matchi/change/by_at_least.rb +0 -61
- data/lib/matchi/change/by_at_most.rb +0 -61
- data/lib/matchi/change/from/to.rb +0 -65
|
@@ -1,119 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# This matcher aims to provide a more reliable way to check if an object is an exact
|
|
7
|
-
# instance of a specific class (not a subclass). While not foolproof, it uses a more
|
|
8
|
-
# robust method to get the actual class of an object that helps resist common
|
|
9
|
-
# attempts at type checking manipulation.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic usage
|
|
12
|
-
# require "matchi/be_an_instance_of"
|
|
13
|
-
#
|
|
14
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
|
15
|
-
# matcher.match? { "foo" } # => true
|
|
16
|
-
# matcher.match? { :foo } # => false
|
|
17
|
-
#
|
|
18
|
-
# @example Enhanced class checking in practice
|
|
19
|
-
# # Consider a class that attempts to masquerade as String by overriding
|
|
20
|
-
# # common type checking methods:
|
|
21
|
-
# class MaliciousString
|
|
22
|
-
# def class
|
|
23
|
-
# ::String
|
|
24
|
-
# end
|
|
25
|
-
#
|
|
26
|
-
# def instance_of?(klass)
|
|
27
|
-
# self.class == klass
|
|
28
|
-
# end
|
|
29
|
-
#
|
|
30
|
-
# def is_a?(klass)
|
|
31
|
-
# "".is_a?(klass) # Delegates to a real String
|
|
32
|
-
# end
|
|
6
|
+
# Exact type matcher: passes when the class of the actual value is the
|
|
7
|
+
# expected class itself, not a descendant.
|
|
33
8
|
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
9
|
+
# The class of the actual value is read with `Kernel#class` bound to it, so
|
|
10
|
+
# an object that redefines `class` cannot lie, and a `BasicObject` is
|
|
11
|
+
# handled. The expected class can be given directly, or by name; see
|
|
12
|
+
# {Matchi::Constant}.
|
|
38
13
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# obj.kind_of?(String) # => true
|
|
43
|
-
# obj.instance_of?(String) # => true
|
|
44
|
-
#
|
|
45
|
-
# # Using our enhanced checking approach:
|
|
46
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
|
47
|
-
# matcher.match? { obj } # => false
|
|
14
|
+
# @example
|
|
15
|
+
# Matchi::BeAnInstanceOf.new(Integer).match? { 42 } # => true
|
|
16
|
+
# Matchi::BeAnInstanceOf.new(:Numeric).match? { 42 } # => false
|
|
48
17
|
class BeAnInstanceOf
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @example
|
|
52
|
-
# require "matchi/be_an_instance_of"
|
|
53
|
-
#
|
|
54
|
-
# Matchi::BeAnInstanceOf.new(String)
|
|
55
|
-
# Matchi::BeAnInstanceOf.new("String")
|
|
56
|
-
# Matchi::BeAnInstanceOf.new(:String)
|
|
57
|
-
#
|
|
58
|
-
# @param expected [Class, #to_s] The expected class name
|
|
59
|
-
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
|
18
|
+
# @param expected [Class, String, Symbol] the expected class, or its name
|
|
19
|
+
# @raise [ArgumentError] when expected is neither a Class nor a constant name
|
|
60
20
|
def initialize(expected)
|
|
61
|
-
@expected =
|
|
62
|
-
return if /\A[A-Z]/.match?(@expected)
|
|
63
|
-
|
|
64
|
-
raise ::ArgumentError,
|
|
65
|
-
"expected must start with an uppercase letter (got: #{@expected})"
|
|
21
|
+
@expected = Constant.normalize(expected, ::Class)
|
|
66
22
|
end
|
|
67
23
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
# 1. ::Object.instance_method(:class) retrieves the original, unoverridden 'class'
|
|
74
|
-
# method from the Object class
|
|
75
|
-
# 2. .bind_call(obj) binds this original method to our object and calls it,
|
|
76
|
-
# ensuring we get the real class regardless of method overrides
|
|
77
|
-
#
|
|
78
|
-
# This approach is more reliable than obj.class because it uses Ruby's method
|
|
79
|
-
# binding mechanism to call the original implementation directly. While not
|
|
80
|
-
# completely foolproof, it provides better protection against type check spoofing
|
|
81
|
-
# than using regular method calls which can be overridden.
|
|
82
|
-
#
|
|
83
|
-
# @example Basic class check
|
|
84
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
|
85
|
-
# matcher.match? { "test" } # => true
|
|
86
|
-
# matcher.match? { StringIO.new } # => false
|
|
87
|
-
#
|
|
88
|
-
# @see https://ruby-doc.org/core/Method.html#method-i-bind_call
|
|
89
|
-
# @see https://ruby-doc.org/core/UnboundMethod.html
|
|
90
|
-
#
|
|
91
|
-
# @yieldreturn [Object] the actual value to check
|
|
92
|
-
# @return [Boolean] true if the object's actual class is exactly the expected class
|
|
93
|
-
# @raise [ArgumentError] if no block is provided
|
|
24
|
+
# @yieldreturn [Object] the actual value
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
# @raise [ArgumentError] when no block is given
|
|
27
|
+
# @raise [NameError] when the name is not defined
|
|
28
|
+
# @raise [TypeError] when the name does not resolve to a Class
|
|
94
29
|
def match?
|
|
95
30
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
96
31
|
|
|
97
|
-
actual_class = ::
|
|
98
|
-
expected_class == actual_class
|
|
99
|
-
end
|
|
32
|
+
actual_class = ::Kernel.instance_method(:class).bind_call(yield)
|
|
100
33
|
|
|
101
|
-
|
|
102
|
-
#
|
|
103
|
-
# @return [String] a human-readable description of the matcher
|
|
104
|
-
def to_s
|
|
105
|
-
"be an instance of #{@expected}"
|
|
34
|
+
Constant.resolve(@expected, ::Class).equal?(actual_class)
|
|
106
35
|
end
|
|
107
36
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# This method handles both string and symbol class names through constant resolution.
|
|
112
|
-
#
|
|
113
|
-
# @return [Class] the resolved class
|
|
114
|
-
# @raise [NameError] if the class doesn't exist
|
|
115
|
-
def expected_class
|
|
116
|
-
::Object.const_get(@expected)
|
|
37
|
+
# @return [String]
|
|
38
|
+
def to_s
|
|
39
|
+
"be an instance of #{Constant.label(@expected)}"
|
|
117
40
|
end
|
|
118
41
|
end
|
|
119
42
|
end
|
data/lib/matchi/be_within/of.rb
CHANGED
|
@@ -1,50 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../number"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
6
|
class BeWithin
|
|
5
|
-
#
|
|
7
|
+
# Numeric proximity matcher: passes when the actual value is a core number
|
|
8
|
+
# whose distance to the expected value is at most delta.
|
|
9
|
+
#
|
|
10
|
+
# The actual value must be an `Integer`, a `Float`, a `Rational` or a
|
|
11
|
+
# `BigDecimal` (its class is read with `Kernel#class` bound to it). Any
|
|
12
|
+
# other value fails, including a user-defined `Numeric` subclass: such an
|
|
13
|
+
# object would control the subtraction through `coerce`.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# Matchi::BeWithin::Of.new(1, 42).match? { 41 } # => true
|
|
17
|
+
# Matchi::BeWithin::Of.new(1, 42).match? { "42" } # => false
|
|
6
18
|
class Of
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @
|
|
10
|
-
# require "matchi/be_within/of"
|
|
11
|
-
#
|
|
12
|
-
# Matchi::BeWithin::Of.new(1, 41)
|
|
13
|
-
#
|
|
14
|
-
# @param delta [Numeric] The accepted variation of the actual value.
|
|
15
|
-
# @param expected [Numeric] The expected value.
|
|
19
|
+
# @param delta [Numeric] the tolerance: a finite, real, non-negative number
|
|
20
|
+
# @param expected [Numeric] the center of the interval: a finite, real number
|
|
21
|
+
# @raise [ArgumentError] when an argument is not such a number
|
|
16
22
|
def initialize(delta, expected)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
|
20
|
-
|
|
21
|
-
@delta = delta
|
|
22
|
-
@expected = expected
|
|
23
|
+
@delta = Number.non_negative!(delta, "delta")
|
|
24
|
+
@expected = Number.finite_real!(expected, "expected")
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# @example
|
|
29
|
-
# require "matchi/be_within/of"
|
|
30
|
-
#
|
|
31
|
-
# matcher = Matchi::BeWithin::Of.new(1, 41)
|
|
32
|
-
# matcher.match? { 42 } # => true
|
|
33
|
-
#
|
|
34
|
-
# @yieldreturn [Numeric] The block of code to execute.
|
|
35
|
-
#
|
|
36
|
-
# @return [Boolean] Comparison between the actual and the expected values.
|
|
27
|
+
# @yieldreturn [Object] the actual value
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
# @raise [ArgumentError] when no block is given
|
|
37
30
|
def match?
|
|
38
31
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
39
32
|
|
|
40
|
-
|
|
33
|
+
actual = yield
|
|
34
|
+
return false unless core_number?(actual)
|
|
35
|
+
|
|
36
|
+
(@expected - actual).abs <= @delta
|
|
41
37
|
end
|
|
42
38
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
# @return [String] a human-readable description of the matcher
|
|
39
|
+
# @return [String]
|
|
46
40
|
def to_s
|
|
47
|
-
"be within #{@delta} of #{@expected}"
|
|
41
|
+
"be within #{@delta.inspect} of #{@expected.inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
CORE = [::Integer, ::Float, ::Rational].freeze
|
|
47
|
+
private_constant :CORE
|
|
48
|
+
|
|
49
|
+
def core_number?(actual)
|
|
50
|
+
klass = ::Kernel.instance_method(:class).bind_call(actual)
|
|
51
|
+
return true if CORE.include?(klass)
|
|
52
|
+
|
|
53
|
+
defined?(::BigDecimal) ? klass.equal?(::BigDecimal) : false
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
end
|
data/lib/matchi/be_within.rb
CHANGED
|
@@ -1,38 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "be_within/of"
|
|
4
|
+
require_relative "number"
|
|
4
5
|
|
|
5
6
|
module Matchi
|
|
6
|
-
#
|
|
7
|
+
# Builder for the `be within DELTA of EXPECTED` matcher. It is not a matcher
|
|
8
|
+
# itself: it does not respond to `match?`, so a framework that checks the
|
|
9
|
+
# matcher interface rejects a forgotten `of`.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Matchi::BeWithin.new(0.5).of(3).match? { 3.2 } # => true
|
|
7
13
|
class BeWithin
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# @example
|
|
11
|
-
# require "matchi/be_within"
|
|
12
|
-
#
|
|
13
|
-
# Matchi::BeWithin.new(1)
|
|
14
|
-
#
|
|
15
|
-
# @param delta [Numeric] A numeric value.
|
|
14
|
+
# @param delta [Numeric] the tolerance: a finite, real, non-negative number
|
|
15
|
+
# @raise [ArgumentError] when delta is not such a number
|
|
16
16
|
def initialize(delta)
|
|
17
|
-
|
|
18
|
-
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
|
19
|
-
|
|
20
|
-
@delta = delta
|
|
17
|
+
@delta = Number.non_negative!(delta, "delta")
|
|
21
18
|
end
|
|
22
19
|
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# @
|
|
26
|
-
# require "matchi/be_within"
|
|
27
|
-
#
|
|
28
|
-
# be_within_wrapper = Matchi::BeWithin.new(1)
|
|
29
|
-
# be_within_wrapper.of(41)
|
|
30
|
-
#
|
|
31
|
-
# @param expected [Numeric] The expected value.
|
|
32
|
-
#
|
|
33
|
-
# @return [#match?] A *be_within of* matcher.
|
|
20
|
+
# @param expected [Numeric] the center of the interval, a finite real number
|
|
21
|
+
# @return [Matchi::BeWithin::Of] the matcher
|
|
22
|
+
# @raise [ArgumentError] when expected is not a finite real Numeric
|
|
34
23
|
def of(expected)
|
|
35
24
|
Of.new(@delta, expected)
|
|
36
25
|
end
|
|
26
|
+
|
|
27
|
+
# @return [String]
|
|
28
|
+
def to_s
|
|
29
|
+
"be within #{@delta.inspect}"
|
|
30
|
+
end
|
|
37
31
|
end
|
|
38
32
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../number"
|
|
4
|
+
|
|
5
|
+
module Matchi
|
|
6
|
+
class Change
|
|
7
|
+
# Passes when the difference between the observed value after and before
|
|
8
|
+
# the block, `after - before`, is exactly the expected delta (`:by`), at
|
|
9
|
+
# least it (`:by_at_least`) or at most it (`:by_at_most`). The expected
|
|
10
|
+
# delta receives the comparison; the difference is computed by the
|
|
11
|
+
# observed values, so this matcher depends on their honesty.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# list = []
|
|
15
|
+
# Matchi::Change::Delta.new(:by, 2) { list.length }.match? { list.push(1, 2) } # => true
|
|
16
|
+
class Delta
|
|
17
|
+
# For each kind: the comparison the delta receives, and its description.
|
|
18
|
+
KINDS = {
|
|
19
|
+
by: [:==, "by"],
|
|
20
|
+
by_at_least: [:<=, "by at least"],
|
|
21
|
+
by_at_most: [:>=, "by at most"]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# @param kind [Symbol] `:by`, `:by_at_least` or `:by_at_most`
|
|
25
|
+
# @param delta [Numeric] a finite real number, non-negative unless kind is `:by`
|
|
26
|
+
# @yieldreturn [Object] the observed value
|
|
27
|
+
# @raise [ArgumentError] when the kind is unknown, delta is not such a number, or no block is given
|
|
28
|
+
def initialize(kind, delta, &state)
|
|
29
|
+
raise ::ArgumentError, "unknown kind #{kind.inspect}" unless KINDS.key?(kind)
|
|
30
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
31
|
+
|
|
32
|
+
@operator, @label = KINDS.fetch(kind)
|
|
33
|
+
@delta = kind == :by ? Number.finite_real!(delta, "delta") : Number.non_negative!(delta, "delta")
|
|
34
|
+
@state = state
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its effect
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
# @raise [ArgumentError] when no block is given
|
|
40
|
+
def match?
|
|
41
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
42
|
+
|
|
43
|
+
value_before = @state.call
|
|
44
|
+
yield
|
|
45
|
+
value_after = @state.call
|
|
46
|
+
|
|
47
|
+
@delta.public_send(@operator, value_after - value_before) ? true : false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [String]
|
|
51
|
+
def to_s
|
|
52
|
+
"change #{@label} #{@delta.inspect}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/matchi/change/from.rb
CHANGED
|
@@ -1,45 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "from_to"
|
|
4
4
|
|
|
5
5
|
module Matchi
|
|
6
6
|
class Change
|
|
7
|
-
#
|
|
7
|
+
# Builder for the `change from OLD to NEW` matcher. It is not a matcher
|
|
8
|
+
# itself: it does not respond to `match?`, so a forgotten `to` is rejected
|
|
9
|
+
# by any framework that checks the matcher interface.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# Matchi::Change.new(s, :to_s).from("a").to("b")
|
|
8
13
|
class From
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# @
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
# object = "foo"
|
|
15
|
-
#
|
|
16
|
-
# Matchi::Change::From.new("foo") { object.to_s }
|
|
17
|
-
#
|
|
18
|
-
# @param expected [#object_id] An expected initial value.
|
|
19
|
-
# @param state [Proc] A block of code to execute to get the
|
|
20
|
-
# state of the object.
|
|
21
|
-
def initialize(expected, &state)
|
|
14
|
+
# @param old_value [#eql?] the expected value before the block
|
|
15
|
+
# @yieldreturn [Object] the observed value
|
|
16
|
+
# @raise [ArgumentError] when no block is given
|
|
17
|
+
def initialize(old_value, &state)
|
|
22
18
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
23
19
|
|
|
24
|
-
@
|
|
25
|
-
@state
|
|
20
|
+
@old_value = old_value
|
|
21
|
+
@state = state
|
|
26
22
|
end
|
|
27
23
|
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
# @param expected_new_value [#object_id] The new value to expect.
|
|
39
|
-
#
|
|
40
|
-
# @return [#match?] A *change from to* matcher.
|
|
41
|
-
def to(expected_new_value)
|
|
42
|
-
To.new(@expected, expected_new_value, &@state)
|
|
24
|
+
# @param new_value [#eql?] the expected value after the block
|
|
25
|
+
# @return [Matchi::Change::FromTo]
|
|
26
|
+
def to(new_value)
|
|
27
|
+
FromTo.new(@old_value, new_value, &@state)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [String]
|
|
31
|
+
def to_s
|
|
32
|
+
"change from #{@old_value.inspect}"
|
|
43
33
|
end
|
|
44
34
|
end
|
|
45
35
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../eq"
|
|
4
|
+
|
|
5
|
+
module Matchi
|
|
6
|
+
class Change
|
|
7
|
+
# Passes when the observed value is the old value before the block and
|
|
8
|
+
# the new value after it, both compared like {Matchi::Eq}. The block is
|
|
9
|
+
# always evaluated; a wrong initial value is a failure of the
|
|
10
|
+
# expectation, like a wrong final value.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# s = +"a"
|
|
14
|
+
# Matchi::Change.new(s, :to_s).from("a").to("b").match? { s.replace("b") } # => true
|
|
15
|
+
class FromTo
|
|
16
|
+
# @param old_value [#eql?] the expected value before the block
|
|
17
|
+
# @param new_value [#eql?] the expected value after the block
|
|
18
|
+
# @yieldreturn [Object] the observed value
|
|
19
|
+
# @raise [ArgumentError] when no block is given
|
|
20
|
+
def initialize(old_value, new_value, &state)
|
|
21
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
22
|
+
|
|
23
|
+
@old_value = old_value
|
|
24
|
+
@new_value = new_value
|
|
25
|
+
@state = state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its effect
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
# @raise [ArgumentError] when no block is given
|
|
31
|
+
def match?
|
|
32
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
33
|
+
|
|
34
|
+
initial = Eq.eql?(@old_value, @state.call)
|
|
35
|
+
yield
|
|
36
|
+
|
|
37
|
+
initial && Eq.eql?(@new_value, @state.call)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String]
|
|
41
|
+
def to_s
|
|
42
|
+
"change from #{@old_value.inspect} to #{@new_value.inspect}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/matchi/change/to.rb
CHANGED
|
@@ -1,55 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../eq"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
6
|
class Change
|
|
5
|
-
#
|
|
7
|
+
# Passes when the observed value is not the expected value before the
|
|
8
|
+
# block and is the expected value after it: the value must change to it.
|
|
9
|
+
# The block is always evaluated.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# s = +"a"
|
|
13
|
+
# Matchi::Change.new(s, :to_s).to("b").match? { s.replace("b") } # => true
|
|
6
14
|
class To
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# @
|
|
10
|
-
# require "matchi/change/to"
|
|
11
|
-
#
|
|
12
|
-
# object = "foo"
|
|
13
|
-
#
|
|
14
|
-
# Matchi::Change::To.new("FOO") { object.to_s }
|
|
15
|
-
#
|
|
16
|
-
# @param expected [#object_id] An expected new value.
|
|
17
|
-
# @param state [Proc] A block of code to execute to get the
|
|
18
|
-
# state of the object.
|
|
15
|
+
# @param expected [#eql?] the expected value after the block
|
|
16
|
+
# @yieldreturn [Object] the observed value
|
|
17
|
+
# @raise [ArgumentError] when no block is given
|
|
19
18
|
def initialize(expected, &state)
|
|
20
19
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
21
20
|
|
|
22
21
|
@expected = expected
|
|
23
|
-
@state
|
|
22
|
+
@state = state
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# @example
|
|
30
|
-
# require "matchi/change/to"
|
|
31
|
-
#
|
|
32
|
-
# object = "foo"
|
|
33
|
-
#
|
|
34
|
-
# matcher = Matchi::Change::To.new("FOO") { object.to_s }
|
|
35
|
-
# matcher.match? { object.upcase! } # => true
|
|
36
|
-
#
|
|
37
|
-
# @yieldreturn [#object_id] The block of code to execute.
|
|
38
|
-
#
|
|
39
|
-
# @return [Boolean] Comparison between the value before and after the
|
|
40
|
-
# code execution.
|
|
25
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its effect
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
# @raise [ArgumentError] when no block is given
|
|
41
28
|
def match?
|
|
42
29
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
43
30
|
|
|
31
|
+
already = Eq.eql?(@expected, @state.call)
|
|
44
32
|
yield
|
|
45
|
-
value_after = @state.call
|
|
46
33
|
|
|
47
|
-
@expected
|
|
34
|
+
!already && Eq.eql?(@expected, @state.call)
|
|
48
35
|
end
|
|
49
36
|
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
# @return [String] a human-readable description of the matcher
|
|
37
|
+
# @return [String]
|
|
53
38
|
def to_s
|
|
54
39
|
"change to #{@expected.inspect}"
|
|
55
40
|
end
|