lib/rspec/matchers/composable.rb
RSpec::Support.require_rspec_support "fuzzy_matcher"
module RSpec
module Matchers
# Mixin designed to support the composable matcher features
# of RSpec 3+. Mix it into your custom matcher classes to
# allow them to be used in a composable fashion.
#
# @api public
module Composable
# Creates a compound `and` expectation. The matcher will
# only pass if both sub-matchers pass.
# This can be chained together to form an arbitrarily long
# chain of matchers.
#
# @example
# expect(alphabet).to start_with("a").and end_with("z")
# expect(alphabet).to start_with("a") & end_with("z")
#
# @note The negative form (`expect(...).not_to matcher.and other`)
# is not supported at this time.
def and(matcher)
BuiltIn::Compound::And.new self, matcher
end
alias & and
# Creates a compound `or` expectation. The matcher will
# pass if either sub-matcher passes.
# This can be chained together to form an arbitrarily long
# chain of matchers.
#
# @example
# expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
# expect(stoplight.color).to eq("red") | eq("green") | eq("yellow")
#
# @note The negative form (`expect(...).not_to matcher.or other`)
# is not supported at this time.
def or(matcher)
BuiltIn::Compound::Or.new self, matcher
end
alias | or
# Delegates to `#matches?`. Allows matchers to be used in composable
# fashion and also supports using matchers in case statements.
def ===(value)
matches?(value)
end
private
# This provides a generic way to fuzzy-match an expected value against
# an actual value. It understands nested data structures (e.g. hashes
# and arrays) and is able to match against a matcher being used as
# the expected value or within the expected value at any level of
# nesting.
#
# Within a custom matcher you are encouraged to use this whenever your
# matcher needs to match two values, unless it needs more precise semantics.
# For example, the `eq` matcher _does not_ use this as it is meant to
# use `==` (and only `==`) for matching.
#
# @param expected [Object] what is expected
# @param actual [Object] the actual value
#
# @!visibility public
def values_match?(expected, actual)
expected = with_matchers_cloned(expected)
Support::FuzzyMatcher.values_match?(expected, actual)
end
# Returns the description of the given object in a way that is
# aware of composed matchers. If the object is a matcher with
# a `description` method, returns the description; otherwise
# returns `object.inspect`.
#
# You are encouraged to use this in your custom matcher's
# `description`, `failure_message` or
# `failure_message_when_negated` implementation if you are
# supporting matcher arguments.
#
# @!visibility public
def description_of(object)
RSpec::Support::ObjectFormatter.format(object)
end
# Transforms the given data structue (typically a hash or array)
# into a new data structure that, when `#inspect` is called on it,
# will provide descriptions of any contained matchers rather than
# the normal `#inspect` output.
#
# You are encouraged to use this in your custom matcher's
# `description`, `failure_message` or
# `failure_message_when_negated` implementation if you are
# supporting any arguments which may be a data structure
# containing matchers.
#
# @!visibility public
def surface_descriptions_in(item)
if Matchers.is_a_describable_matcher?(item)
DescribableItem.new(item)
elsif Hash === item
Hash[surface_descriptions_in(item.to_a)]
elsif Struct === item || unreadable_io?(item)
RSpec::Support::ObjectFormatter.format(item)
elsif should_enumerate?(item)
item.map { |subitem| surface_descriptions_in(subitem) }
else
item
end
end
# @private
# Historically, a single matcher instance was only checked
# against a single value. Given that the matcher was only
# used once, it's been common to memoize some intermediate
# calculation that is derived from the `actual` value in
# order to reuse that intermediate result in the failure
# message.
#
# This can cause a problem when using such a matcher as an
# argument to another matcher in a composed matcher expression,
# since the matcher instance may be checked against multiple
# values and produce invalid results due to the memoization.
#
# To deal with this, we clone any matchers in `expected` via
# this method when using `values_match?`, so that any memoization
# does not "leak" between checks.
def with_matchers_cloned(object)
if Matchers.is_a_matcher?(object)
object.clone
elsif Hash === object
Hash[with_matchers_cloned(object.to_a)]
elsif should_enumerate?(object)
object.map { |subobject| with_matchers_cloned(subobject) }
else
object
end
end
# @api private
# We should enumerate arrays as long as they are not recursive.
def should_enumerate?(item)
Array === item && item.none? { |subitem| subitem.equal?(item) }
end
# @api private
def unreadable_io?(object)
return false unless IO === object
object.each {} # STDOUT is enumerable but raises an error
false
rescue IOError
true
end
module_function :surface_descriptions_in, :should_enumerate?, :unreadable_io?
# Wraps an item in order to surface its `description` via `inspect`.
# @api private
DescribableItem = Struct.new(:item) do
# Inspectable version of the item description
def inspect
"(#{item.description})"
end
# A pretty printed version of the item description.
def pretty_print(pp)
pp.text "(#{item.description})"
end
end
end
end
end