webmock 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|