Last active
February 10, 2023 03:23
-
-
Save seejohnrun/422b65ba9f70e08ed6a436ddfbeff91b to your computer and use it in GitHub Desktop.
A pattern for Variants in Ruby
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
require 'set' | |
require 'rspec' | |
### An example of how to implement a variant pattern in Ruby | |
class Variant | |
InvalidLabel = Class.new(StandardError) | |
UnhandledMatchCase = Class.new(StandardError) | |
UnnecessaryDefaultCase = Class.new(StandardError) | |
ExtraMatchCase = Class.new(StandardError) | |
DEFAULT_KEY = :default | |
class P | |
class << self | |
attr_accessor :valid_labels | |
end | |
def initialize(label, *args, **kwargs) | |
raise InvalidLabel unless self.class.valid_labels.include?(label) | |
@label = label | |
@args = args | |
@kwargs = kwargs | |
end | |
def match(handlers) | |
missing_cases = self.class.valid_labels - handlers.keys | |
handles_all_cases = missing_cases.none? | |
extra_cases = handlers.keys.to_set - self.class.valid_labels - [DEFAULT_KEY] | |
raise ExtraMatchCase, extra_cases.join(', ') if extra_cases.any? | |
raise UnhandledMatchCase, missing_cases.join(', ') if !handles_all_cases && !handlers.key?(DEFAULT_KEY) | |
raise UnnecessaryDefaultCase if handles_all_cases && handlers.key?(DEFAULT_KEY) | |
handlers.fetch(@label, handlers[DEFAULT_KEY]).call(*@args, **@kwargs) | |
end | |
end | |
def self.[](*labels) | |
Class.new(P).tap { |k| k.valid_labels = labels.to_set } | |
end | |
end | |
### tests because why not | |
RSpec.describe Variant do | |
let(:variant_type) { Variant[:one, :two] } | |
it 'calls the matched case' do | |
variant = variant_type.new :one | |
result = variant.match( | |
one: -> { 1 }, | |
two: -> { 2 } | |
) | |
expect(result).to eq(1) | |
end | |
it 'calls with provided args & kwargs' do | |
variant = variant_type.new :one, :v1, :v2, k1: :v1, k2: :v2, named_kw: :v | |
result = variant.match( | |
one: ->(*args, named_kw:, **kwargs) { | |
expect(args).to eq([:v1, :v2]) | |
expect(kwargs).to eq(k1: :v1, k2: :v2) | |
expect(named_kw).to eq(:v) | |
}, | |
default: -> { raise } | |
) | |
end | |
it 'calls the default case if no match' do | |
variant = variant_type.new :two | |
result = variant.match( | |
one: -> { 1 }, | |
default: -> { 2 } | |
) | |
expect(result).to eq(2) | |
end | |
it 'raises if trying to create a variant with a bad label' do | |
expect { variant_type.new :three }.to raise_error(Variant::InvalidLabel) | |
end | |
it 'raises if trying to match without all cases' do | |
variant = variant_type.new :one | |
expect { variant.match(one: -> { }) }. | |
to raise_error(Variant::UnhandledMatchCase, 'two') | |
end | |
it 'raises if there is a default match but all cases are matched already' do | |
variant = variant_type.new :one | |
expect { variant.match(one: -> { }, two: -> { }, default: -> { }) }. | |
to raise_error(Variant::UnnecessaryDefaultCase) | |
end | |
it 'raises if there is an extra match not relevant to the variant type' do | |
variant = variant_type.new :one | |
expect { variant.match(one: -> { }, two: -> { }, other: -> { }) }. | |
to raise_error(Variant::ExtraMatchCase, 'other') | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment