exhaustive_case 1.0.0 → 1.0.1

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: e1b92a48ea165ce23641008c4d72179fd6da4c7c31d02c7ba36424ec9a943d22
4
- data.tar.gz: e16ff9171f2572d832b6868639b4b9d9b2b17c6315b117ad9ca2132cdfeed7f6
3
+ metadata.gz: 432d6c2b8f4efcd0eacd5b70b49851a73c6e9f5ba46ef7b5ae8c1f56ac542d4d
4
+ data.tar.gz: 6ecfe98e2467fa36012721d1d637a19e454c97e929eff39056502125343a8597
5
5
  SHA512:
6
- metadata.gz: a9a8894a657c585bca18b9682e76dc68be60ba194fdbc260ff8aca9b9e7a1de2f00f5ba88782a9aac18ada65b589fb0324d676b85eefa72286879e15b6c4ded0
7
- data.tar.gz: b4b38e8823633e5fb6f143d2b975ccbc927988cceee3df92d82b23fa1a37c6e7d8d45665bd48bcfa79cc9f4fd3590462c3df53c938d82fc804b560202a66dcd4
6
+ metadata.gz: 2338d003b512ecf30157dc6f5a5c773e6f5a94ef12100a61af12ae47405465fbdd9c3306709a68307d08d31b3a54e3a3c9ac037304d71348385a09b804003924
7
+ data.tar.gz: f18a5b6ddf5b400c2d5bdb6e0554abe7d1c17e4b681c445299f09eee1f49cc52631488f5e7f99a152e7512a628908711c00523d8b5af3be22c5d413756348324
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.1] - 2025-08-03
11
+
12
+ ### Added
13
+ - `DuplicateCaseError` - raised when the same case value is declared more than once
14
+ - Duplicate case detection prevents declaring the same value across multiple `on` clauses
15
+ - Comprehensive test coverage for duplicate case scenarios
16
+ - Documentation for `DuplicateCaseError` in README with examples
17
+
18
+ ### Technical
19
+ - Enhanced case validation with duplicate detection logic
20
+ - Maintained 100% test coverage with new duplicate case test suite
21
+
10
22
  ## [1.0.0] - 2025-08-02
11
23
 
12
24
  ### Added
@@ -37,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
49
  - RuboCop linting configuration
38
50
  - Development scripts (`bin/setup`, `bin/console`)
39
51
 
40
- [Unreleased]: https://github.com/yourusername/exhaustive_case/compare/v1.0.0...HEAD
52
+ [Unreleased]: https://github.com/yourusername/exhaustive_case/compare/v1.0.1...HEAD
53
+ [1.0.1]: https://github.com/yourusername/exhaustive_case/releases/tag/v1.0.1
41
54
  [1.0.0]: https://github.com/yourusername/exhaustive_case/releases/tag/v1.0.0
42
55
  [0.1.0]: https://github.com/yourusername/exhaustive_case/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,9 +1,36 @@
1
1
  # Exclusive Case
2
2
 
3
+ Exhaustive case statements for Ruby to prevent bugs from unhandled cases when enumerated values are added to or removed a system.
4
+
3
5
  [![Gem Version](https://badge.fury.io/rb/exhaustive_case.svg)](https://badge.fury.io/rb/exhaustive_case)
4
6
  [![CI](https://github.com/ajsharma/exhaustive_case/actions/workflows/ci.yml/badge.svg)](https://github.com/ajsharma/exhaustive_case/actions/workflows/ci.yml)
5
7
 
6
- Exhaustive case statements for Ruby to prevent bugs from unhandled cases when new values are added to a system.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Quick Start](#quick-start)
12
+ - [Installation](#installation)
13
+ - [Usage](#usage)
14
+ - [The Problem](#the-problem)
15
+ - [The Solution](#the-solution)
16
+ - [Enhanced Validation with `of:` Parameter](#enhanced-validation-with-of-parameter)
17
+ - [Error Handling](#error-handling)
18
+ - [Contributing](#contributing)
19
+ - [License](#license)
20
+
21
+ ## Quick Start
22
+
23
+ ```ruby
24
+ require 'exhaustive_case'
25
+ include ExhaustiveCase
26
+
27
+ # Simple exhaustive case - will raise error if user_status doesn't match any case
28
+ result = exhaustive_case user_status do
29
+ on(:active) { "User is active" }
30
+ on(:inactive) { "User is inactive" }
31
+ on(:pending) { "User is pending approval" }
32
+ end
33
+ ```
7
34
 
8
35
  ## Installation
9
36
 
@@ -25,6 +52,10 @@ Or install it yourself as:
25
52
  $ gem install exhaustive_case
26
53
  ```
27
54
 
55
+ ## Requirements
56
+
57
+ - Ruby 2.7 or higher
58
+
28
59
  ## Usage
29
60
 
30
61
  Include the module to get access to the `exhaustive_case` method:
@@ -60,7 +91,7 @@ class UserRenderer
60
91
  end
61
92
  ```
62
93
 
63
- ## The problem
94
+ ## The Problem
64
95
 
65
96
  If/else statements can easily lead to mistake flows when introducing new cases across the systems.
66
97
 
@@ -68,11 +99,11 @@ For instance, with a constant set of inputs `['A', 'B', 'C']`:
68
99
 
69
100
  ```ruby
70
101
  if letter == 'A'
71
- // handle a
102
+ # handle a
72
103
  elsif letter == 'B'
73
- // handle b
74
- else // implicit c
75
- // handle c
104
+ # handle b
105
+ else # implicit c
106
+ # handle c
76
107
  end
77
108
  ```
78
109
 
@@ -80,11 +111,11 @@ However, if a new entry `D` is introduced to the system, now:
80
111
 
81
112
  ```ruby
82
113
  if letter == 'A'
83
- // code to handle a (whoops!)
114
+ # code to handle a (whoops!)
84
115
  elsif letter == 'B'
85
- // code to handle b (whoops!)
86
- else // implicit c and d
87
- // code to handle c (whoops!)
116
+ # code to handle b (whoops!)
117
+ else # implicit c and d
118
+ # code to handle c (whoops!)
88
119
  end
89
120
  ```
90
121
 
@@ -94,11 +125,11 @@ We can address this with a more explicit initial switch:
94
125
 
95
126
  ```ruby
96
127
  if letter == 'A'
97
- // handle a
128
+ # handle a
98
129
  elsif letter == 'B'
99
- // handle b
130
+ # handle b
100
131
  elsif letter == 'C'
101
- // handle c
132
+ # handle c
102
133
  else
103
134
  raise "Unknown letter #{letter}"
104
135
  end
@@ -111,11 +142,11 @@ Note: for these examples, we're using an if/elsif/else chain, but the same conce
111
142
  ```ruby
112
143
  case letter
113
144
  when 'A'
114
- // handle a
145
+ # handle a
115
146
  when 'B'
116
- // handle b
147
+ # handle b
117
148
  when 'C'
118
- // handle c
149
+ # handle c
119
150
  else
120
151
  raise "Unknown letter #{letter}"
121
152
  end
@@ -126,7 +157,7 @@ This new syntax is better, but leaves some gaps:
126
157
  1. Engineers taught to use the class `if/else` structure constantly introduce these problems.
127
158
  2. The final raise statement is often "impossible" to access via testing, it's nature preventing access without mocking (especially when these types of clauses are part of private functions).
128
159
 
129
- ## The solution
160
+ ## The Solution
130
161
 
131
162
  But what if had a new type of case statement that could both ensure that all cases are correctly implemented and provide feedback if a new case has been introduced but not implemented?
132
163
 
@@ -134,9 +165,9 @@ Enter exhaustive case:
134
165
 
135
166
  ```ruby
136
167
  exhaustive_case letter do
137
- on('A') { // handle A }
138
- on('B') { // handle B }
139
- on('C') { // handle C }
168
+ on('A') { # handle A }
169
+ on('B') { # handle B }
170
+ on('C') { # handle C }
140
171
  end
141
172
  ```
142
173
 
@@ -147,8 +178,8 @@ with the new syntax, `exhaustive_case` handles the final else statement and rais
147
178
 
148
179
  ```ruby
149
180
  exhaustive_case letter do
150
- on('A') { // handle A }
151
- on('B', 'C') { // handle B or C }
181
+ on('A') { # handle A }
182
+ on('B', 'C') { # handle B or C }
152
183
  end
153
184
  ```
154
185
 
@@ -168,9 +199,9 @@ For even stronger guarantees, you can specify the complete list of acceptable in
168
199
  VALID_LETTERS = ['A', 'B', 'C'].freeze
169
200
 
170
201
  exhaustive_case letter, of: VALID_LETTERS do
171
- on('A') { // handle A }
172
- on('B') { // handle B }
173
- on('C') { // handle C }
202
+ on('A') { # handle A }
203
+ on('B') { # handle B }
204
+ on('C') { # handle C }
174
205
  end
175
206
  ```
176
207
 
@@ -208,9 +239,52 @@ if a new status is added to `STATUSES` a test suite should reveal that a case is
208
239
 
209
240
  The following are the expected errors when using the gem:
210
241
 
211
- - **`UnhandledCaseError`** - Raised when the input value doesn't match any `on` clause
212
- - **`InvalidCaseError`** - Raised when using `of:` and an `on` clause contains a value not in the allowed list
213
- - **`MissingCaseError`** - Raised when using `of:` and not all allowed values have corresponding `on` clauses
242
+ ### `UnhandledCaseError`
243
+ Raised when the input value doesn't match any `on` clause:
244
+ ```ruby
245
+ exhaustive_case :unknown_status do
246
+ on(:active) { "active" }
247
+ on(:inactive) { "inactive" }
248
+ end
249
+ # => ExhaustiveCase::UnhandledCaseError: No case matched for value: :unknown_status
250
+ ```
251
+
252
+ ### `InvalidCaseError`
253
+ Raised when using `of:` and an `on` clause contains a value not in the allowed list:
254
+ ```ruby
255
+ exhaustive_case status, of: [:active, :inactive] do
256
+ on(:active) { "active" }
257
+ on(:pending) { "pending" } # :pending not in allowed list
258
+ end
259
+ # => ExhaustiveCase::InvalidCaseError: Case :pending is not in the allowed list: [:active, :inactive]
260
+ ```
261
+
262
+ ### `MissingCaseError`
263
+ Raised when using `of:` and not all allowed values have corresponding `on` clauses:
264
+ ```ruby
265
+ exhaustive_case status, of: [:active, :inactive, :pending] do
266
+ on(:active) { "active" }
267
+ # Missing :inactive and :pending cases
268
+ end
269
+ # => ExhaustiveCase::MissingCaseError: Missing cases for: [:inactive, :pending]
270
+ ```
271
+
272
+ ### `DuplicateCaseError`
273
+ Raised when the same case value is declared more than once:
274
+ ```ruby
275
+ exhaustive_case status do
276
+ on(:active) { "first active" }
277
+ on(:active) { "second active" } # Duplicate case
278
+ end
279
+ # => ExhaustiveCase::DuplicateCaseError: Duplicate case(s): :active. Each case value can only be declared once.
280
+
281
+ # Also applies when values appear across multiple on clauses
282
+ exhaustive_case status do
283
+ on(:active, :pending) { "first group" }
284
+ on(:pending, :inactive) { "second group" } # :pending is duplicated
285
+ end
286
+ # => ExhaustiveCase::DuplicateCaseError: Duplicate case(s): :pending. Each case value can only be declared once.
287
+ ```
214
288
 
215
289
  ## Contributing
216
290
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module ExhaustiveCase
4
6
  # CaseBuilder handles the construction and execution of exhaustive case statements.
5
7
  # It validates cases against an optional 'of' list and ensures all required cases are handled.
@@ -11,15 +13,20 @@ module ExhaustiveCase
11
13
  @of = of
12
14
  @cases = []
13
15
  @matched = false
16
+ @declared_matchers = Set.new
14
17
  end
15
18
 
16
19
  def on(*matchers, &block)
17
- validate_matchers(matchers) if @of
20
+ ensure_only_of_matchers(matchers) if @of
21
+ ensure_unique_matchers(matchers)
18
22
  @cases << { matchers: matchers, block: block }
19
23
  end
20
24
 
21
25
  def execute
22
- validate_completeness if @of
26
+ # Important! Make sure this is run before executing a matcher, we need to validate
27
+ # that the case structure is complete so we don't execute branches for an invalid
28
+ # exhaustive case
29
+ ensure_completeness if @of
23
30
 
24
31
  @cases.each do |case_def|
25
32
  if case_def[:matchers].any? { |matcher| matcher == @value }
@@ -33,7 +40,7 @@ module ExhaustiveCase
33
40
 
34
41
  private
35
42
 
36
- def validate_matchers(matchers)
43
+ def ensure_only_of_matchers(matchers)
37
44
  invalid_matchers = matchers.reject { |matcher| @of.include?(matcher) }
38
45
  return if invalid_matchers.empty?
39
46
 
@@ -41,7 +48,18 @@ module ExhaustiveCase
41
48
  "Must be one of: #{@of.map(&:inspect).join(", ")}"
42
49
  end
43
50
 
44
- def validate_completeness
51
+ def ensure_unique_matchers(matchers)
52
+ duplicate_matchers = matchers.select { |matcher| @declared_matchers.include?(matcher) }
53
+
54
+ unless duplicate_matchers.empty?
55
+ raise DuplicateCaseError, "Duplicate case(s): #{duplicate_matchers.map(&:inspect).join(", ")}. " \
56
+ "Each case value can only be declared once."
57
+ end
58
+
59
+ @declared_matchers.merge(matchers)
60
+ end
61
+
62
+ def ensure_completeness
45
63
  declared_cases = @cases.flat_map { |case_def| case_def[:matchers] }.uniq
46
64
  missing_cases = @of - declared_cases
47
65
  return if missing_cases.empty?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExhaustiveCase
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
@@ -35,6 +35,9 @@ module ExhaustiveCase
35
35
  # implemented
36
36
  class MissingCaseError < Error; end
37
37
 
38
+ # Indicates that a case value was declared more than once
39
+ class DuplicateCaseError < Error; end
40
+
38
41
  def exhaustive_case(value, of: nil, &block)
39
42
  builder = CaseBuilder.new(value, of)
40
43
  builder.instance_eval(&block)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exhaustive_case
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ajay Sharma