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
@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
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
@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
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
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
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
Private Instance Methods
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
@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
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
@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
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
@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