require 'awesome_print' require 'pact/term' require 'pact/something_like' require 'pact/shared/null_expectation' require 'pact/shared/key_not_found' require 'pact/matchers/unexpected_key' require 'pact/matchers/unexpected_index' require 'pact/matchers/index_not_found' require 'pact/matchers/difference' require 'pact/matchers/regexp_difference' require 'pact/matchers/type_difference' require 'pact/matchers/expected_type' require 'pact/matchers/actual_type' require 'pact/matchers/no_diff_at_index' require 'pact/array_like' module Pact # should be called Differs module Matchers NO_DIFF_AT_INDEX = NoDiffAtIndex.new DEFAULT_OPTIONS = {allow_unexpected_keys: true, type: false}.freeze NO_DIFF = {}.freeze def diff expected, actual, opts = {} calculate_diff(Pact::Term.unpack_regexps(expected), actual, DEFAULT_OPTIONS.merge(opts)) end def type_diff expected, actual, opts = {} calculate_diff Pact::Term.unpack_regexps(expected), actual, DEFAULT_OPTIONS.merge(opts).merge(type: true) end private def calculate_diff expected, actual, opts = {} options = DEFAULT_OPTIONS.merge(opts) case expected when Hash then hash_diff(expected, actual, options) when Array then array_diff(expected, actual, options) when Regexp then regexp_diff(expected, actual, options) when Pact::SomethingLike then calculate_diff(expected.contents, actual, options.merge(:type => true)) when Pact::ArrayLike then array_like_diff(expected, actual, options) else object_diff(expected, actual, options) end end alias_method :structure_diff, :type_diff # Backwards compatibility def regexp_diff regexp, actual, options if actual.is_a?(String) && regexp.match(actual) NO_DIFF else RegexpDifference.new regexp, actual end end def array_diff expected, actual, options if actual.is_a? Array actual_array_diff expected, actual, options else Difference.new expected, actual end end def actual_array_diff expected, actual, options difference = [] diff_found = false length = [expected.length, actual.length].max length.times do | index| expected_item = expected.fetch(index, Pact::UnexpectedIndex.new) actual_item = actual.fetch(index, Pact::IndexNotFound.new) if (item_diff = calculate_diff(expected_item, actual_item, options)).any? diff_found = true difference << item_diff else difference << NO_DIFF_AT_INDEX end end diff_found ? difference : NO_DIFF end def array_like_diff array_like, actual, options if actual.is_a? Array expected_size = [array_like.min, actual.size].max expected_array = expected_size.times.collect{ Pact::Term.unpack_regexps(array_like.contents) } actual_array_diff expected_array, actual, options.merge(:type => true) else Difference.new array_like.generate, actual end end def hash_diff expected, actual, options if actual.is_a? Hash actual_hash_diff expected, actual, options else Difference.new expected, actual end end def actual_hash_diff expected, actual, options hash_diff = expected.each_with_object({}) do |(key, value), difference| diff_at_key = calculate_diff(value, actual.fetch(key, Pact::KeyNotFound.new), options) difference[key] = diff_at_key if diff_at_key.any? end hash_diff.merge(check_for_unexpected_keys(expected, actual, options)) end def check_for_unexpected_keys expected, actual, options if options[:allow_unexpected_keys] NO_DIFF else (actual.keys - expected.keys).each_with_object({}) do | key, running_diff | running_diff[key] = Difference.new(UnexpectedKey.new, actual[key]) end end end def object_diff expected, actual, options if options[:type] type_difference expected, actual else exact_value_diff expected, actual, options end end def exact_value_diff expected, actual, options if expected != actual Difference.new expected, actual else NO_DIFF end end def type_difference expected, actual if types_match? expected, actual NO_DIFF else TypeDifference.new type_diff_expected_display(expected), type_diff_actual_display(actual) end end def type_diff_expected_display expected ExpectedType.new(expected) end def type_diff_actual_display actual actual.is_a?(KeyNotFound) ? actual : ActualType.new(actual) end def types_match? expected, actual expected.class == actual.class || (is_boolean(expected) && is_boolean(actual)) end def is_boolean object object == true || object == false end end end