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.
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