Skip to content

Commit

Permalink
Add error messages and optimistic / pessimistic failures
Browse files Browse the repository at this point in the history
  • Loading branch information
baweaver committed Feb 18, 2019
1 parent 60362f1 commit e8d7d01
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 8 deletions.
16 changes: 15 additions & 1 deletion lib/qo/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,25 @@ module Exceptions
# @author baweaver
# @since 0.99.1
class ExhaustiveMatchNotMet < StandardError
MESSAGE = 'Exhaustive match required - pattern does not satisfy all possible conditions'
MESSAGE = 'Exhaustive match required: pattern does not satisfy all possible conditions'

def initialize
super(MESSAGE)
end
end

# Not all branches were definied in an exhaustive matcher
#
# @author baweaver
# @since 0.99.1
class ExhaustiveMatchMissingBranches < StandardError
def initialize(expected_branches:, given_branches:)
super <<~MESSAGE
Exhaustive match required: pattern does not specify all branches.
Expected Branches: #{expected_branches.join(', ')}
Given Branches: #{given_branches.join(', ')}
MESSAGE
end
end
end
end
11 changes: 11 additions & 0 deletions lib/qo/pattern_matchers/branching.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def self.included(base)
# @author baweaver
# @since 1.0.0
module ClassMethods
attr_reader :available_branches

# Registers a branch to a pattern matcher.
#
# This defines a method on the pattern matcher matching the `name` of
Expand All @@ -29,10 +31,19 @@ module ClassMethods
# When called, this will either ammend a matcher to the list of matchers
# or set a default matcher if the branch happens to be a default.
#
# It also adds the branch to a registry of branches for later use in
# error handling or other such potential requirements.
#
# @param branch [Branch]
# Branch object to register with a pattern matcher
def register_branch(branch)
@available_branches ||= {}
@available_branches[branch.name] = branch

define_method(branch.name) do |*conditions, **keyword_conditions, &function|
@provided_matchers ||= []
@provided_matchers.push(branch.name)

qo_matcher = Qo::Matchers::Matcher.new('and', conditions, keyword_conditions)

branch_matcher = branch.create_matcher(
Expand Down
66 changes: 60 additions & 6 deletions lib/qo/pattern_matchers/pattern_match.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ module PatternMatchers
class PatternMatch
include Branching

# All matchers that have currently been added to an instance
# of a pattern match
attr_reader :provided_matchers

# The regular pattern matcher from classic Qo uses `when` and `else`
# branches, like a `case` statement
register_branch Qo::Branches::WhenBranch.new
Expand All @@ -20,7 +24,7 @@ class PatternMatch
#
# @param exhaustive: false [Boolean]
# If no matches are found, this will raise a
# `Qo::ExhaustiveMatchNotMet` error.
# `Qo::Errors::ExhaustiveMatchNotMet` error.
#
# @param &fn [Proc]
# Function to be used to construct the pattern matcher's branches
Expand All @@ -33,6 +37,13 @@ def initialize(destructure: false, exhaustive: false, &fn)
@exhaustive = exhaustive

yield(self) if block_given?

if lacking_branches?
raise Qo::Exceptions::ExhaustiveMatchMissingBranches.new(
expected_branches: available_branch_names,
given_branches: provided_matchers
)
end
end

# Allows for the creation of an anonymous PatternMatcher based on this
Expand Down Expand Up @@ -121,13 +132,19 @@ def self.create(branches: [])
# @param destructure: false [Boolean]
# Whether or not to destructure values before yielding to a block
#
# @param exhaustive: false [Boolean]
# If no matches are found, this will raise a
# `Qo::Errors::ExhaustiveMatchNotMet` error.
#
# @param as: :match [Symbol]
# Name to use as a method name bound to the including class
#
# @return [Module]
# Module to be mixed into a class
def self.mixin(destructure: false, as: :match)
create_self = -> &function { new(destructure: destructure, &function) }
def self.mixin(destructure: false, exhaustive: false, as: :match)
create_self = -> &function {
new(destructure: destructure, exhaustive: exhaustive, &function)
}

Module.new do
define_method(as) do |&function|
Expand All @@ -139,19 +156,56 @@ def self.mixin(destructure: false, as: :match)
# Whether or not the current pattern match requires a matching branch
#
# @return [Boolean]
def exhaustive_match?
@exhaustive && !@default
def exhaustive?
@exhaustive
end

# Whether or not the current pattern match is exhaustive and has a missing
# default branch
#
# @return [Boolean]
def exhaustive_no_default?
exhaustive? && !@default
end

# Names of all of the available branch names set in `Branching` on
# registration of a branch
#
# @return [Array[String]]
def available_branch_names
self.class.available_branches.keys
end

# Whether or not all branch types have been provided to the matcher.
#
# @return [Boolean]
def all_branches_provided?
available_branch_names == @provided_matchers.uniq
end

# Whether the current matcher is lacking branches
#
# @return [Boolean]
def lacking_branches?
exhaustive_no_default? && !all_branches_provided?
end

# Calls the pattern matcher, yielding the target value to the first
# matching branch it encounters.
#
# In the case of an exhaustive match, this will raise an error if no
# default branch is provided.
#
# @param value [Any]
# Value to match against
#
# @return [Any]
# Result of the called branch
#
# @raises [Qo::Exceptions::ExhaustiveMatchNotMet]
# If the matcher is exhaustive and no default branch is provided, it is
# considered to have failed an optimistic exhaustive match.
#
# @return [nil]
# Returns nil if no branch is matched
def call(value)
Expand All @@ -160,7 +214,7 @@ def call(value)
return return_value if status
end

raise Qo::Exceptions::ExhaustiveMatchNotMet if exhaustive_match?
raise Qo::Exceptions::ExhaustiveMatchNotMet if exhaustive_no_default?

if @default
_, return_value = @default.call(value)
Expand Down
100 changes: 99 additions & 1 deletion spec/pattern_matchers/pattern_match_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
require "spec_helper"

RSpec.describe Qo::PatternMatchers::PatternMatch do
let(:exhaustive) { false }

let(:pattern_match) do
Qo::PatternMatchers::PatternMatch.new { |m|
Qo::PatternMatchers::PatternMatch.new(exhaustive: exhaustive) { |m|
m.when(1) { |v| v + 4 }
m.when(2) { |v| v * 2 }
m.else { |v| v }
Expand Down Expand Up @@ -41,6 +43,57 @@
end
end

describe '#exhaustive?' do
it 'checks if a match is exhaustive' do
expect(pattern_match.exhaustive?).to eq(false)
end

context 'When a match is set as exhaustive' do
let(:exhaustive) { true }

it 'will be an exhaustive match' do
expect(pattern_match.exhaustive?).to eq(true)
end
end
end

describe '#exhaustive_no_default?' do
it 'checks if a match is exhaustive and lacks a default branch' do
expect(pattern_match.exhaustive_no_default?).to eq(false)
end

context 'When an exhaustive match is specified' do
let(:exhaustive) { true }

it 'will be false if there is a default' do
expect(pattern_match.exhaustive_no_default?).to eq(false)
end

context 'When there is no default' do
let(:pattern_match) do
Qo::PatternMatchers::PatternMatch.new(exhaustive: exhaustive) { |m|
m.when(1) { |v| v + 4 }
m.when(2) { |v| v * 2 }
}
end

it 'will raise a pessimistic error' do
error_message = <<~ERROR
Exhaustive match required: pattern does not specify all branches.
Expected Branches: when, else
Given Branches: when, when
ERROR

expect {
pattern_match.exhaustive_no_default?
}.to raise_error(
Qo::Exceptions::ExhaustiveMatchMissingBranches, error_message
)
end
end
end
end

# These are entirely for people to get ideas from. It uses the Public API
# shorthand to be less formal than the above specs.
#
Expand Down Expand Up @@ -114,5 +167,50 @@
expect(pattern_match.call(Person.new('Foo', 42)).age).to eq(43)
end
end

context 'When working with exhaustive matches' do
let(:pattern_match) {
Qo::PatternMatchers::PatternMatch.new(exhaustive: true) { |m|
m.when(name: /^F/) { |person| person.age + 1 }
}
}

it 'will raise an exception if not all branches are provided' do
expected_error = <<~ERROR
Exhaustive match required: pattern does not specify all branches.
Expected Branches: when, else
Given Branches: when
ERROR

expect {
pattern_match.call(Person.new('Foo', 42)).age
}.to raise_error(Qo::Exceptions::ExhaustiveMatchMissingBranches, expected_error)
end

context 'When all branches are provided' do
let(:pattern_match) {
Qo::PatternMatchers::PatternMatch.new(exhaustive: true) { |m|
m.when(name: /^F/) { |person| person.age + 1 }
m.else { 7 }
}
}

it 'will proceed as normal' do
expect(pattern_match.call(Person.new('Foo', 42))).to eq(43)
end
end

context 'When given a default branch' do
let(:pattern_match) {
Qo::PatternMatchers::PatternMatch.new(exhaustive: true) { |m|
m.else { |person| person.age + 1 }
}
}

it 'will ignore the strict requirement for all branches, as default satisfies exhaustive' do
expect(pattern_match.call(Person.new('Foo', 42))).to eq(43)
end
end
end
end
end

0 comments on commit e8d7d01

Please sign in to comment.