exclusive_case 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3e6f61086cef55f398527daf61d443a704f3ee229292b8c1c018274cafe8500b
4
+ data.tar.gz: b368ec1f93619f448b331d40047e6c0d7793fcf93e8ed130246d08dd81f2ba2d
5
+ SHA512:
6
+ metadata.gz: a9974d64182c95765dfc1d20737a3d336d8e5cfabeb09d05778f48454fe822503396c75422dcc88100ed6327f04ba7fdf47baab9555e426c8a6135cd8f6616cf
7
+ data.tar.gz: a5c174dcd2af2969127485719096832635d68c855b858ca7bcc9f44b16c27811902b7d2eaf37e2be6fb7705e4e9ab2de65ad46dd5d5bbbbaea9b5145c42e2bf6
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+
5
+ plugins:
6
+ - rubocop-rspec
7
+
8
+ Style/StringLiterals:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Style/StringLiteralsInInterpolation:
13
+ Enabled: true
14
+ EnforcedStyle: double_quotes
15
+
16
+ Layout/LineLength:
17
+ Max: 120
18
+
19
+ Style/Documentation:
20
+ Enabled: true
21
+
22
+ Metrics/BlockLength:
23
+ Exclude:
24
+ - "spec/**/*"
25
+
26
+ Style/FrozenStringLiteralComment:
27
+ Enabled: true
28
+
29
+ # Disable RSpec/ExampleLength - allow longer test examples for clarity
30
+ RSpec/ExampleLength:
31
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.5
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [1.0.0] - 2025-08-02
11
+
12
+ ### Added
13
+ - **BREAKING**: Optional `of:` parameter for enhanced validation
14
+ - `InvalidCaseError` - raised when cases are not in the allowed `of:` list
15
+ - `MissingCaseError` - raised when not all `of:` values are handled
16
+ - Comprehensive validation ensuring all cases belong to specified list
17
+ - Requirement that all `of:` values must have corresponding cases
18
+ - Full backward compatibility when `of:` parameter is not used
19
+
20
+ ### Changed
21
+ - **BREAKING**: `exhaustive_case` signature now accepts optional `of:` parameter
22
+
23
+ ### Technical
24
+ - 100% test coverage maintained
25
+ - Full RuboCop compliance with comprehensive documentation
26
+ - GitHub Actions CI/CD pipeline with multi-Ruby version testing
27
+ - Automated dependency management via Dependabot
28
+
29
+ ## [0.1.0] - 2025-08-02
30
+
31
+ ### Added
32
+ - Initial implementation of `exhaustive_case` method
33
+ - Support for single and multiple value matching in `on` clauses
34
+ - Automatic error raising for unhandled cases via `UnhandledCaseError`
35
+ - Core gem structure and configuration
36
+ - RSpec testing framework setup
37
+ - RuboCop linting configuration
38
+ - Development scripts (`bin/setup`, `bin/console`)
39
+
40
+ [Unreleased]: https://github.com/yourusername/exclusive_case/compare/v1.0.0...HEAD
41
+ [1.0.0]: https://github.com/yourusername/exclusive_case/releases/tag/v1.0.0
42
+ [0.1.0]: https://github.com/yourusername/exclusive_case/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ajay Sharma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # Exclusive Case
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/exclusive_case.svg)](https://badge.fury.io/rb/exclusive_case)
4
+ [![CI](https://github.com/ajsharma/exclusive_case/actions/workflows/ci.yml/badge.svg)](https://github.com/ajsharma/exclusive_case/actions/workflows/ci.yml)
5
+
6
+ Exhaustive case statements for Ruby to prevent bugs from unhandled cases when new values are added to a system.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'exclusive_case'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ $ bundle install
20
+ ```
21
+
22
+ Or install it yourself as:
23
+
24
+ ```bash
25
+ $ gem install exclusive_case
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Include the module to get access to the `exhaustive_case` method:
31
+
32
+ ```ruby
33
+ require 'exclusive_case'
34
+ include ExclusiveCase
35
+
36
+ # Now you can use exhaustive_case
37
+ result = exhaustive_case user_status do
38
+ on(:active) { "User is active" }
39
+ on(:inactive) { "User is inactive" }
40
+ on(:pending) { "User is pending approval" }
41
+ end
42
+ ```
43
+
44
+ or to avoid globally adding the module, it can be included in a class:
45
+
46
+ ```ruby
47
+ require 'exclusive_case'
48
+
49
+ class UserRenderer
50
+ include ExclusiveCase
51
+
52
+ def handle_status(user_status)
53
+ # Now you can use exhaustive_case
54
+ result = exhaustive_case user_status do
55
+ on(:active) { "User is active" }
56
+ on(:inactive) { "User is inactive" }
57
+ on(:pending) { "User is pending approval" }
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ ## The problem
64
+
65
+ If/else statements can easily lead to mistake flows when introducing new cases across the systems.
66
+
67
+ For instance, with a constant set of inputs `['A', 'B', 'C']`:
68
+
69
+ ```ruby
70
+ if letter == 'A'
71
+ // handle a
72
+ elsif letter == 'B'
73
+ // handle b
74
+ else // implicit c
75
+ // handle c
76
+ end
77
+ ```
78
+
79
+ However, if a new entry `D` is introduced to the system, now:
80
+
81
+ ```ruby
82
+ if letter == 'A'
83
+ // code to handle a (whoops!)
84
+ elsif letter == 'B'
85
+ // code to handle b (whoops!)
86
+ else // implicit c and d
87
+ // code to handle c (whoops!)
88
+ end
89
+ ```
90
+
91
+ This kind of code does not reveal itself in tests, providing no feedback to the developer that that a new flow is needed.
92
+
93
+ We can address this with a more explicit initial switch:
94
+
95
+ ```ruby
96
+ if letter == 'A'
97
+ // handle a
98
+ elsif letter == 'B'
99
+ // handle b
100
+ elsif letter == 'C'
101
+ // handle c
102
+ else
103
+ raise "Unknown letter #{letter}"
104
+ end
105
+ ```
106
+
107
+ Now, a new letter would trigger the runtime error, alerting the developer to address this gap.
108
+
109
+ Note: for these examples, we're using an if/elsif/else chain, but the same concepts apply for `case/when/else` statements:
110
+
111
+ ```ruby
112
+ case letter
113
+ when 'A'
114
+ // handle a
115
+ when 'B'
116
+ // handle b
117
+ when 'C'
118
+ // handle c
119
+ else
120
+ raise "Unknown letter #{letter}"
121
+ end
122
+ ```
123
+
124
+ This new syntax is better, but leaves some gaps:
125
+
126
+ 1. Engineers taught to use the class `if/else` structure constantly introduce these problems.
127
+ 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
+
129
+ ## The solution
130
+
131
+ 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
+
133
+ Enter exhaustive case:
134
+
135
+ ```ruby
136
+ exhaustive_case letter do
137
+ on('A') { // handle A }
138
+ on('B') { // handle B }
139
+ on('C') { // handle C }
140
+ end
141
+ ```
142
+
143
+ with the new syntax, `exhaustive_case` handles the final else statement and raises and error if there's an unexpected.
144
+
145
+ `exhaustive_case` also tries to provide a syntax simple to the native Ruby `case` switch, with multiple matchers supported.
146
+
147
+
148
+ ```ruby
149
+ exhaustive_case letter do
150
+ on('A') { // handle A }
151
+ on('B', 'C') { // handle B or C }
152
+ end
153
+ ```
154
+
155
+ ## Enhanced validation with `of:` parameter
156
+
157
+ In situations where the central system relies on a series of strategies or an enumerated list, it's often unclear where a growing codebase new logic should be added.
158
+
159
+ By declaring explicitly the known list of forks, the list of forks can be centralized and then the test suite can provide feedback when a new entry to the list is added or removed.
160
+
161
+ For even stronger guarantees, you can specify the complete list of acceptable inputs using the `of:` parameter. This ensures that:
162
+
163
+ 1. All declared cases must belong to the specified list
164
+ 2. All values in the `of:` list must be handled by at least one case
165
+
166
+ ```ruby
167
+ # Define all possible values upfront
168
+ VALID_LETTERS = ['A', 'B', 'C'].freeze
169
+
170
+ exhaustive_case letter, of: VALID_LETTERS do
171
+ on('A') { // handle A }
172
+ on('B') { // handle B }
173
+ on('C') { // handle C }
174
+ end
175
+ ```
176
+
177
+ This will raise an `InvalidCaseError` if:
178
+ - You try to handle a case not in the `of:` list: `on('D') { ... }`
179
+ - You forget to handle all cases: missing `on('C') { ... }`
180
+
181
+ The `of:` parameter is optional - without it, `exhaustive_case` works as before, only validating that the input value has a matching case.
182
+
183
+ ### Examples with `of:`
184
+
185
+ ```ruby
186
+ # Status handling with validation
187
+ STATUSES = [:pending, :success, :failure].freeze
188
+
189
+ result = exhaustive_case status, of: STATUSES do
190
+ on(:pending) { "Processing..." }
191
+ on(:success) { "Completed successfully" }
192
+ on(:failure) { "Failed with error" }
193
+ end
194
+
195
+ # Multiple values per case still work
196
+ USER_TYPES = [:admin, :moderator, :user, :guest].freeze
197
+
198
+ permissions = exhaustive_case user_type, of: USER_TYPES do
199
+ on(:admin) { [:read, :write, :delete, :manage] }
200
+ on(:moderator, :user) { [:read, :write] }
201
+ on(:guest) { [:read] }
202
+ end
203
+ ```
204
+
205
+ if a new status is added to `STATUSES` a test suite should reveal that a case is missing.
206
+
207
+ ## Error Handling
208
+
209
+ The following are the expected errors when using the gem:
210
+
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
214
+
215
+ ## Contributing
216
+
217
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ajsharma/exclusive_case.
218
+
219
+ ## License
220
+
221
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ desc "Run tests and linting"
11
+ task default: %i[spec rubocop]
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/exclusive_case/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "exclusive_case"
7
+ spec.version = ExclusiveCase::VERSION
8
+ spec.authors = ["Ajay Sharma"]
9
+ spec.email = ["aj@ajsharma.com"]
10
+
11
+ spec.summary = "Exhaustive case statements for Ruby to prevent unhandled cases"
12
+ spec.description = "A Ruby gem that provides exhaustive case statement functionality to prevent bugs " \
13
+ "from unhandled cases when new values are added to a system."
14
+ spec.homepage = "https://github.com/ajsharma/exclusive_case"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 3.0.0"
17
+
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/ajsharma/exclusive_case"
21
+ spec.metadata["changelog_uri"] = "https://github.com/ajsharma/exclusive_case/blob/main/CHANGELOG.md"
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExclusiveCase
4
+ # CaseBuilder handles the construction and execution of exhaustive case statements.
5
+ # It validates cases against an optional 'of' list and ensures all required cases are handled.
6
+ #
7
+ # @api private
8
+ class CaseBuilder
9
+ def initialize(value, of)
10
+ @value = value
11
+ @of = of
12
+ @cases = []
13
+ @matched = false
14
+ end
15
+
16
+ def on(*matchers, &block)
17
+ validate_matchers(matchers) if @of
18
+ @cases << { matchers: matchers, block: block }
19
+ end
20
+
21
+ def execute
22
+ validate_completeness if @of
23
+
24
+ @cases.each do |case_def|
25
+ if case_def[:matchers].any? { |matcher| matcher == @value }
26
+ @matched = true
27
+ return case_def[:block].call
28
+ end
29
+ end
30
+
31
+ raise UnhandledCaseError, "No case handled value: #{@value.inspect}"
32
+ end
33
+
34
+ private
35
+
36
+ def validate_matchers(matchers)
37
+ invalid_matchers = matchers.reject { |matcher| @of.include?(matcher) }
38
+ return if invalid_matchers.empty?
39
+
40
+ raise InvalidCaseError, "Invalid case(s): #{invalid_matchers.map(&:inspect).join(", ")}. " \
41
+ "Must be one of: #{@of.map(&:inspect).join(", ")}"
42
+ end
43
+
44
+ def validate_completeness
45
+ declared_cases = @cases.flat_map { |case_def| case_def[:matchers] }.uniq
46
+ missing_cases = @of - declared_cases
47
+ return if missing_cases.empty?
48
+
49
+ raise MissingCaseError, "Missing case(s): #{missing_cases.map(&:inspect).join(", ")}. " \
50
+ "All values from 'of' must be handled: #{@of.map(&:inspect).join(", ")}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ExclusiveCase
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "exclusive_case/version"
4
+ require_relative "exclusive_case/case_builder"
5
+
6
+ # ExclusiveCase provides exhaustive case statement functionality to prevent bugs
7
+ # from unhandled cases when new values are added to a system.
8
+ #
9
+ # @example Basic usage
10
+ # exhaustive_case letter do
11
+ # on('A') { "handle A" }
12
+ # on('B') { "handle B" }
13
+ # on('C') { "handle C" }
14
+ # end
15
+ #
16
+ # @example With validation using 'of' parameter
17
+ # exhaustive_case letter, of: ['A', 'B', 'C'] do
18
+ # on('A') { "handle A" }
19
+ # on('B') { "handle B" }
20
+ # on('C') { "handle C" }
21
+ # end
22
+ module ExclusiveCase
23
+ # Generic Error class for the gem
24
+ class Error < StandardError; end
25
+
26
+ # Indicates that an unknown input was passed into the system.
27
+ #
28
+ # Correct this by changing the input or know allowed values
29
+ class UnhandledCaseError < Error; end
30
+
31
+ # Indicates that a case was declared that is not in the allowed 'of' list
32
+ class InvalidCaseError < Error; end
33
+
34
+ # Indicates that one or more cases from the initial value list was not
35
+ # implemented
36
+ class MissingCaseError < Error; end
37
+
38
+ def exhaustive_case(value, of: nil, &block)
39
+ builder = CaseBuilder.new(value, of)
40
+ builder.instance_eval(&block)
41
+ builder.execute
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: exclusive_case
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ajay Sharma
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: A Ruby gem that provides exhaustive case statement functionality to prevent
13
+ bugs from unhandled cases when new values are added to a system.
14
+ email:
15
+ - aj@ajsharma.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - ".rubocop.yml"
22
+ - ".ruby-version"
23
+ - CHANGELOG.md
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - exclusive_case.gemspec
28
+ - lib/exclusive_case.rb
29
+ - lib/exclusive_case/case_builder.rb
30
+ - lib/exclusive_case/version.rb
31
+ homepage: https://github.com/ajsharma/exclusive_case
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ allowed_push_host: https://rubygems.org
36
+ homepage_uri: https://github.com/ajsharma/exclusive_case
37
+ source_code_uri: https://github.com/ajsharma/exclusive_case
38
+ changelog_uri: https://github.com/ajsharma/exclusive_case/blob/main/CHANGELOG.md
39
+ rubygems_mfa_required: 'true'
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.6.9
55
+ specification_version: 4
56
+ summary: Exhaustive case statements for Ruby to prevent unhandled cases
57
+ test_files: []