class RSpec::SleepingKingStudios::Matchers::Core::DeepMatcher

Matcher for performing a deep comparison between two objects.

@since 2.5.0

Public Class Methods

new(expected) click to toggle source

@param [Object] expected The expected object.

# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 14
def initialize(expected)
  @expected = expected
end

Public Instance Methods

description() click to toggle source

(see BaseMatcher#description)

# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 19
def description
  "match #{format_expected(@expected)}"
end
does_not_match?(actual) click to toggle source

Inverse of matches? method.

@param [Object] actual The object to check.

@return [Boolean] true if the actual object does not match the

expectation, otherwise true.

@see matches?

# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 31
def does_not_match? actual
  super

  if matcher?(@expected)
    delegate_to_negated_matcher(@expected)
  elsif @expected.is_a?(Array) && actual.is_a?(Array)
    diff_arrays_negated
  elsif @expected.is_a?(Hash) && actual.is_a?(Hash)
    diff_hashes_negated
  else
    delegate_to_negated_matcher(equality_matcher)
  end

  !@matches
end
failure_message() click to toggle source

(see BaseMatcher#failure_message)

# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 48
def failure_message
  @failure_message
end
failure_message_when_negated() click to toggle source

(see BaseMatcher#failure_message_when_negated)

# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 53
def failure_message_when_negated
  @failure_message_when_negated
end
matches?(actual) click to toggle source

Performs a deep comparison between the actual object and the expected object. The type of comparison depends on the type of the expected object:

  • If the expected object is an RSpec matcher, the matches? method on the matcher is called with the expected object.

  • If the expected object is an Array, then each item is compared based on the type of the expected item.

  • If the expected object is a Hash, then the keys must match and each value is compared based on the type of the expected value.

  • Otherwise, the two objects are compared using an equality comparison.

@param [Object] actual The object to check.

@return [Boolean] true if the actual object matches the expectation,

otherwise false.
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 72
def matches?(actual)
  super

  if matcher?(@expected)
    delegate_to_matcher(@expected)
  elsif @expected.is_a?(Array) && actual.is_a?(Array)
    diff_arrays
  elsif @expected.is_a?(Hash)  && actual.is_a?(Hash)
    diff_hashes
  else
    delegate_to_matcher(equality_matcher)
  end

  @matches
end

Private Instance Methods

compare_arrays(expected, actual) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 90
def compare_arrays(expected, actual)
  compare_hashes({ _ary: expected }, { _ary: actual })
    .map { |(char, path, *values)| [char, path[1..-1], *values] }
end
compare_hashes(expected, actual) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 95
def compare_hashes(expected, actual)
  HashDiff.diff(expected, actual, array_path: true, use_lcs: false) \
  do |path, exp, act|
    # Handle missing keys with matcher values.
    next nil unless nested_key?(actual, path)

    next exp.matches?(act) if matcher?(exp)
  end
end
delegate_to_matcher(matcher) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 105
def delegate_to_matcher(matcher)
  @matches = matcher.matches?(actual)

  return if @matches

  @failure_message = matcher.failure_message
end
delegate_to_negated_matcher(matcher) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 113
def delegate_to_negated_matcher(matcher)
  @matches =
    if matcher.respond_to?(:does_not_match?)
      !matcher.does_not_match?(actual)
    else
      matcher.matches?(actual)
    end

  return unless @matches

  @failure_message_when_negated = matcher.failure_message_when_negated
end
diff_arrays() click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 126
def diff_arrays
  diff     = compare_arrays(@expected, actual)
  @matches = diff.empty?

  @failure_message = format_message(diff)
end
diff_arrays_negated() click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 133
def diff_arrays_negated
  diff     = compare_arrays(@expected, actual)
  @matches = diff.empty?

  @failure_message_when_negated =
    "`expect(#{format_expected(@expected)}).not_to be == #{actual.inspect}`"
end
diff_hashes() click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 141
def diff_hashes
  diff     = compare_hashes(@expected, actual)
  @matches = diff.empty?

  @failure_message = format_message(diff)
end
diff_hashes_negated() click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 148
def diff_hashes_negated
  diff     = compare_hashes(@expected, actual)
  @matches = diff.empty?

  @failure_message_when_negated =
    "`expect(#{format_expected(@expected)}).not_to be == #{actual.inspect}`"
end
equality_matcher() click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 156
def equality_matcher
  matchers_delegate.be == @expected
end
format_diff(diff) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 160
def format_diff(diff)
  diff
    .sort_by { |(char, path, *_values)| [path.map(&:to_s)] }
    .map { |item| format_diff_item(*item) }
    .join "\n"
end
format_diff_item(char, path, *values) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 167
def format_diff_item(char, path, *values)
  "#{char} #{format_diff_path(path)} => #{format_diff_values(char, values)}"
end
format_diff_path(path) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 171
def format_diff_path(path)
  path.map(&:inspect).join('.')
end
format_diff_values(char, values) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 175
def format_diff_values(char, values)
  case char
  when '-'
    "expected #{format_expected(values.first)}"
  when '~'
    "expected #{format_expected(values.first)}, got #{values.last.inspect}"
  when '+'
    "got #{values.last.inspect}"
  end
end
format_expected(object) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 186
def format_expected(object)
  RSpec::Support::ObjectFormatter.format(object)
end
format_message(diff) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 190
def format_message(diff)
  "expected: == #{format_expected(@expected)}\n" \
  "     got:    #{@actual.inspect}\n" \
  "\n" \
  "(compared using HashDiff)\n" \
  "\n" \
  "Diff:\n" \
  "#{format_diff(diff)}"
end
matcher?(object) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 200
def matcher?(object)
  %i[description failure_message failure_message_when_negated matches?]
    .all? { |method_name| object.respond_to?(method_name) }
end
matchers_delegate() click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 205
def matchers_delegate
  Object.new.extend RSpec::Matchers
end
nested_key?(object, path) click to toggle source
# File lib/rspec/sleeping_king_studios/matchers/core/deep_matcher.rb, line 209
def nested_key?(object, path)
  key    = path.last
  object = object.dig(*path[0...-1]) if path.size > 1

  return object.key?(key) if object.is_a?(Hash)
  return object.size > key if object.is_a?(Array)

  false
end