From fc89696f8bb3479d5186a7b099c5123219d6cdff Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Tue, 3 Apr 2018 15:50:19 +1000 Subject: [PATCH] feat: add support for writing v3 matching rules --- lib/pact/matching_rules.rb | 11 +- lib/pact/matching_rules/v3/extract.rb | 94 +++++++ .../pact/matching_rules/v3/extract_spec.rb | 238 ++++++++++++++++++ 3 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 lib/pact/matching_rules/v3/extract.rb create mode 100644 spec/lib/pact/matching_rules/v3/extract_spec.rb diff --git a/lib/pact/matching_rules.rb b/lib/pact/matching_rules.rb index c6011e5..7facaa6 100644 --- a/lib/pact/matching_rules.rb +++ b/lib/pact/matching_rules.rb @@ -7,11 +7,18 @@ module MatchingRules # @api public Used by pact-mock_service def self.extract object_graph, options = {} - Extract.(object_graph) + pact_specification_version = options[:pact_specification_version] || Pact::SpecificationVersion::NIL_VERSION + case pact_specification_version.major + when nil, 0, 1, 2 + Extract.(object_graph, matching_rules) + else + V3::Extract.(object_graph, matching_rules) + end end def self.merge object_graph, matching_rules, options = {} - case options[:pact_specification_version].major + pact_specification_version = options[:pact_specification_version] || Pact::SpecificationVersion::NIL_VERSION + case pact_specification_version.major when nil, 0, 1, 2 Merge.(object_graph, matching_rules) else diff --git a/lib/pact/matching_rules/v3/extract.rb b/lib/pact/matching_rules/v3/extract.rb new file mode 100644 index 0000000..8cced5a --- /dev/null +++ b/lib/pact/matching_rules/v3/extract.rb @@ -0,0 +1,94 @@ +require 'pact/something_like' +require 'pact/array_like' +require 'pact/term' + +module Pact + module MatchingRules::V3 + class Extract + + def self.call matchable + new(matchable).call + end + + def initialize matchable + @matchable = matchable + @rules = Hash.new + end + + def call + recurse matchable, "$", nil + rules + end + + private + + attr_reader :matchable, :rules + + def recurse object, path, match_type + case object + when Hash then recurse_hash(object, path, match_type) + when Array then recurse_array(object, path, match_type) + when Pact::SomethingLike then handle_something_like(object, path, match_type) + when Pact::ArrayLike then handle_array_like(object, path, match_type) + when Pact::Term then record_regex_rule object, path + when Pact::QueryString then recurse(object.query, path, match_type) + when Pact::QueryHash then recurse_hash(object.query, path, match_type) + end + end + + def recurse_hash hash, path, match_type + hash.each do | (key, value) | + recurse value, "#{path}#{next_path_part(key)}", match_type + end + end + + def recurse_array new_array, path, match_type + new_array.each_with_index do | value, index | + recurse value, "#{path}[#{index}]", match_type + end + end + + def handle_something_like something_like, path, match_type + record_match_type_rule path, "type" + recurse something_like.contents, path, "type" + end + + def handle_array_like array_like, path, match_type + record_rule "#{path}", 'min' => array_like.min + record_match_type_rule "#{path}[*].*", 'type' + recurse array_like.contents, "#{path}[*]", :array_like + end + + def record_rule path, rule + rules[path] ||= {} + rules[path]['matchers'] ||= [] + rules[path]['matchers'] << rule + end + + def record_regex_rule term, path + rules[path] ||= {} + rules[path]['matchers'] ||= [] + rule = { 'match' => 'regex', 'regex' => term.matcher.inspect[1..-2]} + rules[path]['matchers'] << rule + end + + def record_match_type_rule path, match_type + unless match_type == :array_like || match_type.nil? + rules[path] ||= {} + rules[path]['matchers'] ||= [] + rules[path]['matchers'] << { 'match' => match_type } + end + end + + # Beth: there's a potential bug if the key contains a dot and a single quote. + # Not sure what to do then. + def next_path_part key + if key.to_s.include?('.') + "['#{key}']" + else + ".#{key}" + end + end + end + end +end diff --git a/spec/lib/pact/matching_rules/v3/extract_spec.rb b/spec/lib/pact/matching_rules/v3/extract_spec.rb new file mode 100644 index 0000000..8458c06 --- /dev/null +++ b/spec/lib/pact/matching_rules/v3/extract_spec.rb @@ -0,0 +1,238 @@ +require 'pact/matching_rules/v3/extract' +require 'pact/support' + +module Pact + module MatchingRules::V3 + describe Extract do + + describe ".call" do + + subject { Extract.call(matchable) } + + context "with a Pact::SomethingLike" do + let(:matchable) do + { + body: Pact::SomethingLike.new(foo: 'bar', alligator: { name: 'Mary' }) + } + end + + let(:rules) do + { + "$.body" => { + "matchers" => [ {"match" => "type"} ] + } + } + end + + it "creates a rule that matches by type" do + expect(subject).to eq rules + end + end + + context "with a Pact::Term" do + let(:matchable) do + { + body: { + alligator: { + name: Pact::Term.new(generate: 'Mary', matcher: /.*a/) + } + } + } + end + + let(:rules) do + { + "$.body.alligator.name" => { + "matchers" => [ {"match" => "regex", "regex" => ".*a"} ] + } + } + end + + it "creates a rule that matches by regex" do + expect(subject).to eq rules + end + end + + context "with a Pact::SomethingLike containing a Term" do + let(:matchable) do + { + body: Pact::SomethingLike.new( + foo: 'bar', + alligator: { name: Pact::Term.new(generate: 'Mary', matcher: /.*a/) } + ) + } + end + + let(:rules) do + { + "$.body" => { + "matchers" => [ {"match" => "type"} ] + }, + "$.body.alligator.name" => { + "matchers" => [ {"match" => "regex", "regex"=>".*a"} ] + }, + } + end + + it "the match:regex overrides the match:type" do + expect(subject).to eq rules + end + end + + context "with a Pact::SomethingLike containing an array" do + let(:matchable) do + { + body: Pact::SomethingLike.new( + alligators: [ + {name: 'Mary'}, + {name: 'Betty'} + ] + ) + } + end + + let(:rules) do + { + "$.body" => { + "matchers" => [ {"match" => "type"} ] + } + } + end + + it "lists a rule for each item" do + expect(subject).to eq rules + end + end + + context "with an ArrayLike" do + let(:matchable) do + { + body: { + alligators: Pact::ArrayLike.new( + name: 'Fred' + ) + } + } + end + + let(:rules) do + { + "$.body.alligators" => { + "matchers" => [ {"min" => 1} ] + }, + "$.body.alligators[*].*" => { + "matchers" => [ {"match" => "type"} ] + } + } + end + + it "lists a rule for all items" do + expect(subject).to eq rules + end + end + + context "with an ArrayLike with a Pact::Term inside" do + let(:matchable) do + { + body: { + alligators: Pact::ArrayLike.new( + name: 'Fred', + phoneNumber: Pact::Term.new(generate: '1234567', matcher: /\d+/) + ) + } + } + end + + let(:rules) do + { + "$.body.alligators" => { + "matchers" => [ {"min" => 1} ] + }, + "$.body.alligators[*].*" => { + "matchers" => [ {"match" => "type"} ] + }, + "$.body.alligators[*].phoneNumber" => { + "matchers" => [ {"match" => "regex", "regex" => "\\d+"} ] + } + } + end + + it "lists a rule that specifies that the regular expression must match" do + expect(subject).to eq rules + end + end + + context "with a Pact::QueryString containing a Pact::Term" do + let(:matchable) do + { + query: Pact::QueryString.new(Pact::Term.new(generate: 'foobar', matcher: /foo/)) + } + end + + let(:rules) do + { + "$.query" => { + "matchers" => [ {"match" => "regex", "regex" => "foo"} ] + } + } + end + + it "lists a rule that specifies that the regular expression must match" do + expect(subject).to eq rules + end + end + + context "with a Pact::QueryHash containing a Pact::Term" do + let(:matchable) do + { + query: Pact::QueryHash.new(bar: Pact::Term.new(generate: 'foobar', matcher: /foo/)) + } + end + + let(:rules) do + { + "$.query.bar[0]" => { + "matchers" => [ {"match" => "regex", "regex" => "foo"} ] + } + } + end + + it "lists a rule that specifies that the regular expression must match" do + expect(subject).to eq rules + end + end + + context "with no special matching" do + let(:matchable) do + { + body: { alligator: { name: 'Mary' } } + } + end + + let(:rules) do + {} + end + + + it "does not create any rules" do + expect(subject).to eq rules + end + end + + context "with a key containing a dot" do + let(:matchable) do + { + "key" => { + "key.with.dots" => Pact::SomethingLike.new("foo") + } + } + end + + it "uses square brackets notation for the key with dots" do + expect(subject.keys).to include "$.key['key.with.dots']" + end + end + end + end + end +end