webmock 1.18.0 → 1.19.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/CHANGELOG.md +46 -0
- data/README.md +11 -1
- data/lib/webmock/config.rb +6 -0
- data/lib/webmock/errors.rb +9 -7
- data/lib/webmock/http_lib_adapters/em_http_request/em_http_request_1_x.rb +23 -7
- data/lib/webmock/http_lib_adapters/excon_adapter.rb +4 -4
- data/lib/webmock/http_lib_adapters/http_gem/response.rb +3 -2
- data/lib/webmock/http_lib_adapters/http_gem/webmock.rb +1 -1
- data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +4 -4
- data/lib/webmock/matchers/hash_including_matcher.rb +1 -1
- data/lib/webmock/request_pattern.rb +11 -10
- data/lib/webmock/request_signature.rb +1 -1
- data/lib/webmock/request_stub.rb +1 -1
- data/lib/webmock/util/hash_keys_stringifier.rb +5 -3
- data/lib/webmock/util/query_mapper.rb +226 -146
- data/lib/webmock/util/uri.rb +4 -3
- data/lib/webmock/version.rb +1 -1
- data/lib/webmock/webmock.rb +12 -0
- data/spec/acceptance/em_http_request/em_http_request_spec.rb +73 -0
- data/spec/acceptance/excon/excon_spec.rb +15 -5
- data/spec/acceptance/http_gem/http_gem_spec.rb +9 -0
- data/spec/acceptance/httpclient/httpclient_spec.rb +11 -3
- data/spec/acceptance/httpclient/httpclient_spec_helper.rb +1 -1
- data/spec/acceptance/patron/patron_spec_helper.rb +1 -0
- data/spec/acceptance/shared/request_expectations.rb +18 -0
- data/spec/acceptance/shared/stubbing_requests.rb +5 -0
- data/spec/unit/errors_spec.rb +53 -15
- data/spec/unit/request_pattern_spec.rb +15 -0
- data/spec/unit/request_signature_spec.rb +3 -0
- data/spec/unit/util/hash_keys_stringifier_spec.rb +1 -1
- data/spec/unit/util/query_mapper_spec.rb +91 -17
- data/spec/unit/util/uri_spec.rb +29 -0
- data/webmock.gemspec +2 -2
- metadata +7 -15
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,51 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 1.19.0
|
4
|
+
|
5
|
+
* Fixed issue with Excon adapter giving warning message when redirects middleware was enabled.
|
6
|
+
|
7
|
+
Thanks to [Theo Hultberg](https://github.com/iconara) for reporting that.
|
8
|
+
|
9
|
+
* Fixed issue with `undefined method 'valid_request_keys' for Excon::Utils:Module`
|
10
|
+
|
11
|
+
Thanks to [Pablo Jairala](https://github.com/davidjairala)
|
12
|
+
|
13
|
+
* Fixed query mapper to encode `'one' => ['1','2']` as `'one[]=1&one[]=2'`.
|
14
|
+
|
15
|
+
Thanks to [Insoo Buzz Jung](https://github.com/insoul)
|
16
|
+
|
17
|
+
* Improved cookies support for em-http-request
|
18
|
+
|
19
|
+
Thanks to [Carlos Alonso Pérez](https://github.com/calonso)
|
20
|
+
|
21
|
+
* Fix HTTP Gem adapter to ensure uri attribute is set on response object.
|
22
|
+
|
23
|
+
Thanks to [Aleksey V. Zapparov](https://github.com/ixti)
|
24
|
+
|
25
|
+
* Fixed HTTPClient adapter. The response header now receives `request_method`, `request_uri`, and `request_query` transferred from request header
|
26
|
+
|
27
|
+
Thanks to [trlorenz](https://github.com/trlorenz)
|
28
|
+
|
29
|
+
* Query mapper supports nested data structures i.e. `{"first" => [{"two" => [{"three" => "four"}, "five"]}]}`
|
30
|
+
|
31
|
+
Thanks to [Alexander Simonov](https://github.com/simonoff)
|
32
|
+
|
33
|
+
* Fixed compatibility with latest versions of Excon which don't define `VALID_REQUEST_KEYS` anymore.
|
34
|
+
|
35
|
+
Thanks to [Pablo Jairala](https://github.com/davidjairala)
|
36
|
+
|
37
|
+
* Request method is always a symbol is request signatures. This fixes the issue of WebMock not matching Typhoeus requests with request method defined as string.
|
38
|
+
|
39
|
+
Thanks to [Thorbjørn Hermanse](https://github.com/thhermansen)
|
40
|
+
|
41
|
+
* Stubbing instructions which are displayed when no matching stub is found, can be disabled with `Config.instance.show_stubbing_instructions = false`
|
42
|
+
|
43
|
+
Thanks to [Mark Lorenz](https://github.com/dapplebeforedawn)
|
44
|
+
|
45
|
+
* Notation used for mapping query strings to data structure can be configured i.e. `WebMock::Config.instance.query_values_notation = :subscript`. This allows setting `:flat_array` notation which supports duplicated parameter names in query string.
|
46
|
+
|
47
|
+
Thanks to [tjsousa](https://github.com/tjsousa)
|
48
|
+
|
3
49
|
## 1.18.0
|
4
50
|
|
5
51
|
* Updated dependency on Addressable to versions >= 2.3.6
|
data/README.md
CHANGED
@@ -320,7 +320,7 @@ RestClient.post('www.example.net', 'abc') # ===> "abc\n"
|
|
320
320
|
`curl -is www.example.com > /tmp/www.example.com.txt`
|
321
321
|
```ruby
|
322
322
|
stub_request(:get, "www.example.com").
|
323
|
-
to_return(lambda { |request| File.new("/tmp/#{request.uri.host.to_s}.txt" })
|
323
|
+
to_return(lambda { |request| File.new("/tmp/#{request.uri.host.to_s}.txt") })
|
324
324
|
```
|
325
325
|
|
326
326
|
### Responses with dynamically evaluated parts
|
@@ -910,6 +910,16 @@ People who submitted patches and new features or suggested improvements. Many th
|
|
910
910
|
* Oleg Gritsenko
|
911
911
|
* Hwan-Joon Choi
|
912
912
|
* SHIBATA Hiroshi
|
913
|
+
* Caleb Thompson
|
914
|
+
* Theo Hultberg
|
915
|
+
* Pablo Jairala
|
916
|
+
* Insoo Buzz Jung
|
917
|
+
* Carlos Alonso Pérez
|
918
|
+
* trlorenz
|
919
|
+
* Alexander Simonov
|
920
|
+
* Thorbjørn Hermanse
|
921
|
+
* Mark Lorenz
|
922
|
+
* tjsousa
|
913
923
|
|
914
924
|
For a full list of contributors you can visit the
|
915
925
|
[contributors](https://github.com/bblimke/webmock/contributors) page.
|
data/lib/webmock/config.rb
CHANGED
@@ -2,9 +2,15 @@ module WebMock
|
|
2
2
|
class Config
|
3
3
|
include Singleton
|
4
4
|
|
5
|
+
def initialize
|
6
|
+
@show_stubbing_instructions = true
|
7
|
+
end
|
8
|
+
|
5
9
|
attr_accessor :allow_net_connect
|
6
10
|
attr_accessor :allow_localhost
|
7
11
|
attr_accessor :allow
|
8
12
|
attr_accessor :net_http_connect_on_start
|
13
|
+
attr_accessor :show_stubbing_instructions
|
14
|
+
attr_accessor :query_values_notation
|
9
15
|
end
|
10
16
|
end
|
data/lib/webmock/errors.rb
CHANGED
@@ -2,19 +2,20 @@ module WebMock
|
|
2
2
|
|
3
3
|
class NetConnectNotAllowedError < Exception
|
4
4
|
def initialize(request_signature)
|
5
|
-
text =
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
text = [
|
6
|
+
"Real HTTP connections are disabled. Unregistered request: #{request_signature}",
|
7
|
+
stubbing_instructions(request_signature),
|
8
|
+
request_stubs,
|
9
|
+
"="*60
|
10
|
+
].compact.join("\n\n")
|
10
11
|
super(text)
|
11
12
|
end
|
12
13
|
|
13
14
|
private
|
14
15
|
|
15
16
|
def request_stubs
|
16
|
-
return
|
17
|
-
text = "
|
17
|
+
return if WebMock::StubRegistry.instance.request_stubs.empty?
|
18
|
+
text = "registered request stubs:\n"
|
18
19
|
WebMock::StubRegistry.instance.request_stubs.each do |stub|
|
19
20
|
text << "\n#{WebMock::StubRequestSnippet.new(stub).to_s(false)}"
|
20
21
|
end
|
@@ -22,6 +23,7 @@ module WebMock
|
|
22
23
|
end
|
23
24
|
|
24
25
|
def stubbing_instructions(request_signature)
|
26
|
+
return unless WebMock.show_stubbing_instructions?
|
25
27
|
text = ""
|
26
28
|
request_stub = RequestStub.from_request_signature(request_signature)
|
27
29
|
text << "You can stub this request with the following snippet:\n\n"
|
@@ -133,6 +133,16 @@ if defined?(EventMachine::HttpClient)
|
|
133
133
|
@stubbed_webmock_response
|
134
134
|
end
|
135
135
|
|
136
|
+
def get_response_cookie(name)
|
137
|
+
name = name.to_s
|
138
|
+
|
139
|
+
raw_cookie = response_header.cookie
|
140
|
+
raw_cookie = [raw_cookie] if raw_cookie.is_a? String
|
141
|
+
|
142
|
+
cookie = raw_cookie.select { |c| c.start_with? name }.first
|
143
|
+
cookie and cookie.split('=', 2)[1]
|
144
|
+
end
|
145
|
+
|
136
146
|
private
|
137
147
|
|
138
148
|
def build_webmock_response
|
@@ -189,15 +199,21 @@ if defined?(EventMachine::HttpClient)
|
|
189
199
|
|
190
200
|
headers["Content-Length"] = body.bytesize unless headers["Content-Length"]
|
191
201
|
headers.each do |header, value|
|
192
|
-
|
202
|
+
if header =~ /set-cookie/i
|
203
|
+
[value].flatten.each do |cookie|
|
204
|
+
response_string << "#{header}: #{cookie}"
|
205
|
+
end
|
206
|
+
else
|
207
|
+
value = value.join(", ") if value.is_a?(Array)
|
193
208
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
209
|
+
# WebMock's internal processing will not handle the body
|
210
|
+
# correctly if the header indicates that it is chunked, unless
|
211
|
+
# we also create all the chunks.
|
212
|
+
# It's far easier just to remove the header.
|
213
|
+
next if header =~ /transfer-encoding/i && value =~/chunked/i
|
199
214
|
|
200
|
-
|
215
|
+
response_string << "#{header}: #{value}"
|
216
|
+
end
|
201
217
|
end if headers
|
202
218
|
|
203
219
|
response_string << "" << body
|
@@ -79,9 +79,8 @@ if defined?(Excon)
|
|
79
79
|
|
80
80
|
def self.request_params_from(hash)
|
81
81
|
hash = hash.dup
|
82
|
-
if Excon::
|
83
|
-
|
84
|
-
hash.reject! {|key,_| !request_keys.include?(key) }
|
82
|
+
if defined?(Excon::VALID_REQUEST_KEYS)
|
83
|
+
hash.reject! {|key,_| !Excon::VALID_REQUEST_KEYS.include?(key) }
|
85
84
|
end
|
86
85
|
PARAMS_TO_DELETE.each { |key| hash.delete(key) }
|
87
86
|
hash
|
@@ -146,7 +145,8 @@ if defined?(Excon)
|
|
146
145
|
|
147
146
|
Excon::Connection.class_eval do
|
148
147
|
def self.new(args)
|
149
|
-
|
148
|
+
args.delete(:__construction_args)
|
149
|
+
super(args).tap do |instance|
|
150
150
|
instance.data[:__construction_args] = args
|
151
151
|
end
|
152
152
|
end
|
@@ -10,12 +10,13 @@ module HTTP
|
|
10
10
|
webmock_response
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.from_webmock(webmock_response)
|
13
|
+
def self.from_webmock(webmock_response, request_signature = nil)
|
14
14
|
status = webmock_response.status.first
|
15
15
|
headers = webmock_response.headers || {}
|
16
16
|
body = Body.new Streamer.new webmock_response.body
|
17
|
+
uri = URI request_signature.uri.to_s if request_signature
|
17
18
|
|
18
|
-
new(status, "1.1", headers, body)
|
19
|
+
new(status, "1.1", headers, body, uri)
|
19
20
|
end
|
20
21
|
end
|
21
22
|
end
|
@@ -46,7 +46,7 @@ if defined?(::HTTPClient)
|
|
46
46
|
|
47
47
|
if webmock_responses[request_signature]
|
48
48
|
webmock_response = webmock_responses.delete(request_signature)
|
49
|
-
response = build_httpclient_response(webmock_response, stream, &block)
|
49
|
+
response = build_httpclient_response(webmock_response, stream, req.header, &block)
|
50
50
|
@request_filter.each do |filter|
|
51
51
|
filter.filter_response(req, response)
|
52
52
|
end
|
@@ -89,9 +89,9 @@ if defined?(::HTTPClient)
|
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
-
def build_httpclient_response(webmock_response, stream = false, &block)
|
92
|
+
def build_httpclient_response(webmock_response, stream = false, req_header = nil, &block)
|
93
93
|
body = stream ? StringIO.new(webmock_response.body) : webmock_response.body
|
94
|
-
response = HTTP::Message.new_response(body)
|
94
|
+
response = HTTP::Message.new_response(body, req_header)
|
95
95
|
response.header.init_response(webmock_response.status[0])
|
96
96
|
response.reason=webmock_response.status[1]
|
97
97
|
webmock_response.headers.to_a.each { |name, value| response.header.set(name, value) }
|
@@ -132,7 +132,7 @@ if defined?(::HTTPClient)
|
|
132
132
|
|
133
133
|
def build_request_signature(req, reuse_existing = false)
|
134
134
|
uri = WebMock::Util::URI.heuristic_parse(req.header.request_uri.to_s)
|
135
|
-
uri.query = WebMock::Util::QueryMapper.values_to_query(req.header.request_query) if req.header.request_query
|
135
|
+
uri.query = WebMock::Util::QueryMapper.values_to_query(req.header.request_query, :notation => WebMock::Config.instance.query_values_notation) if req.header.request_query
|
136
136
|
uri.port = req.header.request_uri.port
|
137
137
|
uri = uri.omit(:userinfo)
|
138
138
|
|
@@ -4,7 +4,7 @@ module WebMock
|
|
4
4
|
#https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/argument_matchers.rb
|
5
5
|
class HashIncludingMatcher
|
6
6
|
def initialize(expected)
|
7
|
-
@expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected).sort]
|
7
|
+
@expected = Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(expected, :deep => true).sort]
|
8
8
|
end
|
9
9
|
|
10
10
|
def ==(actual)
|
@@ -48,9 +48,10 @@ module WebMock
|
|
48
48
|
|
49
49
|
|
50
50
|
def assign_options(options)
|
51
|
-
|
52
|
-
@
|
53
|
-
@
|
51
|
+
options = WebMock::Util::HashKeysStringifier.stringify_keys!(options, :deep => true)
|
52
|
+
@body_pattern = BodyPattern.new(options['body']) if options.has_key?('body')
|
53
|
+
@headers_pattern = HeadersPattern.new(options['headers']) if options.has_key?('headers')
|
54
|
+
@uri_pattern.add_query_params(options['query']) if options.has_key?('query')
|
54
55
|
end
|
55
56
|
|
56
57
|
def create_uri_pattern(uri)
|
@@ -102,7 +103,7 @@ module WebMock
|
|
102
103
|
elsif rSpecHashIncludingMatcher?(query_params)
|
103
104
|
WebMock::Matchers::HashIncludingMatcher.from_rspec_matcher(query_params)
|
104
105
|
else
|
105
|
-
WebMock::Util::QueryMapper.query_to_values(query_params)
|
106
|
+
WebMock::Util::QueryMapper.query_to_values(query_params, :notation => Config.instance.query_values_notation)
|
106
107
|
end
|
107
108
|
end
|
108
109
|
|
@@ -116,7 +117,7 @@ module WebMock
|
|
116
117
|
class URIRegexpPattern < URIPattern
|
117
118
|
def matches?(uri)
|
118
119
|
WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) } &&
|
119
|
-
(@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query))
|
120
|
+
(@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, :notation => Config.instance.query_values_notation))
|
120
121
|
end
|
121
122
|
|
122
123
|
def to_s
|
@@ -155,7 +156,7 @@ module WebMock
|
|
155
156
|
if @pattern.is_a?(Addressable::URI)
|
156
157
|
if @query_params
|
157
158
|
uri.omit(:query) === @pattern &&
|
158
|
-
(@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query))
|
159
|
+
(@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, :notation => Config.instance.query_values_notation))
|
159
160
|
else
|
160
161
|
uri === @pattern
|
161
162
|
end
|
@@ -167,8 +168,8 @@ module WebMock
|
|
167
168
|
def add_query_params(query_params)
|
168
169
|
super
|
169
170
|
if @query_params.is_a?(Hash) || @query_params.is_a?(String)
|
170
|
-
query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query) || {}).merge(@query_params)
|
171
|
-
@pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash)
|
171
|
+
query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, :notation => Config.instance.query_values_notation) || {}).merge(@query_params)
|
172
|
+
@pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, :notation => WebMock::Config.instance.query_values_notation)
|
172
173
|
@query_params = nil
|
173
174
|
end
|
174
175
|
end
|
@@ -232,7 +233,7 @@ module WebMock
|
|
232
233
|
when :xml then
|
233
234
|
Crack::XML.parse(body)
|
234
235
|
else
|
235
|
-
WebMock::Util::QueryMapper.query_to_values(body)
|
236
|
+
WebMock::Util::QueryMapper.query_to_values(body, :notation => Config.instance.query_values_notation)
|
236
237
|
end
|
237
238
|
end
|
238
239
|
|
@@ -279,7 +280,7 @@ module WebMock
|
|
279
280
|
end
|
280
281
|
|
281
282
|
def normalize_hash(hash)
|
282
|
-
Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash).sort]
|
283
|
+
Hash[WebMock::Util::HashKeysStringifier.stringify_keys!(hash, :deep => true).sort]
|
283
284
|
end
|
284
285
|
|
285
286
|
end
|
data/lib/webmock/request_stub.rb
CHANGED
@@ -81,7 +81,7 @@ module WebMock
|
|
81
81
|
|
82
82
|
if signature.body.to_s != ''
|
83
83
|
body = if signature.url_encoded?
|
84
|
-
WebMock::Util::QueryMapper.query_to_values(signature.body)
|
84
|
+
WebMock::Util::QueryMapper.query_to_values(signature.body, :notation => Config.instance.query_values_notation)
|
85
85
|
else
|
86
86
|
signature.body
|
87
87
|
end
|
@@ -2,15 +2,17 @@ module WebMock
|
|
2
2
|
module Util
|
3
3
|
class HashKeysStringifier
|
4
4
|
|
5
|
-
def self.stringify_keys!(arg)
|
5
|
+
def self.stringify_keys!(arg, options = {})
|
6
6
|
case arg
|
7
7
|
when Array
|
8
|
-
arg.map { |elem|
|
8
|
+
arg.map { |elem|
|
9
|
+
options[:deep] ? stringify_keys!(elem, options) : elem
|
10
|
+
}
|
9
11
|
when Hash
|
10
12
|
Hash[
|
11
13
|
*arg.map { |key, value|
|
12
14
|
k = key.is_a?(Symbol) ? key.to_s : key
|
13
|
-
v = stringify_keys!(value)
|
15
|
+
v = (options[:deep] ? stringify_keys!(value, options) : value)
|
14
16
|
[k,v]
|
15
17
|
}.inject([]) {|r,x| r + x}]
|
16
18
|
else
|
@@ -1,142 +1,227 @@
|
|
1
1
|
module WebMock::Util
|
2
2
|
class QueryMapper
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
query
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
3
|
+
class << self
|
4
|
+
#This class is based on Addressable::URI pre 2.3.0
|
5
|
+
|
6
|
+
##
|
7
|
+
# Converts the query component to a Hash value.
|
8
|
+
#
|
9
|
+
# @option [Symbol] notation
|
10
|
+
# May be one of <code>:flat</code>, <code>:dot</code>, or
|
11
|
+
# <code>:subscript</code>. The <code>:dot</code> notation is not
|
12
|
+
# supported for assignment. Default value is <code>:subscript</code>.
|
13
|
+
#
|
14
|
+
# @return [Hash, Array] The query string parsed as a Hash or Array object.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# WebMock::Util::QueryMapper.query_to_values("?one=1&two=2&three=3")
|
18
|
+
# #=> {"one" => "1", "two" => "2", "three" => "3"}
|
19
|
+
# WebMock::Util::QueryMapper("?one[two][three]=four").query_values
|
20
|
+
# #=> {"one" => {"two" => {"three" => "four"}}}
|
21
|
+
# WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
|
22
|
+
# :notation => :dot
|
23
|
+
# )
|
24
|
+
# #=> {"one" => {"two" => {"three" => "four"}}}
|
25
|
+
# WebMock::Util::QueryMapper.query_to_values("?one[two][three]=four",
|
26
|
+
# :notation => :flat
|
27
|
+
# )
|
28
|
+
# #=> {"one[two][three]" => "four"}
|
29
|
+
# WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
|
30
|
+
# :notation => :flat
|
31
|
+
# )
|
32
|
+
# #=> {"one.two.three" => "four"}
|
33
|
+
# WebMock::Util::QueryMapper(
|
34
|
+
# "?one[two][three][]=four&one[two][three][]=five"
|
35
|
+
# )
|
36
|
+
# #=> {"one" => {"two" => {"three" => ["four", "five"]}}}
|
37
|
+
# WebMock::Util::QueryMapper.query_to_values(
|
38
|
+
# "?one=two&one=three").query_values(:notation => :flat_array)
|
39
|
+
# #=> [['one', 'two'], ['one', 'three']]
|
40
|
+
def query_to_values(query, options={})
|
41
|
+
return nil if query.nil?
|
42
|
+
query.force_encoding('utf-8') if query.respond_to?(:force_encoding)
|
43
|
+
|
44
|
+
options[:notation] ||= :subscript
|
45
|
+
|
46
|
+
if ![:flat, :dot, :subscript, :flat_array].include?(options[:notation])
|
47
|
+
raise ArgumentError,
|
48
|
+
'Invalid notation. Must be one of: ' +
|
49
|
+
'[:flat, :dot, :subscript, :flat_array].'
|
50
|
+
end
|
51
|
+
|
52
|
+
empty_accumulator = :flat_array == options[:notation] ? [] : {}
|
53
|
+
|
54
|
+
query_array = collect_query_parts(query)
|
55
|
+
|
56
|
+
query_hash = collect_query_hash(query_array, empty_accumulator, options)
|
57
|
+
|
58
|
+
normalize_query_hash(query_hash, empty_accumulator, options)
|
47
59
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
60
|
+
|
61
|
+
def normalize_query_hash(query_hash, empty_accumulator, options)
|
62
|
+
query_hash.inject(empty_accumulator.dup) do |accumulator, (key, value)|
|
63
|
+
if options[:notation] == :flat_array
|
64
|
+
accumulator << [key, value]
|
65
|
+
else
|
66
|
+
accumulator[key] = value.kind_of?(Hash) ? dehash(value) : value
|
52
67
|
end
|
68
|
+
accumulator
|
53
69
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
hash
|
70
|
+
end
|
71
|
+
|
72
|
+
def collect_query_parts(query)
|
73
|
+
query_parts = query.split('&').map do |pair|
|
74
|
+
pair.split('=', 2) if pair && !pair.empty?
|
60
75
|
end
|
76
|
+
query_parts.compact
|
61
77
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
value = true if value.nil?
|
68
|
-
key = Addressable::URI.unencode_component(key)
|
69
|
-
key = key.dup.force_encoding(Encoding::ASCII_8BIT) if key.respond_to?(:force_encoding)
|
70
|
-
if value != true
|
71
|
-
value = Addressable::URI.unencode_component(value.gsub(/\+/, " "))
|
72
|
-
end
|
73
|
-
if options[:notation] == :flat
|
74
|
-
if accumulator[key]
|
75
|
-
raise ArgumentError, "Key was repeated: #{key.inspect}"
|
76
|
-
end
|
77
|
-
accumulator[key] = value
|
78
|
-
elsif options[:notation] == :flat_array
|
79
|
-
accumulator << [key, value]
|
80
|
-
else
|
81
|
-
if options[:notation] == :dot
|
82
|
-
array_value = false
|
83
|
-
subkeys = key.split(".")
|
84
|
-
elsif options[:notation] == :subscript
|
85
|
-
array_value = !!(key =~ /\[\]$/)
|
86
|
-
subkeys = key.split(/[\[\]]+/)
|
87
|
-
end
|
88
|
-
current_hash = accumulator
|
89
|
-
for i in 0...(subkeys.size - 1)
|
90
|
-
subkey = subkeys[i]
|
91
|
-
current_hash[subkey] = {} unless current_hash[subkey]
|
92
|
-
current_hash = current_hash[subkey]
|
93
|
-
end
|
94
|
-
if array_value
|
95
|
-
if current_hash[subkeys.last] && !current_hash[subkeys.last].is_a?(Array)
|
96
|
-
current_hash[subkeys.last] = [current_hash[subkeys.last]]
|
97
|
-
end
|
98
|
-
current_hash[subkeys.last] = [] unless current_hash[subkeys.last]
|
99
|
-
current_hash[subkeys.last] << value
|
78
|
+
|
79
|
+
def collect_query_hash(query_array, empty_accumulator, options)
|
80
|
+
query_array.compact.inject(empty_accumulator.dup) do |accumulator, (key, value)|
|
81
|
+
value = if value.nil?
|
82
|
+
true
|
100
83
|
else
|
101
|
-
|
84
|
+
::Addressable::URI.unencode_component(value.gsub(/\+/, ' '))
|
102
85
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
86
|
+
key = Addressable::URI.unencode_component(key)
|
87
|
+
key = key.dup.force_encoding(Encoding::ASCII_8BIT) if key.respond_to?(:force_encoding)
|
88
|
+
self.__send__("fill_accumulator_for_#{options[:notation]}", accumulator, key, value)
|
89
|
+
accumulator
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def fill_accumulator_for_flat(accumulator, key, value)
|
94
|
+
if accumulator[key]
|
95
|
+
raise ArgumentError, "Key was repeated: #{key.inspect}"
|
96
|
+
end
|
97
|
+
accumulator[key] = value
|
98
|
+
end
|
99
|
+
|
100
|
+
def fill_accumulator_for_flat_array(accumulator, key, value)
|
101
|
+
accumulator << [key, value]
|
102
|
+
end
|
103
|
+
|
104
|
+
def fill_accumulator_for_dot(accumulator, key, value)
|
105
|
+
array_value = false
|
106
|
+
subkeys = key.split(".")
|
107
|
+
current_hash = accumulator
|
108
|
+
subkeys[0..-2].each do |subkey|
|
109
|
+
current_hash[subkey] = {} unless current_hash[subkey]
|
110
|
+
current_hash = current_hash[subkey]
|
111
|
+
end
|
112
|
+
if array_value
|
113
|
+
if current_hash[subkeys.last] && !current_hash[subkeys.last].is_a?(Array)
|
114
|
+
current_hash[subkeys.last] = [current_hash[subkeys.last]]
|
115
|
+
end
|
116
|
+
current_hash[subkeys.last] = [] unless current_hash[subkeys.last]
|
117
|
+
current_hash[subkeys.last] << value
|
108
118
|
else
|
109
|
-
|
119
|
+
current_hash[subkeys.last] = value
|
110
120
|
end
|
111
|
-
accumulator
|
112
121
|
end
|
113
|
-
end
|
114
122
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
123
|
+
def fill_accumulator_for_subscript(accumulator, key, value)
|
124
|
+
current_node = accumulator
|
125
|
+
subkeys = key.split(/(?=\[\w)/)
|
126
|
+
subkeys[0..-2].each do |subkey|
|
127
|
+
node = subkey =~ /\[\]\z/ ? [] : {}
|
128
|
+
subkey = subkey.gsub(/[\[\]]/, '')
|
129
|
+
if current_node.is_a? Array
|
130
|
+
container = current_node.find { |n| n.is_a?(Hash) && n.has_key?(subkey) }
|
131
|
+
if container
|
132
|
+
current_node = container[subkey]
|
133
|
+
else
|
134
|
+
current_node << {subkey => node}
|
135
|
+
current_node = node
|
136
|
+
end
|
137
|
+
else
|
138
|
+
current_node[subkey] = node unless current_node[subkey]
|
139
|
+
current_node = current_node[subkey]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
last_key = subkeys.last
|
143
|
+
array_value = !!(last_key =~ /\[\]$/)
|
144
|
+
last_key = last_key.gsub(/[\[\]]/, '')
|
145
|
+
if current_node.is_a? Array
|
146
|
+
container = current_node.find { |n| n.is_a?(Hash) && n.has_key?(last_key) }
|
147
|
+
if container
|
148
|
+
if array_value
|
149
|
+
container[last_key] << value
|
150
|
+
else
|
151
|
+
container[last_key] = value
|
152
|
+
end
|
153
|
+
else
|
154
|
+
if array_value
|
155
|
+
current_node << {last_key => [value]}
|
156
|
+
else
|
157
|
+
current_node << {last_key => value}
|
158
|
+
end
|
159
|
+
end
|
160
|
+
else
|
161
|
+
if array_value
|
162
|
+
current_node[last_key] = [] unless current_node[last_key]
|
163
|
+
current_node[last_key] << value
|
164
|
+
else
|
165
|
+
current_node[last_key] = value
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Sets the query component for this URI from a Hash object.
|
172
|
+
# This method produces a query string using the :subscript notation.
|
173
|
+
# An empty Hash will result in a nil query.
|
174
|
+
#
|
175
|
+
# @param [Hash, #to_hash, Array] new_query_values The new query values.
|
176
|
+
def values_to_query(new_query_values, options = {})
|
177
|
+
options[:notation] ||= :subscript
|
178
|
+
return if new_query_values.nil?
|
179
|
+
|
180
|
+
unless new_query_values.is_a?(Array)
|
181
|
+
unless new_query_values.respond_to?(:to_hash)
|
182
|
+
raise TypeError,
|
183
|
+
"Can't convert #{new_query_values.class} into Hash."
|
184
|
+
end
|
185
|
+
new_query_values = new_query_values.to_hash
|
186
|
+
new_query_values = new_query_values.inject([]) do |object, (key, value)|
|
187
|
+
key = key.to_s if key.is_a?(::Symbol) || key.nil?
|
188
|
+
if value.is_a?(Array)
|
189
|
+
value.each { |v| object << [key.to_s + '[]', v] }
|
190
|
+
elsif value.is_a?(Hash)
|
191
|
+
value.each { |k, v| object << ["#{key.to_s}[#{k}]", v]}
|
192
|
+
else
|
193
|
+
object << [key.to_s, value]
|
194
|
+
end
|
195
|
+
object
|
196
|
+
end
|
197
|
+
# Useful default for OAuth and caching.
|
198
|
+
# Only to be used for non-Array inputs. Arrays should preserve order.
|
199
|
+
new_query_values.sort!
|
200
|
+
end
|
122
201
|
|
123
|
-
|
124
|
-
|
202
|
+
buffer = ''
|
203
|
+
new_query_values.each do |parent, value|
|
204
|
+
encoded_parent = ::Addressable::URI.encode_component(
|
205
|
+
parent.dup, ::Addressable::URI::CharacterClasses::UNRESERVED
|
206
|
+
)
|
207
|
+
buffer << "#{to_query(encoded_parent, value, options)}&"
|
208
|
+
end
|
209
|
+
buffer.chop
|
125
210
|
end
|
126
211
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
212
|
+
def dehash(hash)
|
213
|
+
hash.each do |(key, value)|
|
214
|
+
if value.is_a?(::Hash)
|
215
|
+
hash[key] = self.dehash(value)
|
216
|
+
end
|
131
217
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
218
|
+
if hash != {} && hash.keys.all? { |key| key =~ /^\d+$/ }
|
219
|
+
hash.sort.inject([]) do |accu, (_, value)|
|
220
|
+
accu << value; accu
|
221
|
+
end
|
222
|
+
else
|
223
|
+
hash
|
136
224
|
end
|
137
|
-
# Useful default for OAuth and caching.
|
138
|
-
# Only to be used for non-Array inputs. Arrays should preserve order.
|
139
|
-
new_query_values.sort!
|
140
225
|
end
|
141
226
|
|
142
227
|
##
|
@@ -148,47 +233,42 @@ module WebMock::Util
|
|
148
233
|
# @param [Array, Hash, Symbol, #to_str] value
|
149
234
|
#
|
150
235
|
# @return [String] a properly escaped and ordered URL query.
|
151
|
-
|
152
|
-
|
236
|
+
|
237
|
+
# new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
|
238
|
+
def to_query(parent, value, options = {})
|
239
|
+
options[:notation] ||= :subscript
|
240
|
+
case value
|
241
|
+
when ::Hash
|
153
242
|
value = value.map do |key, val|
|
154
243
|
[
|
155
|
-
Addressable::URI.encode_component(key.dup, Addressable::URI::CharacterClasses::UNRESERVED),
|
244
|
+
::Addressable::URI.encode_component(key.dup, ::Addressable::URI::CharacterClasses::UNRESERVED),
|
156
245
|
val
|
157
246
|
]
|
158
247
|
end
|
159
248
|
value.sort!
|
160
|
-
buffer =
|
249
|
+
buffer = ''
|
161
250
|
value.each do |key, val|
|
162
|
-
new_parent = "#{parent}[#{key}]"
|
163
|
-
buffer << "#{to_query
|
251
|
+
new_parent = options[:notation] != :flat_array ? "#{parent}[#{key}]" : parent
|
252
|
+
buffer << "#{to_query(new_parent, val, options)}&"
|
164
253
|
end
|
165
|
-
|
166
|
-
|
167
|
-
buffer =
|
254
|
+
buffer.chop
|
255
|
+
when ::Array
|
256
|
+
buffer = ''
|
168
257
|
value.each_with_index do |val, i|
|
169
|
-
new_parent = "#{parent}[#{i}]"
|
170
|
-
buffer << "#{to_query
|
258
|
+
new_parent = options[:notation] != :flat_array ? "#{parent}[#{i}]" : parent
|
259
|
+
buffer << "#{to_query(new_parent, val, options)}&"
|
171
260
|
end
|
172
|
-
|
173
|
-
|
174
|
-
|
261
|
+
buffer.chop
|
262
|
+
when TrueClass
|
263
|
+
parent
|
175
264
|
else
|
176
265
|
encoded_value = Addressable::URI.encode_component(
|
177
266
|
value.to_s.dup, Addressable::URI::CharacterClasses::UNRESERVED
|
178
267
|
)
|
179
|
-
|
268
|
+
"#{parent}=#{encoded_value}"
|
180
269
|
end
|
181
270
|
end
|
182
|
-
|
183
|
-
# new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
|
184
|
-
buffer = ""
|
185
|
-
new_query_values.each do |parent, value|
|
186
|
-
encoded_parent = Addressable::URI.encode_component(
|
187
|
-
parent.dup, Addressable::URI::CharacterClasses::UNRESERVED
|
188
|
-
)
|
189
|
-
buffer << "#{to_query.call(encoded_parent, value)}&"
|
190
|
-
end
|
191
|
-
return buffer.chop
|
192
271
|
end
|
272
|
+
|
193
273
|
end
|
194
274
|
end
|