module RSpec::Matchers::Composable

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

Constants

DescribableItem

Wraps an item in order to surface its `description` via `inspect`. @api private

Public Class Methods

should_enumerate?(item) click to toggle source

@api private We should enumerate arrays as long as they are not recursive.

# File lib/rspec/matchers/composable.rb, line 142
def should_enumerate?(item)
  Array === item && item.none? { |subitem| subitem.equal?(item) }
end
surface_descriptions_in(item) click to toggle source

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

# File lib/rspec/matchers/composable.rb, line 98
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
unreadable_io?(object) click to toggle source

@api private

# File lib/rspec/matchers/composable.rb, line 147
def unreadable_io?(object)
  return false unless IO === object
  object.each {} # STDOUT is enumerable but raises an error
  false
rescue IOError
  true
end

Public Instance Methods

&(matcher)
Alias for: and
===(value) click to toggle source

Delegates to `#matches?`. Allows matchers to be used in composable fashion and also supports using matchers in case statements.

# File lib/rspec/matchers/composable.rb, line 45
def ===(value)
  matches?(value)
end
and(matcher) click to toggle source

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.
# File lib/rspec/matchers/composable.rb, line 22
def and(matcher)
  BuiltIn::Compound::And.new self, matcher
end
Also aliased as: &
or(matcher) click to toggle source

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.
# File lib/rspec/matchers/composable.rb, line 38
def or(matcher)
  BuiltIn::Compound::Or.new self, matcher
end
Also aliased as: |
|(matcher)
Alias for: or

Private Instance Methods

description_of(object) click to toggle source

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

# File lib/rspec/matchers/composable.rb, line 82
def description_of(object)
  RSpec::Support::ObjectFormatter.format(object)
end
should_enumerate?(item) click to toggle source

@api private We should enumerate arrays as long as they are not recursive.

# File lib/rspec/matchers/composable.rb, line 142
def should_enumerate?(item)
  Array === item && item.none? { |subitem| subitem.equal?(item) }
end
surface_descriptions_in(item) click to toggle source

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

# File lib/rspec/matchers/composable.rb, line 98
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
unreadable_io?(object) click to toggle source

@api private

# File lib/rspec/matchers/composable.rb, line 147
def unreadable_io?(object)
  return false unless IO === object
  object.each {} # STDOUT is enumerable but raises an error
  false
rescue IOError
  true
end
values_match?(expected, actual) click to toggle source

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

# File lib/rspec/matchers/composable.rb, line 66
def values_match?(expected, actual)
  expected = with_matchers_cloned(expected)
  Support::FuzzyMatcher.values_match?(expected, actual)
end
with_matchers_cloned(object) click to toggle source

@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.

# File lib/rspec/matchers/composable.rb, line 128
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