Skip to content

Commit faff17c

Browse files
committed
fix: parse query string to hash for v2 interactions
Fixes: pact-foundation/pact-mock_service#80
1 parent 8246d3b commit faff17c

File tree

4 files changed

+153
-2
lines changed

4 files changed

+153
-2
lines changed

lib/pact/consumer_contract/interaction_v2_parser.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require 'pact/consumer_contract/request'
22
require 'pact/consumer_contract/response'
33
require 'pact/consumer_contract/provider_state'
4+
require 'pact/consumer_contract/query'
45
require 'pact/symbolize_keys'
56
require 'pact/matching_rules'
67
require 'pact/errors'
@@ -15,13 +16,17 @@ def self.call hash, options
1516
response = parse_response(hash['response'], options)
1617
provider_states = parse_provider_states(hash['providerState'] || hash['provider_state'])
1718
metadata = parse_metadata(hash['metadata'])
18-
Interaction.new(symbolize_keys(hash).merge(request: request,
19-
response: response,
19+
Interaction.new(symbolize_keys(hash).merge(request: request,
20+
response: response,
2021
provider_states: provider_states,
2122
metadata: metadata))
2223
end
2324

2425
def self.parse_request request_hash, options
26+
if request_hash['query'].is_a?(String)
27+
request_hash = request_hash.dup
28+
request_hash['query'] = Pact::Query.parse_string(request_hash['query'])
29+
end
2530
request_hash = Pact::MatchingRules.merge(request_hash, request_hash['matchingRules'], options)
2631
Pact::Request::Expected.from_hash(request_hash)
2732
end

lib/pact/consumer_contract/query.rb

+98
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,110 @@
33

44
module Pact
55
class Query
6+
DEFAULT_SEP = /[&;] */n
7+
COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
8+
69
def self.create query
710
if query.is_a? Hash
811
Pact::QueryHash.new(query)
912
else
1013
Pact::QueryString.new(query)
1114
end
1215
end
16+
17+
def self.parse_string query_string
18+
parsed_query = parse_query(query_string)
19+
20+
# If Rails nested params...
21+
if parsed_query.keys.any?{ | key| key.include?("[") }
22+
parse_nested_query(query_string)
23+
else
24+
parsed_query.each_with_object({}) do | (key, value), new_hash |
25+
new_hash[key] = [*value]
26+
end
27+
end
28+
end
29+
30+
# Ripped from Rack to avoid adding an unnecessary dependency, thank you Rack
31+
# https://github.com/rack/rack/blob/649c72bab9e7b50d657b5b432d0c205c95c2be07/lib/rack/utils.rb
32+
def self.parse_query(qs, d = nil, &unescaper)
33+
unescaper ||= method(:unescape)
34+
35+
params = {}
36+
37+
(qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
38+
next if p.empty?
39+
k, v = p.split('=', 2).map!(&unescaper)
40+
41+
if cur = params[k]
42+
if cur.class == Array
43+
params[k] << v
44+
else
45+
params[k] = [cur, v]
46+
end
47+
else
48+
params[k] = v
49+
end
50+
end
51+
52+
return params.to_h
53+
end
54+
55+
def self.parse_nested_query(qs, d = nil)
56+
params = {}
57+
58+
unless qs.nil? || qs.empty?
59+
(qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
60+
k, v = p.split('=', 2).map! { |s| unescape(s) }
61+
62+
normalize_params(params, k, v)
63+
end
64+
end
65+
66+
return params.to_h
67+
end
68+
69+
def self.normalize_params(params, name, v)
70+
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
71+
k = $1 || ''
72+
after = $' || ''
73+
74+
if k.empty?
75+
if !v.nil? && name == "[]"
76+
return Array(v)
77+
else
78+
return
79+
end
80+
end
81+
82+
if after == ''
83+
params[k] = v
84+
elsif after == "["
85+
params[name] = v
86+
elsif after == "[]"
87+
params[k] ||= []
88+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
89+
params[k] << v
90+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
91+
child_key = $1
92+
params[k] ||= []
93+
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
94+
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
95+
normalize_params(params[k].last, child_key, v)
96+
else
97+
params[k] << normalize_params({}, child_key, v)
98+
end
99+
else
100+
params[k] ||= {}
101+
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
102+
params[k] = normalize_params(params[k], after, v, depth - 1)
103+
end
104+
105+
params
106+
end
107+
108+
def self.unescape(s, encoding = Encoding::UTF_8)
109+
URI.decode_www_form_component(s, encoding)
110+
end
13111
end
14112
end

spec/lib/pact/consumer_contract/interaction_v2_parser_spec.rb

+15
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,21 @@ module Pact
4949
expect(subject.provider_state).to eq "foo"
5050
end
5151
end
52+
53+
describe "query" do
54+
let(:interaction_hash) do
55+
{
56+
"description" => "description",
57+
"request" => { "method" => "GET", "path" => "/", "query" => "foo=bar1&foo=bar2"},
58+
"response" => { "status" => 200 },
59+
"providerState" => "foo"
60+
}
61+
end
62+
63+
it "parses a string query into a hash" do
64+
expect(subject.request.query).to eq Pact::QueryHash.new("foo"=> [ "bar1", "bar2" ])
65+
end
66+
end
5267
end
5368
end
5469
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'pact/consumer_contract/query'
2+
3+
module Pact
4+
describe Query do
5+
describe ".parse_string" do
6+
subject { Query.parse_string(query_string) }
7+
8+
describe "with a non nested query string" do
9+
let(:query_string) { "foo=bar1" }
10+
11+
it "returns a map of string to array" do
12+
expect(subject).to eq "foo" => ["bar1"]
13+
end
14+
end
15+
16+
describe "with a non nested query string with multiple params with the same name" do
17+
let(:query_string) { "foo=bar1&foo=bar2" }
18+
19+
it "returns a map of string to array" do
20+
expect(subject).to eq "foo" => ["bar1", "bar2"]
21+
end
22+
end
23+
24+
describe "with a rails style nested query" do
25+
let(:query_string) { "foo=bar1&foo=bar2&baz[]=thing1&baz[]=thing2" }
26+
27+
it "returns a nested map" do
28+
expect(subject).to eq "foo" => "bar2", "baz" => ["thing1", "thing2"]
29+
end
30+
end
31+
end
32+
end
33+
end

0 commit comments

Comments
 (0)