spectus 5.0.1 → 5.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11f7d2775c0de9fcba11f795927752e4f15aa1de85b63e2d5bd806b82c434bca
4
- data.tar.gz: ebf62bbee1e7d30003995918b368f0905ee2305a3fb79a242f940837d94c793b
3
+ metadata.gz: 27755e146e515eb8767ab7d71d9e5f712538376e89df1100f6629ba27f5635c2
4
+ data.tar.gz: 5e9c7e45e5f91f5a98aa62ea005732fec936c2dd456a8c52d9d50e55b81510ec
5
5
  SHA512:
6
- metadata.gz: 978d85aa4e676d8edea4c94644976a06284820b912602cb98808d1a39cf3ef3e2538bee688da3d7401593710de86b03f4b28b05cc0e6555c45741ca0782c78ad
7
- data.tar.gz: 9c51ec6b30adb0590bdd514c5c6c74474508ab146a72dbd7ebddc375a6dedc765a7201e18022514cc5634c5f4fb377d064acac2ae0a8832f491437a614fbd83c
6
+ metadata.gz: 885d638883c196a7511d2402c4113c44b4d1f9dcca8784dfdbd1bd7061c32da8fbfbf50c001d0e5c6c1320fef1dabdaa320643d760651067a1441cde0acfcd69
7
+ data.tar.gz: 202bef0668a0477517d5cfce46428b7c34f376dc5033018427533e8c3d6bdf1263ae816c96f50ef65ec6d093a3cc4654c0baa75345fcb7ccf5ca9ff6343ea33c
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2014-2024 Cyril Kato
3
+ Copyright (c) 2014-2025 Cyril Kato
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -2,126 +2,143 @@
2
2
 
3
3
  [![Version](https://img.shields.io/github/v/tag/fixrb/spectus?label=Version&logo=github)](https://github.com/fixrb/spectus/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/spectus/main)
5
- [![Ruby](https://github.com/fixrb/spectus/workflows/Ruby/badge.svg?branch=main)](https://github.com/fixrb/spectus/actions?query=workflow%3Aruby+branch%3Amain)
6
- [![RuboCop](https://github.com/fixrb/spectus/workflows/RuboCop/badge.svg?branch=main)](https://github.com/fixrb/spectus/actions?query=workflow%3Arubocop+branch%3Amain)
7
5
  [![License](https://img.shields.io/github/license/fixrb/spectus?label=License&logo=github)](https://github.com/fixrb/spectus/raw/main/LICENSE.md)
8
6
 
9
- > A Ruby library for defining expectations with precision, using [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) compliance levels. 🚥
7
+ > A Ruby testing library that brings precision to your expectations using RFC 2119 compliance levels. 🚥
8
+
9
+ ## Quick Start
10
+
11
+ ```ruby
12
+ require "spectus"
13
+ require "matchi"
14
+
15
+ # Define a must-have requirement
16
+ test = Spectus.must Matchi::Eq.new(42)
17
+ test.call { 42 } # => Pass ✅
18
+
19
+ # Define an optional feature
20
+ test = Spectus.may Matchi::Be.new(:empty?)
21
+ test.call { [].empty? } # => Pass ✅
22
+ ```
10
23
 
11
24
  ## Installation
12
25
 
13
- Add this line to your application's Gemfile:
26
+ Add to your Gemfile:
14
27
 
15
28
  ```ruby
16
29
  gem "spectus"
30
+ gem "matchi" # For matchers
17
31
  ```
18
32
 
19
- And then execute:
33
+ Or install directly:
20
34
 
21
- ```sh
22
- bundle install
35
+ ```bash
36
+ gem install spectus
37
+ gem install matchi
23
38
  ```
24
39
 
25
- Or install it yourself as:
40
+ ## Understanding RFC 2119
26
41
 
27
- ```sh
28
- gem install spectus
29
- ```
42
+ Spectus implements [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) requirement levels to bring clarity and precision to test expectations:
30
43
 
31
- ## Usage
44
+ - **MUST** (✅): Absolute requirement, no exceptions
45
+ - **SHOULD** (⚠️): Strong recommendation with valid exceptions
46
+ - **MAY** (💡): Optional feature
32
47
 
33
- The __Spectus__ library is basically a module defining methods that can be used to qualify expectations in specifications.
48
+ This approach helps you clearly communicate the importance of each test in your suite.
34
49
 
35
- To make __Spectus__ available:
50
+ ## Features
36
51
 
37
- ```ruby
38
- require "spectus"
39
- ```
52
+ ### Requirement Levels
40
53
 
41
- For convenience, we will also instantiate some matchers from the [Matchi library](https://github.com/fixrb/matchi):
54
+ | Level | Description | Pass Conditions |
55
+ |-------|-------------|-----------------|
56
+ | MUST | Absolute requirement | Only when exact match |
57
+ | SHOULD | Recommended behavior | When matches or has valid reason not to |
58
+ | MAY | Optional feature | When matches or not implemented |
42
59
 
43
- ```ruby
44
- require "matchi"
45
- ```
60
+ ### Results Classification
46
61
 
47
- All examples here assume that this has been done.
62
+ - **Pass Results:**
63
+ - ✅ Success (MUST level met)
64
+ - ⚠️ Warning (SHOULD level met)
65
+ - 💡 Info (MAY level met)
48
66
 
49
- ### Absolute Requirement
67
+ - **Fail Results:**
68
+ - ❌ Failure (requirement not met)
69
+ - 💥 Error (unexpected exception)
50
70
 
51
- There is exactly one bat:
71
+ ## Usage Examples
72
+
73
+ ### Testing Required Behavior
52
74
 
53
75
  ```ruby
54
- definition = Spectus.must Matchi::Be.new(1)
55
- definition.call { "🦇".size }
56
- # => Expresenter::Pass(actual: 1, definition: "be 1", error: nil, expected: 1, got: true, negate: false, level: :MUST)
76
+ test = Spectus.must Matchi::Be.new(1)
77
+ test.call { "🦇".size } # Must be exactly 1
57
78
  ```
58
79
 
59
- The test is passed.
60
-
61
- ### Absolute Prohibition
62
-
63
- Truth and lies:
80
+ ### Testing Recommended Behavior
64
81
 
65
82
  ```ruby
66
- definition = Spectus.must_not Matchi::Be.new(true)
67
- definition.call { false }
68
- # => Expresenter::Pass(actual: false, definition: "be true", error: nil, expected: true, got: true, negate: true, level: :MUST)
83
+ test = Spectus.should Matchi::Be.new(0.3)
84
+ test.call { 0.1 + 0.2 } # Should be close to 0.3
69
85
  ```
70
86
 
71
- ### Recommended
72
-
73
- A well-known joke. The addition of `0.1` and `0.2` is deadly precise:
87
+ ### Testing Optional Features
74
88
 
75
89
  ```ruby
76
- definition = Spectus.should Matchi::Be.new(0.3)
77
- definition.call { 0.1 + 0.2 }
78
- # => Expresenter::Pass(actual: 0.30000000000000004, definition: "be 0.3", error: nil, expected: 0.3, got: false, negate: false, level: :SHOULD)
90
+ test = Spectus.may Matchi::Be.new(true)
91
+ test.call { [].blank? } # May implement blank? method
79
92
  ```
80
93
 
81
- ### Not Recommended
94
+ ## Advanced Usage
82
95
 
83
- This should not be wrong:
96
+ <details>
97
+ <summary>Click to expand custom matcher example</summary>
84
98
 
85
99
  ```ruby
86
- definition = Spectus.should_not Matchi::Match.new("123456")
87
-
88
- definition.call do
89
- require "securerandom"
90
-
91
- SecureRandom.hex(3)
100
+ class PositiveNumber
101
+ def match?
102
+ yield.positive?
103
+ end
92
104
  end
93
- # => Expresenter::Pass(actual: "bb5716", definition: "match \"123456\"", error: nil, expected: "123456", got: true, negate: true, level: :SHOULD)
94
- ```
95
105
 
96
- In any case, as long as there are no exceptions, the test passes.
97
-
98
- ### Optional
106
+ test = Spectus.must PositiveNumber.new
107
+ test.call { 42 } # => Pass
108
+ ```
109
+ </details>
99
110
 
100
- An empty array is blank, right?
111
+ <details>
112
+ <summary>Click to expand integration example</summary>
101
113
 
102
114
  ```ruby
103
- definition = Spectus.may Matchi::Be.new(true)
104
- definition.call { [].blank? }
105
- # => Expresenter::Pass(actual: nil, definition: "be true", error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: true, got: nil, negate: false, level: :MAY)
106
- ```
107
-
108
- My bad! ActiveSupport was not imported. 🤦‍♂️
115
+ require "spectus"
116
+ require "matchi"
109
117
 
110
- Anyways, the test passes because the exception produced is `NoMethodError`, meaning that the functionality is not implemented.
118
+ RSpec.describe Calculator do
119
+ it "must perform exact arithmetic" do
120
+ test = Spectus.must Matchi::Eq.new(4)
121
+ expect { test.call { 2 + 2 } }.not_to raise_error
122
+ end
123
+ end
124
+ ```
125
+ </details>
111
126
 
112
- ## Contact
127
+ ## Related Projects
113
128
 
114
- * Home page: https://github.com/fixrb/spectus
115
- * Bugs/issues: https://github.com/fixrb/spectus/issues
116
- * Blog post: https://cyrilllllll.medium.com/a-spectus-tutorial-expectations-with-rfc-2119-compliance-1fc769861c1
129
+ - [Matchi](https://github.com/fixrb/matchi) - Collection of compatible matchers
130
+ - [Test Tube](https://github.com/fixrb/test_tube) - Underlying test execution engine
131
+ - [Expresenter](https://github.com/fixrb/expresenter) - Test result presentation
117
132
 
118
- ## Versioning
133
+ ## License
119
134
 
120
- __Spectus__ follows [Semantic Versioning 2.0](https://semver.org/).
135
+ Released under the [MIT License](LICENSE.md).
121
136
 
122
- ## License
137
+ ## Support
123
138
 
124
- The [gem](https://rubygems.org/gems/spectus) is available as open source under the terms of the [MIT License](https://github.com/fixrb/spectus/raw/main/LICENSE.md).
139
+ - Issues: [GitHub Issues](https://github.com/fixrb/spectus/issues)
140
+ - Documentation: [RubyDoc](https://rubydoc.info/github/fixrb/spectus/main)
141
+ - Blog Post: [Medium Article](https://cyrilllllll.medium.com/a-spectus-tutorial-expectations-with-rfc-2119-compliance-1fc769861c1)
125
142
 
126
143
  ## Sponsors
127
144
 
@@ -6,23 +6,42 @@ require "test_tube"
6
6
  module Spectus
7
7
  # Namespace for the requirement levels.
8
8
  module Requirement
9
- # Requirement level's base class.
9
+ # Base class for implementing RFC 2119 requirement levels.
10
+ #
11
+ # This class provides the core functionality for running tests against
12
+ # different requirement levels (MUST, SHOULD, MAY). It uses TestTube for
13
+ # test execution and Expresenter for result presentation.
14
+ #
15
+ # @see https://github.com/fixrb/test_tube Test execution
16
+ # @see https://github.com/fixrb/expresenter Result presentation
10
17
  class Base
11
18
  # Initialize the requirement level class.
12
19
  #
13
- # @param matcher [#match?] The matcher.
14
- # @param negate [Boolean] Invert the matcher or not.
20
+ # @param matcher [#match?] The matcher used to evaluate the test
21
+ # @param negate [Boolean] When true, inverts the matcher's result
22
+ #
23
+ # @raise [ArgumentError] If matcher doesn't respond to match?
15
24
  def initialize(matcher:, negate:)
16
- @matcher = matcher
17
- @negate = negate
25
+ raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
26
+
27
+ @matcher = matcher
28
+ @negate = negate
18
29
  end
19
30
 
20
- # Test result.
31
+ # Execute the test and return its result.
21
32
  #
22
- # @raise [::Expresenter::Fail] A failed spec exception.
23
- # @return [::Expresenter::Pass] A passed spec instance.
33
+ # Runs the provided block through the matcher and evaluates the result
34
+ # according to the requirement level's rules. The result is presented
35
+ # through an Expresenter instance containing all test details.
24
36
  #
25
- # @see https://github.com/fixrb/expresenter
37
+ # @example
38
+ # test = Base.new(matcher: SomeMatcher.new, negate: false)
39
+ # test.call { some_value }
40
+ # # => #<Expresenter::Pass actual: some_value, ...>
41
+ #
42
+ # @yield The block containing the code to test
43
+ # @raise [::Expresenter::Fail] When the test fails
44
+ # @return [::Expresenter::Pass] When the test passes
26
45
  #
27
46
  # @api public
28
47
  def call(&)
@@ -38,36 +57,16 @@ module Spectus
38
57
  )
39
58
  end
40
59
 
41
- # :nocov:
42
-
43
- # A string containing a human-readable representation of the definition.
44
- #
45
- # @example The human-readable representation of an absolute requirement.
46
- # require "spectus"
47
- # require "matchi/be"
48
- #
49
- # definition = Spectus.must Matchi::Be.new(1)
50
- # definition.inspect
51
- # # => "#<MUST Matchi::Be(1) negate=false>"
52
- #
53
- # @return [String] The human-readable representation of the definition.
54
- #
55
- # @api public
56
- def inspect
57
- "#<#{self.class.level} #{@matcher.inspect} negate=#{@negate}>"
58
- end
59
-
60
- # :nocov:
61
-
62
60
  private
63
61
 
64
- # Code experiment result.
65
- #
66
- # @param test [::TestTube::Base] The state of the experiment.
62
+ # Determine if the test passed according to the requirement level's rules.
67
63
  #
68
- # @see https://github.com/fixrb/test_tube
64
+ # This base implementation considers the test passed if the matcher
65
+ # returned true. Subclasses may override this method to implement
66
+ # specific requirement level rules.
69
67
  #
70
- # @return [Boolean] The result of the test (passed or failed).
68
+ # @param test [::TestTube::Base] The test execution state
69
+ # @return [Boolean] true if the test passed, false otherwise
71
70
  def passed?(test)
72
71
  test.got.equal?(true)
73
72
  end
@@ -4,21 +4,39 @@ require_relative "base"
4
4
 
5
5
  module Spectus
6
6
  module Requirement
7
- # Optional requirement level.
7
+ # Implementation of MAY requirements from RFC 2119.
8
+ #
9
+ # This level represents optional features. A test at this level
10
+ # passes in two cases:
11
+ # - When the feature is implemented and the matcher returns true
12
+ # - When NoMethodError is raised, indicating the feature is not implemented
13
+ #
14
+ # @example Testing a MAY requirement with implemented feature
15
+ # test = Optional.new(matcher: some_matcher, negate: false)
16
+ # test.call { implemented_feature } # Passes if matcher returns true
17
+ #
18
+ # @example Testing a MAY requirement with unimplemented feature
19
+ # test = Optional.new(matcher: some_matcher, negate: false)
20
+ # test.call { unimplemented_feature } # Passes if NoMethodError is raised
21
+ #
22
+ # @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 MAY keyword
8
23
  class Optional < Base
9
- # Key word for use in RFCs to indicate requirement levels.
24
+ # The RFC 2119 keyword for this requirement level.
10
25
  #
11
- # @return [Symbol] The requirement level.
26
+ # @return [Symbol] :MAY indicating an optional requirement
27
+ # @api public
12
28
  def self.level
13
29
  :MAY
14
30
  end
15
31
 
16
32
  private
17
33
 
18
- # Code experiment result.
34
+ # Determine if the test passed according to MAY requirement rules.
35
+ # A test passes if either:
36
+ # - The base matcher validation passes (super)
37
+ # - The feature is not implemented (NoMethodError)
19
38
  #
20
39
  # @param (see Base#passed?)
21
- #
22
40
  # @return (see Base#passed?)
23
41
  def passed?(test)
24
42
  super || test.error.is_a?(::NoMethodError)
@@ -4,21 +4,39 @@ require_relative "base"
4
4
 
5
5
  module Spectus
6
6
  module Requirement
7
- # Recommended and not recommended requirement levels.
7
+ # Implementation of SHOULD/SHOULD NOT requirements from RFC 2119.
8
+ #
9
+ # This level is less strict than MUST requirements. A test at this level
10
+ # passes in two cases:
11
+ # - When the matcher returns the expected result
12
+ # - When no error was raised during the test
13
+ #
14
+ # @example Testing a SHOULD requirement
15
+ # test = Recommended.new(matcher: some_matcher, negate: false)
16
+ # test.call { value } # Passes if matcher returns true or no error occurs
17
+ #
18
+ # @example Testing a SHOULD NOT requirement
19
+ # test = Recommended.new(matcher: some_matcher, negate: true)
20
+ # test.call { value } # Passes if matcher returns false or no error occurs
21
+ #
22
+ # @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 SHOULD/SHOULD NOT keywords
8
23
  class Recommended < Base
9
- # Key word for use in RFCs to indicate requirement levels.
24
+ # The RFC 2119 keyword for this requirement level.
10
25
  #
11
- # @return [Symbol] The requirement level.
26
+ # @return [Symbol] :SHOULD indicating a recommended requirement
27
+ # @api public
12
28
  def self.level
13
29
  :SHOULD
14
30
  end
15
31
 
16
32
  private
17
33
 
18
- # Code experiment result.
34
+ # Determine if the test passed according to SHOULD requirement rules.
35
+ # A test passes if either:
36
+ # - The base matcher validation passes (super)
37
+ # - No error occurred during test execution
19
38
  #
20
39
  # @param (see Base#passed?)
21
- #
22
40
  # @return (see Base#passed?)
23
41
  def passed?(test)
24
42
  super || test.error.nil?
@@ -4,11 +4,30 @@ require_relative "base"
4
4
 
5
5
  module Spectus
6
6
  module Requirement
7
- # Absolute requirement and absolute prohibition levels.
7
+ # Implementation of MUST/MUST NOT requirements from RFC 2119.
8
+ #
9
+ # This level represents an absolute requirement - tests at this level
10
+ # must pass without any exceptions or conditions. Unlike SHOULD or MAY levels,
11
+ # there is no flexibility in what constitutes a passing test.
12
+ #
13
+ # The test passes only when:
14
+ # - MUST: The matcher returns true (when negate: false)
15
+ # - MUST NOT: The matcher returns false (when negate: true)
16
+ #
17
+ # @example Testing a MUST requirement
18
+ # test = Required.new(matcher: some_matcher, negate: false)
19
+ # test.call { value } # Passes only if matcher returns true
20
+ #
21
+ # @example Testing a MUST NOT requirement
22
+ # test = Required.new(matcher: some_matcher, negate: true)
23
+ # test.call { value } # Passes only if matcher returns false
24
+ #
25
+ # @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 MUST/MUST NOT keywords
8
26
  class Required < Base
9
- # Key word for use in RFCs to indicate requirement levels.
27
+ # The RFC 2119 keyword for this requirement level.
10
28
  #
11
- # @return [Symbol] The requirement level.
29
+ # @return [Symbol] :MUST indicating an absolute requirement
30
+ # @api public
12
31
  def self.level
13
32
  :MUST
14
33
  end
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Spectus
4
- # Namespace for the results.
4
+ # Namespace for RFC 2119 requirement levels.
5
+ #
6
+ # This module contains different requirement level implementations:
7
+ # - Required (MUST/MUST NOT)
8
+ # - Recommended (SHOULD/SHOULD NOT)
9
+ # - Optional (MAY)
10
+ #
11
+ # Each level has its own rules for determining test success/failure.
5
12
  #
6
13
  # @api private
7
14
  module Requirement
data/lib/spectus.rb CHANGED
@@ -2,106 +2,207 @@
2
2
 
3
3
  require_relative File.join("spectus", "requirement")
4
4
 
5
- # Namespace for the Spectus library.
5
+ # A Ruby library for defining expectations with precision using RFC 2119 compliance levels.
6
6
  #
7
- # This module defines methods that can be used to qualify expectations in
8
- # specifications.
7
+ # This module provides methods to define expectations according to different requirement
8
+ # levels (MUST, SHOULD, MAY). Each method accepts a matcher object that responds to `match?`
9
+ # and follows the block-passing protocol.
10
+ #
11
+ # While the {https://github.com/fixrb/matchi Matchi gem} provides a collection of ready-to-use
12
+ # matchers, you can create your own custom matchers:
13
+ #
14
+ # @example Creating a custom matcher
15
+ # class BeTheAnswer
16
+ # def match?
17
+ # 42.equal?(yield)
18
+ # end
19
+ # end
20
+ #
21
+ # test = Spectus.must BeTheAnswer.new
22
+ # test.call { 42 } # => pass
23
+ # test.call { 41 } # => fail
24
+ #
25
+ # @example Using with Matchi gem
26
+ # require "spectus"
27
+ # require "matchi/eq"
28
+ #
29
+ # test = Spectus.must Matchi::Eq.new(42)
30
+ # test.call { 42 } # => pass
31
+ #
32
+ # @see https://www.ietf.org/rfc/rfc2119.txt RFC 2119 specification
33
+ # @see https://github.com/fixrb/matchi Matchi - Collection of compatible matchers
9
34
  module Spectus
10
- # This method mean that the definition is an absolute requirement of the
11
- # specification.
35
+ # Defines an absolute requirement that must be satisfied by the implementation.
36
+ # This represents the RFC 2119 "MUST" level - an absolute requirement of the specification.
12
37
  #
13
- # @example An absolute requirement definition
38
+ # @example With a custom matcher
39
+ # class PositiveNumber
40
+ # def match?
41
+ # (yield).positive?
42
+ # end
43
+ # end
44
+ #
45
+ # test = Spectus.must PositiveNumber.new
46
+ # test.call { 42 }
47
+ # # => #<Expresenter::Pass actual: 42, ...>
48
+ #
49
+ # @example With Matchi gem
14
50
  # require "spectus"
15
51
  # require "matchi/eq"
16
52
  #
17
- # Spectus.must Matchi::Eq.new("FOO")
18
- # # => #<MUST Matchi::Eq("FOO") negate=false>
19
- #
20
- # @param matcher [#match?] The matcher.
53
+ # test = Spectus.must Matchi::Eq.new(42)
54
+ # test.call { 42 }
55
+ # # => #<Expresenter::Pass actual: 42, ...>
21
56
  #
22
- # @return [Requirement::Required] An absolute requirement level instance.
57
+ # @param matcher [#match?] Any object that implements the matcher protocol:
58
+ # - Responds to `match?`
59
+ # - Accepts a block in `match?` that provides the actual value
60
+ # @return [Requirement::Required] An absolute requirement level instance
61
+ # @raise [ArgumentError] If matcher doesn't respond to match?
23
62
  #
24
63
  # @api public
25
64
  def self.must(matcher)
65
+ raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
66
+
26
67
  Requirement::Required.new(negate: false, matcher:)
27
68
  end
28
69
 
29
- # This method mean that the definition is an absolute prohibition of the specification.
70
+ # Defines an absolute prohibition in the specification.
71
+ # This represents the RFC 2119 "MUST NOT" level - an absolute prohibition.
72
+ #
73
+ # @example With a custom matcher
74
+ # class NegativeNumber
75
+ # def match?
76
+ # (yield).negative?
77
+ # end
78
+ # end
30
79
  #
31
- # @example An absolute prohibition definition
80
+ # test = Spectus.must_not NegativeNumber.new
81
+ # test.call { 42 }
82
+ # # => #<Expresenter::Pass actual: 42, ...>
83
+ #
84
+ # @example With Matchi gem
32
85
  # require "spectus"
33
86
  # require "matchi/be"
34
87
  #
35
- # Spectus.must_not Matchi::Be.new(42)
36
- # # => #<MUST Matchi::Be(42) negate=true>
37
- #
38
- # @param matcher [#match?] The matcher.
88
+ # test = Spectus.must_not Matchi::Be.new(0)
89
+ # test.call { 42 }
90
+ # # => #<Expresenter::Pass actual: 42, ...>
39
91
  #
40
- # @return [Requirement::Required] An absolute prohibition level instance.
92
+ # @param matcher [#match?] Any object that implements the matcher protocol
93
+ # @return [Requirement::Required] An absolute prohibition level instance
94
+ # @raise [ArgumentError] If matcher doesn't respond to match?
41
95
  def self.must_not(matcher)
96
+ raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
97
+
42
98
  Requirement::Required.new(negate: true, matcher:)
43
99
  end
44
100
 
45
- # This method mean that there may exist valid reasons in particular
46
- # circumstances to ignore a particular item, but the full implications must be
47
- # understood and carefully weighed before choosing a different course.
48
- #
49
- # @example A recommended definition
101
+ # Defines a recommended requirement that should be satisfied unless there are valid reasons not to.
102
+ # This represents the RFC 2119 "SHOULD" level - where valid reasons may exist to ignore
103
+ # a particular item, but the implications must be understood and weighed.
104
+ #
105
+ # @example With a custom matcher
106
+ # class EvenNumber
107
+ # def match?
108
+ # (yield).even?
109
+ # end
110
+ # end
111
+ #
112
+ # test = Spectus.should EvenNumber.new
113
+ # test.call { 42 }
114
+ # # => #<Expresenter::Pass actual: 42, ...>
115
+ #
116
+ # @example With Matchi gem
50
117
  # require "spectus"
51
118
  # require "matchi/be"
52
119
  #
53
- # Spectus.should Matchi::Be.new(true)
54
- # # => #<SHOULD Matchi::Be(true) negate=false>
55
- #
56
- # @param matcher [#match?] The matcher.
120
+ # test = Spectus.should Matchi::Be.new(:even?)
121
+ # test.call { 42 }
122
+ # # => #<Expresenter::Pass actual: 42, ...>
57
123
  #
58
- # @return [Requirement::Recommended] A recommended requirement level instance.
124
+ # @param matcher [#match?] Any object that implements the matcher protocol
125
+ # @return [Requirement::Recommended] A recommended requirement level instance
126
+ # @raise [ArgumentError] If matcher doesn't respond to match?
59
127
  def self.should(matcher)
128
+ raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
129
+
60
130
  Requirement::Recommended.new(negate: false, matcher:)
61
131
  end
62
132
 
63
- # This method mean that there may exist valid reasons in particular
64
- # circumstances when the particular behavior is acceptable or even useful, but
65
- # the full implications should be understood and the case carefully weighed
66
- # before implementing any behavior described with this label.
67
- #
68
- # @example A not recommended definition
133
+ # Defines a behavior that is not recommended but may be acceptable in specific circumstances.
134
+ # This represents the RFC 2119 "SHOULD NOT" level - where particular behavior may be acceptable
135
+ # but the implications should be understood and the case carefully weighed.
136
+ #
137
+ # @example With a custom matcher
138
+ # class RaisesError
139
+ # def match?
140
+ # yield
141
+ # false
142
+ # rescue
143
+ # true
144
+ # end
145
+ # end
146
+ #
147
+ # test = Spectus.should_not RaisesError.new
148
+ # test.call { 42 }
149
+ # # => #<Expresenter::Pass actual: 42, ...>
150
+ #
151
+ # @example With Matchi gem
69
152
  # require "spectus"
70
153
  # require "matchi/raise_exception"
71
154
  #
72
- # Spectus.should_not Matchi::RaiseException.new(NoMethodError)
73
- # # => #<SHOULD Matchi::RaiseException(NoMethodError) negate=true>
74
- #
75
- # @param matcher [#match?] The matcher.
155
+ # test = Spectus.should_not Matchi::RaiseException.new(StandardError)
156
+ # test.call { 42 }
157
+ # # => #<Expresenter::Pass actual: 42, ...>
76
158
  #
77
- # @return [Requirement::Recommended] A not recommended requirement level
78
- # instance.
159
+ # @param matcher [#match?] Any object that implements the matcher protocol
160
+ # @return [Requirement::Recommended] A not recommended requirement level instance
161
+ # @raise [ArgumentError] If matcher doesn't respond to match?
79
162
  def self.should_not(matcher)
163
+ raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
164
+
80
165
  Requirement::Recommended.new(negate: true, matcher:)
81
166
  end
82
167
 
83
- # This method mean that an item is truly optional.
84
- # One vendor may choose to include the item because a particular marketplace
85
- # requires it or because the vendor feels that it enhances the product while
86
- # another vendor may omit the same item. An implementation which does not
87
- # include a particular option must be prepared to interoperate with another
88
- # implementation which does include the option, though perhaps with reduced
89
- # functionality. In the same vein an implementation which does include a
90
- # particular option must be prepared to interoperate with another
91
- # implementation which does not include the option (except, of course, for the
92
- # feature the option provides).
93
- #
94
- # @example An optional definition
168
+ # Defines an optional feature or behavior.
169
+ # This represents the RFC 2119 "MAY" level - where an item is truly optional.
170
+ # Implementations can freely choose whether to include the item based on their
171
+ # specific needs, while maintaining interoperability with other implementations.
172
+ #
173
+ # For MAY requirements, a test passes in two cases:
174
+ # 1. When a NoMethodError is raised, indicating the feature is not implemented
175
+ # 2. When the feature is implemented and the test succeeds
176
+ #
177
+ # @example With a custom matcher testing an optional method
178
+ # class RespondsTo
179
+ # def initialize(method)
180
+ # @method = method
181
+ # end
182
+ #
183
+ # def match?
184
+ # (yield).respond_to?(@method)
185
+ # end
186
+ # end
187
+ #
188
+ # test = Spectus.may RespondsTo.new(:to_h)
189
+ # test.call { {} } # => pass (feature is implemented)
190
+ # test.call { BasicObject.new } # => pass (NoMethodError - feature not implemented)
191
+ #
192
+ # @example With Matchi gem
95
193
  # require "spectus"
96
- # require "matchi/match"
194
+ # require "matchi/predicate"
97
195
  #
98
- # Spectus.may Matchi::Match.new(/^foo$/)
99
- # # => #<MAY Matchi::Match(/^foo$/) negate=false>
196
+ # test = Spectus.may Matchi::Predicate.new(:be_frozen)
197
+ # test.call { "".freeze } # => pass (feature is implemented)
198
+ # test.call { BasicObject.new } # => pass (NoMethodError - feature not implemented)
100
199
  #
101
- # @param matcher [#match?] The matcher.
102
- #
103
- # @return [Requirement::Optional] An optional requirement level instance.
200
+ # @param matcher [#match?] Any object that implements the matcher protocol
201
+ # @return [Requirement::Optional] An optional requirement level instance
202
+ # @raise [ArgumentError] If matcher doesn't respond to match?
104
203
  def self.may(matcher)
204
+ raise ::ArgumentError, "matcher must respond to match?" unless matcher.respond_to?(:match?)
205
+
105
206
  Requirement::Optional.new(negate: false, matcher:)
106
207
  end
107
208
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spectus
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2024-12-30 00:00:00.000000000 Z
11
+ date: 2025-01-01 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: expresenter
@@ -15,42 +16,28 @@ dependencies:
15
16
  requirements:
16
17
  - - "~>"
17
18
  - !ruby/object:Gem::Version
18
- version: 1.5.0
19
+ version: 1.5.1
19
20
  type: :runtime
20
21
  prerelease: false
21
22
  version_requirements: !ruby/object:Gem::Requirement
22
23
  requirements:
23
24
  - - "~>"
24
25
  - !ruby/object:Gem::Version
25
- version: 1.5.0
26
- - !ruby/object:Gem::Dependency
27
- name: matchi
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '4.0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '4.0'
26
+ version: 1.5.1
40
27
  - !ruby/object:Gem::Dependency
41
28
  name: test_tube
42
29
  requirement: !ruby/object:Gem::Requirement
43
30
  requirements:
44
31
  - - "~>"
45
32
  - !ruby/object:Gem::Version
46
- version: 4.0.0
33
+ version: 4.0.1
47
34
  type: :runtime
48
35
  prerelease: false
49
36
  version_requirements: !ruby/object:Gem::Requirement
50
37
  requirements:
51
38
  - - "~>"
52
39
  - !ruby/object:Gem::Version
53
- version: 4.0.0
40
+ version: 4.0.1
54
41
  description: "Expectation library with RFC 2119's requirement levels \U0001F6A5"
55
42
  email: contact@cyril.email
56
43
  executables: []
@@ -70,6 +57,7 @@ licenses:
70
57
  - MIT
71
58
  metadata:
72
59
  rubygems_mfa_required: 'true'
60
+ post_install_message:
73
61
  rdoc_options: []
74
62
  require_paths:
75
63
  - lib
@@ -77,14 +65,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
65
  requirements:
78
66
  - - ">="
79
67
  - !ruby/object:Gem::Version
80
- version: 3.2.0
68
+ version: 3.1.0
81
69
  required_rubygems_version: !ruby/object:Gem::Requirement
82
70
  requirements:
83
71
  - - ">="
84
72
  - !ruby/object:Gem::Version
85
73
  version: '0'
86
74
  requirements: []
87
- rubygems_version: 3.6.2
75
+ rubygems_version: 3.3.27
76
+ signing_key:
88
77
  specification_version: 4
89
78
  summary: "Expectation library with RFC 2119's requirement levels \U0001F6A5"
90
79
  test_files: []