Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exhaustive match #26

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions lib/qo/exceptions.rb
Original file line number Diff line number Diff line change
@@ -8,5 +8,32 @@ module Qo
# @since 0.2.0
#
module Exceptions
# Error for not all possible cases being handled. This is optimistic as currently
# it will only catch when an actual failure to handle a case is present. This
# should be patched in later versions
#
# @author baweaver
# @since 0.99.1
class ExhaustiveMatchNotMet < StandardError
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest having common ancestor for all Qo errors and, probably for exhaustive match errors. So end users will be able to catch errors with desired granularity. For example Qo::Error, Qo, MatchError, ExhaustiveMatchNotMet etc.

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
@@ -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
@@ -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(
74 changes: 71 additions & 3 deletions lib/qo/pattern_matchers/pattern_match.rb
Original file line number Diff line number Diff line change
@@ -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
@@ -18,16 +22,28 @@ class PatternMatch
# @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 &fn [Proc]
# Function to be used to construct the pattern matcher's branches
#
# @return [Qo::PatternMatchers::PatternMatch]
def initialize(destructure: false, &fn)
def initialize(destructure: false, exhaustive: false, &fn)
@matchers = []
@default = nil
@destructure = destructure
@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
@@ -116,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|
@@ -131,15 +153,59 @@ def self.mixin(destructure: false, as: :match)
end
end

# Whether or not the current pattern match requires a matching branch
#
# @return [Boolean]
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
Copy link
Contributor

@bolshakov bolshakov Feb 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May fail if you provide branches in order other than order of registration

Suggested change
available_branch_names == @provided_matchers.uniq
available_branch_names.sort == @provided_matchers.uniq.sort

or you can store names as an instance of Set

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to integrate latest changes into my PR, and got this error:

     Qo::Exceptions::ExhaustiveMatchMissingBranches:
       Exhaustive match required: pattern does not specify all branches.
         Expected Branches: right, left
         Given Branches:    left, right

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may jump back to implementing this with sets, silly mistake on my part.

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)
@@ -148,6 +214,8 @@ def call(value)
return return_value if status
end

raise Qo::Exceptions::ExhaustiveMatchNotMet if exhaustive_no_default?

if @default
_, return_value = @default.call(value)
return_value
10 changes: 7 additions & 3 deletions lib/qo/pattern_matchers/result_pattern_match.rb
Original file line number Diff line number Diff line change
@@ -36,9 +36,13 @@ class ResultPatternMatch < PatternMatch
# Whether or not to destructure the value before yielding to
# the first matched block
#
# @return [type] [description]
def initialize(destructure: false)
super(destructure: destructure)
# @param exhaustive: false [Boolean]
# If no matches are found, this will raise a
# `Qo::ExhaustiveMatchNotMet` error.
#
# @return [Qo::PatternMatch::ResultPatternMatch]
def initialize(destructure: false, exhaustive: false)
super(destructure: destructure, exhaustive: exhaustive)
end
end
end
2 changes: 1 addition & 1 deletion lib/qo/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Qo
VERSION = '0.99.0'
VERSION = '0.99.1'
end
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 }
@@ -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.
#
@@ -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
14 changes: 13 additions & 1 deletion spec/pattern_matchers/result_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::ResultPatternMatch do
let(:exhaustive) { false }

let(:pattern_match) do
Qo::PatternMatchers::ResultPatternMatch.new { |m|
Qo::PatternMatchers::ResultPatternMatch.new(exhaustive: exhaustive) { |m|
m.success(Integer) { |v| v + 1 }
m.success(String) { |v| "OHAI #{v}!" }
m.success { |v| 42 }
@@ -47,6 +49,16 @@
it 'will return nil' do
expect(pattern_match.call([:wat?, :wat?])).to eq(nil)
end

context 'When the match is required to be exhaustive' do
let(:exhaustive) { true }

it 'will raise an error' do
expect {
pattern_match.call([:wat?, :wat?])
}.to raise_error(Qo::Exceptions::ExhaustiveMatchNotMet)
end
end
end
end
end