-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Can be used by including `Ruby::Enum::Ecase` in an enum class. It will add a method called `ecase` that can be used to simulate a case statement that will raise an error if a case/enum value is not handled.
- Loading branch information
1 parent
b5740c3
commit 0d91bc2
Showing
4 changed files
with
197 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# frozen_string_literal: true | ||
|
||
module Ruby | ||
module Enum | ||
## | ||
# Adds a method to an enum class that allows for exhaustive matching on a value. | ||
# | ||
# @example | ||
# class Color | ||
# include Ruby::Enum | ||
# include Ruby::Enum::Ecase | ||
# | ||
# define :RED, :red | ||
# define :GREEN, :green | ||
# define :BLUE, :blue | ||
# end | ||
# | ||
# Color.ecase(Color::RED, { | ||
# [Color::RED, Color::GREEN] => -> { puts "red or green" }, | ||
# Color::BLUE => -> { puts "blue" }, | ||
# }) | ||
# | ||
# Reserves the :else key for a default case: | ||
# Color.ecase(Color::RED, { | ||
# [Color::RED, Color::GREEN] => -> { puts "red or green" }, | ||
# else: -> { puts "blue" }, | ||
# }) | ||
module Ecase | ||
def self.included(klass) | ||
klass.extend(ClassMethods) | ||
end | ||
|
||
## | ||
# @see Ruby::Enum::Ecase | ||
module ClassMethods | ||
class ValuesNotDefinedError < StandardError | ||
end | ||
|
||
class NotAllCasesHandledError < StandardError | ||
end | ||
|
||
def ecase(value, cases) | ||
validate_cases(cases) | ||
|
||
filtered_cases = cases.select do |values, _block| | ||
values = [values] unless values.is_a?(Array) | ||
values.include?(value) | ||
end | ||
|
||
return cases[:else]&.call if filtered_cases.none? | ||
|
||
results = filtered_cases.map { |_values, block| block.call } | ||
|
||
# Return the first result if there is only one result | ||
results.size == 1 ? results.first : results | ||
end | ||
|
||
private | ||
|
||
def validate_cases(cases) | ||
all_values = cases.keys.flatten - [:else] | ||
else_defined = cases.key?(:else) | ||
superfluous_values = all_values - values | ||
missing_values = values - all_values | ||
|
||
raise ValuesNotDefinedError, "Value(s) not defined: #{superfluous_values.join(', ')}" if superfluous_values.any? | ||
raise NotAllCasesHandledError, "Not all cases handled: #{missing_values.join(', ')}" if missing_values.any? && !else_defined | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'spec_helper' | ||
|
||
RSpec.describe Ruby::Enum::Ecase do | ||
test_enum = | ||
Class.new do | ||
include Ruby::Enum | ||
include Ruby::Enum::Ecase | ||
|
||
define :RED, :red | ||
define :GREEN, :green | ||
define :BLUE, :blue | ||
end | ||
|
||
describe '.ecase' do | ||
context 'when all cases are defined' do | ||
subject do | ||
test_enum.ecase( | ||
test_enum::RED, | ||
{ | ||
[test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, | ||
test_enum::BLUE => -> { 'blue' } | ||
} | ||
) | ||
end | ||
|
||
it { is_expected.to eq('red or green') } | ||
end | ||
|
||
context 'when there are mutliple matches' do | ||
subject do | ||
test_enum.ecase( | ||
test_enum::RED, | ||
{ | ||
[test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, | ||
test_enum::RED => -> { 'red' }, | ||
test_enum::BLUE => -> { 'blue' } | ||
} | ||
) | ||
end | ||
|
||
it { is_expected.to eq(['red or green', 'red']) } | ||
end | ||
|
||
context 'when not all cases are defined' do | ||
it 'raises an error' do | ||
expect do | ||
test_enum.ecase( | ||
test_enum::RED, | ||
{ [test_enum::RED, test_enum::GREEN] => -> { 'red or green' } } | ||
) | ||
end.to raise_error(Ruby::Enum::Ecase::ClassMethods::NotAllCasesHandledError) | ||
end | ||
end | ||
|
||
context 'when not all cases are defined but :else is specified (default case)' do | ||
it 'does not raise an error' do | ||
expect do | ||
result = test_enum.ecase( | ||
test_enum::BLUE, | ||
{ | ||
[test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, | ||
else: -> { 'blue' } | ||
} | ||
) | ||
|
||
expect(result).to eq('blue') | ||
end.not_to raise_error | ||
end | ||
end | ||
|
||
context 'when a superfluous case is defined' do | ||
it 'raises an error' do | ||
expect do | ||
test_enum.ecase( | ||
test_enum::RED, | ||
{ | ||
[test_enum::RED, test_enum::GREEN] => -> { 'red or green' }, | ||
test_enum::BLUE => -> { 'blue' }, | ||
:something => -> { 'green' } | ||
} | ||
) | ||
end.to raise_error(Ruby::Enum::Ecase::ClassMethods::ValuesNotDefinedError) | ||
end | ||
end | ||
end | ||
end |