diff --git a/lib/pact/combined_match.rb b/lib/pact/combined_match.rb new file mode 100644 index 0000000..94d9edd --- /dev/null +++ b/lib/pact/combined_match.rb @@ -0,0 +1,15 @@ +module Pact + + # Specifies that the actual object should be considered a match if + # it matches any of the matchers depending on combinator operation. + + class CombinedMatch + + attr_reader :combiner, :matchers + + def initialize combiner, matchers + @combiner = combiner + @matchers = matchers + end + end +end diff --git a/lib/pact/matchers/matchers.rb b/lib/pact/matchers/matchers.rb index 8f5bb88..9f27575 100644 --- a/lib/pact/matchers/matchers.rb +++ b/lib/pact/matchers/matchers.rb @@ -1,6 +1,7 @@ require 'pact/configuration' require 'pact/term' require 'pact/something_like' +require 'pact/combined_match' require 'pact/array_like' require 'pact/shared/null_expectation' require 'pact/shared/key_not_found' @@ -54,6 +55,7 @@ def calculate_diff expected, actual, opts = {} when Pact::SomethingLike then calculate_diff(expected.contents, actual, options.merge(:type => true)) when Pact::ArrayLike then array_like_diff(expected, actual, options) when Pact::Term then term_diff(expected, actual, options) + when Pact::CombinedMatch then combined_match_diff(expected, actual, options) else object_diff(expected, actual, options) end end @@ -156,6 +158,31 @@ def calculate_diff_at_key key, expected_value, actual, difference, options diff_at_key end + def combined_match_diff expected, actual, options + if expected.matchers.empty? + return NO_DIFF + end + case expected.combiner + when 'AND' then + expected.matchers.each do |e| + diff = calculate_diff(e, actual, options) + if diff.any? + return diff + end + end + return NO_DIFF + when 'OR' then + diff = nil + expected.matchers.each do |e| + diff = calculate_diff(e, actual, options) + if diff.empty? + return NO_DIFF + end + end + return diff + end + end + def check_for_unexpected_keys expected, actual, options if options[:allow_unexpected_keys] NO_DIFF diff --git a/lib/pact/matching_rules/v3/merge.rb b/lib/pact/matching_rules/v3/merge.rb index 2a276b1..9c67674 100644 --- a/lib/pact/matching_rules/v3/merge.rb +++ b/lib/pact/matching_rules/v3/merge.rb @@ -1,4 +1,5 @@ require 'pact/array_like' +require 'pact/combined_match' require 'pact/matching_rules/jsonpath' module Pact @@ -81,14 +82,36 @@ def warn_when_not_one_example_item array, path end def wrap object, path - rules = @matching_rules[path] && @matching_rules[path]['matchers'] && @matching_rules[path]['matchers'].first - array_rules = @matching_rules["#{path}[*]*"] && @matching_rules["#{path}[*]*"]['matchers'] && @matching_rules["#{path}[*]*"]['matchers'].first - return object unless rules || array_rules - - if rules['match'] == 'type' && !rules.has_key?('min') - handle_match_type(object, path, rules) - elsif rules['regex'] - handle_regex(object, path, rules) + rules = @matching_rules[path] + return object unless rules + + combiner = rules['combine'] || 'AND' + if ['AND', 'OR'].include?(combiner) then + rules.delete('combine') + else + # unsupported combine will be reported + return object + end + + matchers = rules['matchers'] + # TODO make it work with array rules + # array_rules = @matching_rules["#{path}[*]*"] && @matching_rules["#{path}[*]*"]['matchers'] && @matching_rules["#{path}[*]*"]['matchers'].first + + wrapped_matchers = matchers.map do |rule| + wrap_single(rule, object, path) + end + Pact::CombinedMatch.new(combiner, wrapped_matchers) + end + + def wrap_single rule, object, path + if rule['match'] == 'type' && !rule.has_key?('min') + handle_match_type(object, path, rule) + elsif rule['match'] == 'null' + handle_null_type(object, path, rule) + elsif rule['match'] == 'timestamp' && rule.has_key?('timestamp') + handle_timestamp_type(object, path, rule) + elsif rule['regex'] + handle_regex(object, path, rule) else object end @@ -99,6 +122,26 @@ def handle_match_type object, path, rules Pact::SomethingLike.new(object) end + def handle_null_type object, path, rule + rule.delete('match') + Pact::SomethingLike.new(nil) + end + + def handle_timestamp_type object, path, rule + # barebone timestamp support + supported_formats = { + "yyyy-MM-dd" => /[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ + } + regexp = supported_formats[rule['timestamp']] + if regexp then + rule.delete('match') + rule.delete('timestamp') + Pact::Term.new(generate: object, matcher: Regexp.new(regexp)) + else + object + end + end + def handle_regex object, path, rules rules.delete('match') regex = rules.delete('regex') @@ -109,9 +152,7 @@ def log_ignored_rules @matching_rules.each do | jsonpath, rules_hash | rules_array = rules_hash["matchers"] if rules_array - ((rules_array.length - 1)..0).each do | index | - rules_array.delete_at(index) if rules_array[index].empty? - end + rules_hash["matchers"] = rules_array.select { | item | item.any? } end end