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 structure (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 structure (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