Skip to content

Instantly share code, notes, and snippets.

@seejohnrun
Last active February 10, 2023 03:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seejohnrun/422b65ba9f70e08ed6a436ddfbeff91b to your computer and use it in GitHub Desktop.
Save seejohnrun/422b65ba9f70e08ed6a436ddfbeff91b to your computer and use it in GitHub Desktop.
A pattern for Variants in Ruby
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