spectus 5.0.0 → 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: 3f0cce51d702c1c3d143fdad6af9384ebe7cbca8718a671d928ca8b5a111fe04
4
- data.tar.gz: 1648b947935ac87ef6c5b02c16a066b92bf44bbcf5d8bfae0d87c07d3697cf91
3
+ metadata.gz: 27755e146e515eb8767ab7d71d9e5f712538376e89df1100f6629ba27f5635c2
4
+ data.tar.gz: 5e9c7e45e5f91f5a98aa62ea005732fec936c2dd456a8c52d9d50e55b81510ec
5
5
  SHA512:
6
- metadata.gz: 165ed9b4d2528c514dcf9856124a9da2d43c414757e2bd31716b9098f9fa55c5b4f9db4d65f35a0b94696c9bde2143c40a7285ddc428171496b1f56d285ac87f
7
- data.tar.gz: 8c4a74459502da707d751ac829efccd41bdb9f49667eb7b94d460f93fbf427b25e7c888d16c207889366bb56e51f58ccad6e3716c2709bb28d2af4d5dd985bee
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,138 +2,144 @@
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. 🚥
10
8
 
11
- ![A traffic light with three distinct sections](https://github.com/fixrb/spectus/raw/main/img/spectus.png)
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
+ ```
12
23
 
13
24
  ## Installation
14
25
 
15
- Add this line to your application's Gemfile:
26
+ Add to your Gemfile:
16
27
 
17
28
  ```ruby
18
29
  gem "spectus"
30
+ gem "matchi" # For matchers
19
31
  ```
20
32
 
21
- And then execute:
33
+ Or install directly:
22
34
 
23
- ```sh
24
- bundle install
35
+ ```bash
36
+ gem install spectus
37
+ gem install matchi
25
38
  ```
26
39
 
27
- Or install it yourself as:
40
+ ## Understanding RFC 2119
28
41
 
29
- ```sh
30
- gem install spectus
31
- ```
42
+ Spectus implements [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) requirement levels to bring clarity and precision to test expectations:
32
43
 
33
- ## Usage
44
+ - **MUST** (✅): Absolute requirement, no exceptions
45
+ - **SHOULD** (⚠️): Strong recommendation with valid exceptions
46
+ - **MAY** (💡): Optional feature
34
47
 
35
- 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.
36
49
 
37
- To make __Spectus__ available:
50
+ ## Features
38
51
 
39
- ```ruby
40
- require "spectus"
41
- ```
52
+ ### Requirement Levels
42
53
 
43
- 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 |
44
59
 
45
- ```sh
46
- gem install matchi
47
- ```
60
+ ### Results Classification
48
61
 
49
- ```ruby
50
- require "matchi"
51
- ```
62
+ - **Pass Results:**
63
+ - ✅ Success (MUST level met)
64
+ - ⚠️ Warning (SHOULD level met)
65
+ - 💡 Info (MAY level met)
52
66
 
53
- All examples here assume that this has been done.
67
+ - **Fail Results:**
68
+ - ❌ Failure (requirement not met)
69
+ - 💥 Error (unexpected exception)
54
70
 
55
- ### Absolute Requirement
71
+ ## Usage Examples
56
72
 
57
- There is exactly one bat:
73
+ ### Testing Required Behavior
58
74
 
59
75
  ```ruby
60
- definition = Spectus.must Matchi::Be.new(1)
61
- definition.call { "🦇".size }
62
- # => 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
63
78
  ```
64
79
 
65
- The test is passed.
66
-
67
- ### Absolute Prohibition
68
-
69
- Truth and lies:
80
+ ### Testing Recommended Behavior
70
81
 
71
82
  ```ruby
72
- definition = Spectus.must_not Matchi::Be.new(true)
73
- definition.call { false }
74
- # => 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
75
85
  ```
76
86
 
77
- ### Recommended
78
-
79
- A well-known joke. The addition of `0.1` and `0.2` is deadly precise:
87
+ ### Testing Optional Features
80
88
 
81
89
  ```ruby
82
- definition = Spectus.should Matchi::Be.new(0.3)
83
- definition.call { 0.1 + 0.2 }
84
- # => 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
85
92
  ```
86
93
 
87
- ### Not Recommended
94
+ ## Advanced Usage
88
95
 
89
- This should not be wrong:
96
+ <details>
97
+ <summary>Click to expand custom matcher example</summary>
90
98
 
91
99
  ```ruby
92
- definition = Spectus.should_not Matchi::Match.new("123456")
93
-
94
- definition.call do
95
- require "securerandom"
96
-
97
- SecureRandom.hex(3)
100
+ class PositiveNumber
101
+ def match?
102
+ yield.positive?
103
+ end
98
104
  end
99
- # => Expresenter::Pass(actual: "bb5716", definition: "match \"123456\"", error: nil, expected: "123456", got: true, negate: true, level: :SHOULD)
100
- ```
101
105
 
102
- In any case, as long as there are no exceptions, the test passes.
103
-
104
- ### Optional
106
+ test = Spectus.must PositiveNumber.new
107
+ test.call { 42 } # => Pass
108
+ ```
109
+ </details>
105
110
 
106
- An empty array is blank, right?
111
+ <details>
112
+ <summary>Click to expand integration example</summary>
107
113
 
108
114
  ```ruby
109
- definition = Spectus.may Matchi::Be.new(true)
110
- definition.call { [].blank? }
111
- # => Expresenter::Pass(actual: nil, definition: "be true", error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: true, got: nil, negate: false, level: :MAY)
112
- ```
113
-
114
- My bad! ActiveSupport was not imported. 🤦‍♂️
115
+ require "spectus"
116
+ require "matchi"
115
117
 
116
- 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>
117
126
 
118
- ## Contact
127
+ ## Related Projects
119
128
 
120
- * Home page: https://github.com/fixrb/spectus
121
- * Bugs/issues: https://github.com/fixrb/spectus/issues
122
- * 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
123
132
 
124
- ## Versioning
133
+ ## License
125
134
 
126
- __Spectus__ follows [Semantic Versioning 2.0](https://semver.org/).
135
+ Released under the [MIT License](LICENSE.md).
127
136
 
128
- ## License
137
+ ## Support
129
138
 
130
- 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)
131
142
 
132
- ---
143
+ ## Sponsors
133
144
 
134
- <p>
135
- This project is sponsored by:<br />
136
- <a href="https://sashite.com/"><img
137
- src="https://github.com/fixrb/spectus/raw/main/img/sashite.png"
138
- alt="Sashité" /></a>
139
- </p>
145
+ This project is sponsored by [Sashité](https://sashite.com/)
@@ -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 [#matches?] 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(&)
@@ -32,43 +51,22 @@ module Spectus
32
51
  actual: test.actual,
33
52
  definition: @matcher.to_s,
34
53
  error: test.error,
35
- expected: @matcher.expected,
36
54
  got: test.got,
37
55
  level: self.class.level,
38
56
  negate: @negate
39
57
  )
40
58
  end
41
59
 
42
- # :nocov:
43
-
44
- # A string containing a human-readable representation of the definition.
45
- #
46
- # @example The human-readable representation of an absolute requirement.
47
- # require "spectus"
48
- # require "matchi/be"
49
- #
50
- # definition = Spectus.must Matchi::Be.new(1)
51
- # definition.inspect
52
- # # => "#<MUST Matchi::Be(1) negate=false>"
53
- #
54
- # @return [String] The human-readable representation of the definition.
55
- #
56
- # @api public
57
- def inspect
58
- "#<#{self.class.level} #{@matcher.inspect} negate=#{@negate}>"
59
- end
60
-
61
- # :nocov:
62
-
63
60
  private
64
61
 
65
- # Code experiment result.
66
- #
67
- # @param test [::TestTube::Base] The state of the experiment.
62
+ # Determine if the test passed according to the requirement level's rules.
68
63
  #
69
- # @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.
70
67
  #
71
- # @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
72
70
  def passed?(test)
73
71
  test.got.equal?(true)
74
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 [#matches?] 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 [#matches?] 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 [#matches?] 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 [#matches?] 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 [#matches?] 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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spectus
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-25 00:00:00.000000000 Z
11
+ date: 2025-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expresenter
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.4.1
19
+ version: 1.5.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.4.1
26
+ version: 1.5.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: test_tube
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 3.0.0
33
+ version: 4.0.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 3.0.0
40
+ version: 4.0.1
41
41
  description: "Expectation library with RFC 2119's requirement levels \U0001F6A5"
42
42
  email: contact@cyril.email
43
43
  executables: []
@@ -57,7 +57,7 @@ licenses:
57
57
  - MIT
58
58
  metadata:
59
59
  rubygems_mfa_required: 'true'
60
- post_install_message:
60
+ post_install_message:
61
61
  rdoc_options: []
62
62
  require_paths:
63
63
  - lib
@@ -65,15 +65,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 3.2.0
68
+ version: 3.1.0
69
69
  required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  requirements:
71
71
  - - ">="
72
72
  - !ruby/object:Gem::Version
73
73
  version: '0'
74
74
  requirements: []
75
- rubygems_version: 3.4.19
76
- signing_key:
75
+ rubygems_version: 3.3.27
76
+ signing_key:
77
77
  specification_version: 4
78
78
  summary: "Expectation library with RFC 2119's requirement levels \U0001F6A5"
79
79
  test_files: []