spectus 5.0.1 → 5.0.2

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 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: []