webmock 1.18.0 → 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +4 -1
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +11 -1
  4. data/lib/webmock/config.rb +6 -0
  5. data/lib/webmock/errors.rb +9 -7
  6. data/lib/webmock/http_lib_adapters/em_http_request/em_http_request_1_x.rb +23 -7
  7. data/lib/webmock/http_lib_adapters/excon_adapter.rb +4 -4
  8. data/lib/webmock/http_lib_adapters/http_gem/response.rb +3 -2
  9. data/lib/webmock/http_lib_adapters/http_gem/webmock.rb +1 -1
  10. data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +4 -4
  11. data/lib/webmock/matchers/hash_including_matcher.rb +1 -1
  12. data/lib/webmock/request_pattern.rb +11 -10
  13. data/lib/webmock/request_signature.rb +1 -1
  14. data/lib/webmock/request_stub.rb +1 -1
  15. data/lib/webmock/util/hash_keys_stringifier.rb +5 -3
  16. data/lib/webmock/util/query_mapper.rb +226 -146
  17. data/lib/webmock/util/uri.rb +4 -3
  18. data/lib/webmock/version.rb +1 -1
  19. data/lib/webmock/webmock.rb +12 -0
  20. data/spec/acceptance/em_http_request/em_http_request_spec.rb +73 -0
  21. data/spec/acceptance/excon/excon_spec.rb +15 -5
  22. data/spec/acceptance/http_gem/http_gem_spec.rb +9 -0
  23. data/spec/acceptance/httpclient/httpclient_spec.rb +11 -3
  24. data/spec/acceptance/httpclient/httpclient_spec_helper.rb +1 -1
  25. data/spec/acceptance/patron/patron_spec_helper.rb +1 -0
  26. data/spec/acceptance/shared/request_expectations.rb +18 -0
  27. data/spec/acceptance/shared/stubbing_requests.rb +5 -0
  28. data/spec/unit/errors_spec.rb +53 -15
  29. data/spec/unit/request_pattern_spec.rb +15 -0
  30. data/spec/unit/request_signature_spec.rb +3 -0
  31. data/spec/unit/util/hash_keys_stringifier_spec.rb +1 -1
  32. data/spec/unit/util/query_mapper_spec.rb +91 -17
  33. data/spec/unit/util/uri_spec.rb +29 -0
  34. data/webmock.gemspec +2 -2
  35. metadata +7 -15
data/.gitignore CHANGED
@@ -13,6 +13,9 @@ tmtags
13
13
  ## VIM
14
14
  .*.sw[a-z]
15
15
 
16
+ ## RubyMine and related
17
+ .idea
18
+
16
19
  ## PROJECT::GENERAL
17
20
  coverage
18
21
  rdoc
@@ -27,4 +30,4 @@ tmp/*
27
30
  *.rbc
28
31
  *.rbx
29
32
  .ruby-gemset
30
- .ruby-version
33
+ .ruby-version
@@ -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.
@@ -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
@@ -2,19 +2,20 @@ module WebMock
2
2
 
3
3
  class NetConnectNotAllowedError < Exception
4
4
  def initialize(request_signature)
5
- text = "Real HTTP connections are disabled. Unregistered request: #{request_signature}"
6
- text << "\n\n"
7
- text << stubbing_instructions(request_signature)
8
- text << request_stubs
9
- text << "\n\n" + "="*60
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 "" if WebMock::StubRegistry.instance.request_stubs.empty?
17
- text = "\n\nregistered request stubs:\n"
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
- value = value.join(", ") if value.is_a?(Array)
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
- # WebMock's internal processing will not handle the body
195
- # correctly if the header indicates that it is chunked, unless
196
- # we also create all the chunks.
197
- # It's far easier just to remove the header.
198
- next if header =~ /transfer-encoding/i && value =~/chunked/i
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
- response_string << "#{header}: #{value}"
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::VERSION >= '0.27.5'
83
- request_keys = Excon::Utils.valid_request_keys(hash)
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
- super.tap do |instance|
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
@@ -37,7 +37,7 @@ module HTTP
37
37
  webmock_response.raise_error_if_any
38
38
 
39
39
  invoke_callbacks(webmock_response, :real_request => false)
40
- ::HTTP::Response.from_webmock webmock_response
40
+ ::HTTP::Response.from_webmock webmock_response, request_signature
41
41
  end
42
42
 
43
43
  def perform
@@ -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
- @body_pattern = BodyPattern.new(options[:body]) if options.has_key?(:body)
52
- @headers_pattern = HeadersPattern.new(options[:headers]) if options.has_key?(:headers)
53
- @uri_pattern.add_query_params(options[:query]) if options.has_key?(:query)
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
@@ -6,7 +6,7 @@ module WebMock
6
6
  attr_reader :headers
7
7
 
8
8
  def initialize(method, uri, options = {})
9
- self.method = method
9
+ self.method = method.to_sym
10
10
  self.uri = uri.is_a?(Addressable::URI) ? uri : WebMock::Util::URI.normalize_uri(uri)
11
11
  assign_options(options)
12
12
  end
@@ -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| stringify_keys!(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
- #This class is based on Addressable::URI pre 2.3.0
4
-
5
- ##
6
- # Converts the query component to a Hash value.
7
- #
8
- # @option [Symbol] notation
9
- # May be one of <code>:flat</code>, <code>:dot</code>, or
10
- # <code>:subscript</code>. The <code>:dot</code> notation is not
11
- # supported for assignment. Default value is <code>:subscript</code>.
12
- #
13
- # @return [Hash, Array] The query string parsed as a Hash or Array object.
14
- #
15
- # @example
16
- # WebMock::Util::QueryMapper.query_to_values("?one=1&two=2&three=3")
17
- # #=> {"one" => "1", "two" => "2", "three" => "3"}
18
- # WebMock::Util::QueryMapper("?one[two][three]=four").query_values
19
- # #=> {"one" => {"two" => {"three" => "four"}}}
20
- # WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
21
- # :notation => :dot
22
- # )
23
- # #=> {"one" => {"two" => {"three" => "four"}}}
24
- # WebMock::Util::QueryMapper.query_to_values("?one[two][three]=four",
25
- # :notation => :flat
26
- # )
27
- # #=> {"one[two][three]" => "four"}
28
- # WebMock::Util::QueryMapper.query_to_values("?one.two.three=four",
29
- # :notation => :flat
30
- # )
31
- # #=> {"one.two.three" => "four"}
32
- # WebMock::Util::QueryMapper(
33
- # "?one[two][three][]=four&one[two][three][]=five"
34
- # )
35
- # #=> {"one" => {"two" => {"three" => ["four", "five"]}}}
36
- # WebMock::Util::QueryMapper.query_to_values(
37
- # "?one=two&one=three").query_values(:notation => :flat_array)
38
- # #=> [['one', 'two'], ['one', 'three']]
39
- def self.query_to_values(query, options={})
40
- query.force_encoding('utf-8') if query.respond_to?(:force_encoding)
41
- defaults = {:notation => :subscript}
42
- options = defaults.merge(options)
43
- if ![:flat, :dot, :subscript, :flat_array].include?(options[:notation])
44
- raise ArgumentError,
45
- "Invalid notation. Must be one of: " +
46
- "[:flat, :dot, :subscript, :flat_array]."
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
- dehash = lambda do |hash|
49
- hash.each do |(key, value)|
50
- if value.kind_of?(Hash)
51
- hash[key] = dehash.call(value)
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
- if hash != {} && hash.keys.all? { |key| key =~ /^\d+$/ }
55
- hash.sort.inject([]) do |accu, (_, value)|
56
- accu << value; accu
57
- end
58
- else
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
- return nil if query == nil
63
- empty_accumulator = :flat_array == options[:notation] ? [] : {}
64
- return ((query.split("&").map do |pair|
65
- pair.split("=", 2) if pair && !pair.empty?
66
- end).compact.inject(empty_accumulator.dup) do |accumulator, (key, value)|
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
- current_hash[subkeys.last] = value
84
+ ::Addressable::URI.unencode_component(value.gsub(/\+/, ' '))
102
85
  end
103
- end
104
- accumulator
105
- end).inject(empty_accumulator.dup) do |accumulator, (key, value)|
106
- if options[:notation] == :flat_array
107
- accumulator << [key, value]
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
- accumulator[key] = value.kind_of?(Hash) ? dehash.call(value) : value
119
+ current_hash[subkeys.last] = value
110
120
  end
111
- accumulator
112
121
  end
113
- end
114
122
 
115
- ##
116
- # Sets the query component for this URI from a Hash object.
117
- # This method produces a query string using the :subscript notation.
118
- # An empty Hash will result in a nil query.
119
- #
120
- # @param [Hash, #to_hash, Array] new_query_values The new query values.
121
- def self.values_to_query(new_query_values)
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
- if new_query_values == nil
124
- return nil
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
- if !new_query_values.is_a?(Array)
128
- if !new_query_values.respond_to?(:to_hash)
129
- raise TypeError,
130
- "Can't convert #{new_query_values.class} into Hash."
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
- new_query_values = new_query_values.to_hash
133
- new_query_values = new_query_values.map do |key, value|
134
- key = key.to_s if key.kind_of?(Symbol) || key.nil?
135
- [key.to_s, value]
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
- to_query = lambda do |parent, value|
152
- if value.is_a?(Hash)
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.call(new_parent, val)}&"
251
+ new_parent = options[:notation] != :flat_array ? "#{parent}[#{key}]" : parent
252
+ buffer << "#{to_query(new_parent, val, options)}&"
164
253
  end
165
- return buffer.chop
166
- elsif value.is_a?(Array)
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.call(new_parent, val)}&"
258
+ new_parent = options[:notation] != :flat_array ? "#{parent}[#{i}]" : parent
259
+ buffer << "#{to_query(new_parent, val, options)}&"
171
260
  end
172
- return buffer.chop
173
- elsif value == true
174
- return parent
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
- return "#{parent}=#{encoded_value}"
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