excon 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of excon might be problematic. Click here for more details.

@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- excon (0.17.0)
4
+ excon (0.18.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
@@ -14,6 +14,7 @@ GEM
14
14
  delorean (2.0.0)
15
15
  chronic
16
16
  eventmachine (1.0.0)
17
+ eventmachine (1.0.0-java)
17
18
  formatador (0.2.3)
18
19
  i18n (0.6.0)
19
20
  jruby-openssl (0.7.7)
data/README.md CHANGED
@@ -100,6 +100,14 @@ You can make `Transfer-Encoding: chunked` requests by passing a block that will
100
100
 
101
101
  Iterating in this way allows you to have more granular control over writes and to write things where you can not calculate the overall length up front.
102
102
 
103
+ Pipelining Requests
104
+ ------------------
105
+
106
+ You can make use of HTTP pipelining to improve performance. Insead of the normal request/response cyle, pipelining sends a series of requests and then receives a series of responses. You can take advantage of this using the `requests` method, which takes an array of params where each is a hash like request would receive and returns an array of responses.
107
+
108
+ connection = Excon.new('http://geemus.com/')
109
+ connection.requests([{:method => :get, :method => :get}])
110
+
103
111
  Streaming Responses
104
112
  -------------------
105
113
 
@@ -1,3 +1,20 @@
1
+ 0.18.0 02/21/2013
2
+ =================
3
+
4
+ more refactoring around middlewares
5
+ add pipelining capabilities
6
+ allow [] style access to response attributes
7
+
8
+
9
+ 0.17.0 02/01/2013
10
+ =================
11
+
12
+ add patch method
13
+ allow passing family for addresses to params/proxy
14
+ more consistent empty header passing
15
+ nicer debug output
16
+ internal refactoring toward middleware pattern
17
+
1
18
  0.16.10 11/16/2012
2
19
  ==================
3
20
 
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
13
13
  ## If your rubyforge_project name is different, then edit it and comment out
14
14
  ## the sub! line in the Rakefile
15
15
  s.name = 'excon'
16
- s.version = '0.17.0'
17
- s.date = '2013-02-01'
16
+ s.version = '0.18.0'
17
+ s.date = '2013-02-21'
18
18
  s.rubyforge_project = 'excon'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
@@ -96,8 +96,10 @@ Gem::Specification.new do |s|
96
96
  lib/excon/connection.rb
97
97
  lib/excon/constants.rb
98
98
  lib/excon/errors.rb
99
+ lib/excon/middlewares/base.rb
99
100
  lib/excon/middlewares/expects.rb
100
101
  lib/excon/middlewares/instrumentor.rb
102
+ lib/excon/middlewares/mock.rb
101
103
  lib/excon/response.rb
102
104
  lib/excon/socket.rb
103
105
  lib/excon/ssl_socket.rb
@@ -109,6 +111,7 @@ Gem::Specification.new do |s|
109
111
  tests/header_tests.rb
110
112
  tests/idempotent_tests.rb
111
113
  tests/middlewares/instrumentation_tests.rb
114
+ tests/middlewares/mock_tests.rb
112
115
  tests/proxy_tests.rb
113
116
  tests/query_string_tests.rb
114
117
  tests/rackups/basic.rb
@@ -124,9 +127,9 @@ Gem::Specification.new do |s|
124
127
  tests/rackups/timeout.ru
125
128
  tests/request_headers_tests.rb
126
129
  tests/request_method_tests.rb
130
+ tests/requests_tests.rb
127
131
  tests/servers/bad.rb
128
132
  tests/servers/eof.rb
129
- tests/stub_tests.rb
130
133
  tests/test_helper.rb
131
134
  tests/thread_safety_tests.rb
132
135
  tests/timeout_tests.rb
@@ -21,6 +21,11 @@ module Excon
21
21
  :headers => {},
22
22
  :idempotent => false,
23
23
  :instrumentor_name => 'excon',
24
+ :middlewares => [
25
+ Excon::Middleware::Instrumentor,
26
+ Excon::Middleware::Expects,
27
+ Excon::Middleware::Mock
28
+ ],
24
29
  :mock => false,
25
30
  :nonblock => DEFAULT_NONBLOCK,
26
31
  :read_timeout => 60,
@@ -43,8 +48,10 @@ end
43
48
  require 'excon/constants'
44
49
  require 'excon/connection'
45
50
  require 'excon/errors'
51
+ require 'excon/middlewares/base'
46
52
  require 'excon/middlewares/expects'
47
53
  require 'excon/middlewares/instrumentor'
54
+ require 'excon/middlewares/mock'
48
55
  require 'excon/response'
49
56
  require 'excon/socket'
50
57
  require 'excon/ssl_socket'
@@ -111,7 +118,7 @@ module Excon
111
118
  request_params.update(
112
119
  :host => uri.host,
113
120
  :path => uri.path,
114
- :port => uri.port,
121
+ :port => uri.port.to_s,
115
122
  :query => uri.query,
116
123
  :scheme => uri.scheme
117
124
  )
@@ -1,6 +1,10 @@
1
1
  module Excon
2
2
  class Connection
3
-
3
+ VALID_CONNECTION_KEYS = [:body, :headers, :host, :host_port, :path, :port, :query, :scheme, :user, :password,
4
+ :instrumentor, :instrumentor_name, :ssl_ca_file, :ssl_verify_peer, :chunk_size,
5
+ :nonblock, :retry_limit, :connect_timeout, :read_timeout, :write_timeout, :captures,
6
+ :exception, :expects, :mock, :proxy, :method, :idempotent, :request_block, :response_block,
7
+ :middlewares, :retries_remaining, :connection, :stack, :response, :pipeline]
4
8
  attr_reader :data
5
9
 
6
10
  def params
@@ -21,6 +25,13 @@ module Excon
21
25
  @data[:proxy] = new_proxy
22
26
  end
23
27
 
28
+ def assert_valid_keys_for_argument!(argument, valid_keys)
29
+ invalid_keys = argument.keys - valid_keys
30
+ return true if invalid_keys.empty?
31
+ raise ArgumentError, "The following keys are invalid: #{invalid_keys.map(&:inspect).join(', ')}"
32
+ end
33
+ private :assert_valid_keys_for_argument!
34
+
24
35
  # Initializes a new Connection instance
25
36
  # @param [String] url The destination URL
26
37
  # @param [Hash<Symbol, >] params One or more optional params
@@ -36,14 +47,16 @@ module Excon
36
47
  # @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
37
48
  # @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
38
49
  def initialize(url, params = {})
50
+ assert_valid_keys_for_argument!(params, VALID_CONNECTION_KEYS)
39
51
  uri = URI.parse(url)
40
52
  @data = Excon.defaults.merge({
41
53
  :host => uri.host,
42
- :host_port => '' << uri.host << ':' << uri.port.to_s,
43
54
  :path => uri.path,
44
- :port => uri.port,
55
+ :port => uri.port.to_s,
45
56
  :query => uri.query,
46
57
  :scheme => uri.scheme,
58
+ :user => (URI.decode(uri.user) if uri.user),
59
+ :password => (URI.decode(uri.password) if uri.password),
47
60
  }).merge!(params)
48
61
  # merge does not deep-dup, so make sure headers is not the original
49
62
  @data[:headers] = @data[:headers].dup
@@ -74,20 +87,21 @@ module Excon
74
87
  @data[:headers]['Authorization'] ||= 'Basic ' << ['' << uri.user.to_s << ':' << uri.password.to_s].pack('m').delete(Excon::CR_NL)
75
88
  end
76
89
 
77
- @socket_key = '' << @data[:host_port]
90
+ @socket_key = '' << @data[:host] << ':' << @data[:port]
78
91
  reset
79
92
  end
80
93
 
81
- def call(datum)
94
+ def request_call(datum)
82
95
  begin
83
- response_datum = if datum[:mock]
84
- invoke_stub(datum)
96
+ if datum.has_key?(:response)
97
+ # we already have data from a middleware, so bail
98
+ return datum
85
99
  else
86
100
  socket.data = datum
87
101
  # start with "METHOD /path"
88
102
  request = datum[:method].to_s.upcase << ' '
89
103
  if @data[:proxy]
90
- request << datum[:scheme] << '://' << datum[:host_port]
104
+ request << datum[:scheme] << '://' << @data[:host] << ':' << @data[:port].to_s
91
105
  end
92
106
  request << datum[:path]
93
107
 
@@ -164,24 +178,21 @@ module Excon
164
178
  end
165
179
  end
166
180
  end
167
-
168
- # read the response
169
- response_datum = Excon::Response.parse(socket, datum)
170
-
171
- if response_datum[:headers]['Connection'] == 'close'
172
- reset
173
- end
174
-
175
- response_datum
176
181
  end
177
- rescue Excon::Errors::StubNotFound, Excon::Errors::Timeout => error
178
- raise(error)
179
- rescue => socket_error
180
- reset
181
- raise(Excon::Errors::SocketError.new(socket_error))
182
+ rescue => error
183
+ case error
184
+ when Excon::Errors::StubNotFound, Excon::Errors::Timeout
185
+ raise(error)
186
+ else
187
+ raise(Excon::Errors::SocketError.new(error))
188
+ end
182
189
  end
183
190
 
184
- response_datum
191
+ datum
192
+ end
193
+
194
+ def response_call(datum)
195
+ datum
185
196
  end
186
197
 
187
198
  # Sends the supplied request to the destination host.
@@ -197,9 +208,9 @@ module Excon
197
208
  def request(params, &block)
198
209
  # @data has defaults, merge in new params to override
199
210
  datum = @data.merge(params)
200
- datum[:host_port] = '' << datum[:host] << ':' << datum[:port].to_s
211
+ assert_valid_keys_for_argument!(params, VALID_CONNECTION_KEYS)
201
212
  datum[:headers] = @data[:headers].merge(datum[:headers] || {})
202
- datum[:headers]['Host'] = '' << datum[:host_port]
213
+ datum[:headers]['Host'] ||= '' << datum[:host] << ':' << datum[:port]
203
214
  datum[:retries_remaining] ||= datum[:retry_limit]
204
215
 
205
216
  # if path is empty or doesn't start with '/', insert one
@@ -212,37 +223,50 @@ module Excon
212
223
  datum[:response_block] = Proc.new
213
224
  end
214
225
 
215
- datum[:middlewares] = [
216
- lambda {|app| Excon::Middleware::Instrumentor.new(app) },
217
- lambda {|app| Excon::Middleware::Expects.new(app) }
218
- ]
219
- stack = datum[:middlewares].reverse.inject(self) do |middlewares, middleware|
226
+ datum[:connection] = self
227
+
228
+ datum[:stack] = datum[:middlewares].map do |middleware|
229
+ lambda {|stack| middleware.new(stack)}
230
+ end.reverse.inject(self) do |middlewares, middleware|
220
231
  middleware.call(middlewares)
221
232
  end
222
- response_datum = stack.call(datum)
233
+ datum = datum[:stack].request_call(datum)
234
+
235
+ unless datum[:pipeline]
236
+ datum = response(datum)
237
+
238
+ if datum[:response][:headers]['Connection'] == 'close'
239
+ reset
240
+ end
223
241
 
224
- Excon::Response.new(response_datum)
242
+ Excon::Response.new(datum[:response])
243
+ else
244
+ datum
245
+ end
225
246
  rescue => request_error
247
+ reset
226
248
  if datum[:idempotent] && [Excon::Errors::Timeout, Excon::Errors::SocketError,
227
- Excon::Errors::HTTPStatusError].any? {|ex| request_error.kind_of? ex }
249
+ Excon::Errors::HTTPStatusError].any? {|ex| request_error.kind_of? ex } && datum[:retries_remaining] > 1
228
250
  datum[:retries_remaining] -= 1
229
- if datum[:retries_remaining] > 0
230
- request(datum, &block)
231
- else
232
- if datum.has_key?(:instrumentor)
233
- datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.error", :error => request_error)
234
- end
235
- raise(request_error)
236
- end
251
+ request(datum, &block)
237
252
  else
238
253
  if datum.has_key?(:instrumentor)
239
254
  datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.error", :error => request_error)
240
255
  end
241
- reset
242
256
  raise(request_error)
243
257
  end
244
258
  end
245
259
 
260
+ # Sends the supplied requests to the destination host using pipelining.
261
+ # @pipeline_params [Array<Hash>] pipeline_params An array of one or more optional params, override defaults set in Connection.new, see #request for details
262
+ def requests(pipeline_params)
263
+ pipeline_params.map do |params|
264
+ request(params.merge!(:pipeline => true))
265
+ end.map do |datum|
266
+ Excon::Response.new(response(datum)[:response])
267
+ end
268
+ end
269
+
246
270
  def reset
247
271
  (old_socket = sockets.delete(@socket_key)) && old_socket.close
248
272
  end
@@ -304,67 +328,75 @@ module Excon
304
328
  end
305
329
  end
306
330
 
307
- def invoke_stub(datum)
308
- # convert File/Tempfile body to string before matching:
309
- unless datum[:body].nil? || datum[:body].is_a?(String)
310
- if datum[:body].respond_to?(:binmode)
311
- datum[:body].binmode
312
- end
313
- if datum[:body].respond_to?(:rewind)
314
- datum[:body].rewind
315
- end
316
- datum[:body] = datum[:body].read
317
- end
318
-
319
- datum[:captures] = {:headers => {}} # setup data to hold captures
320
- Excon.stubs.each do |stub, response|
321
- headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
322
- case value = stub[:headers][key]
323
- when Regexp
324
- if match = value.match(datum[:headers][key])
325
- datum[:captures][:headers][key] = match.captures
326
- end
327
- match
328
- else
329
- value == datum[:headers][key]
330
- end
331
- end
332
- non_headers_match = (stub.keys - [:headers]).all? do |key|
333
- case value = stub[key]
334
- when Regexp
335
- if match = value.match(datum[key])
336
- datum[:captures][key] = match.captures
337
- end
338
- match
339
- else
340
- value == datum[key]
331
+ def response(datum={})
332
+ unless datum.has_key?(:response)
333
+ datum[:response] = {
334
+ :body => '',
335
+ :headers => {},
336
+ :status => socket.read(12)[9, 11].to_i,
337
+ :remote_ip => socket.remote_ip
338
+ }
339
+ socket.readline # read the rest of the status line and CRLF
340
+
341
+ until ((data = socket.readline).chop!).empty?
342
+ key, value = data.split(/:\s*/, 2)
343
+ datum[:response][:headers][key] = ([*datum[:response][:headers][key]] << value).compact.join(', ')
344
+ if key.casecmp('Content-Length') == 0
345
+ content_length = value.to_i
346
+ elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0)
347
+ transfer_encoding_chunked = true
341
348
  end
342
349
  end
343
- if headers_match && non_headers_match
344
- response_datum = case response
345
- when Proc
346
- response.call(datum)
347
- else
348
- response
349
- end
350
350
 
351
- if datum[:expects] && ![*datum[:expects]].include?(response_datum[:status])
352
- # don't pass stuff into a block if there was an error
353
- elsif datum.has_key?(:response_block) && response_datum.has_key?(:body)
354
- body = response_datum.delete(:body)
355
- content_length = remaining = body.bytesize
356
- i = 0
357
- while i < body.length
358
- datum[:response_block].call(body[i, datum[:chunk_size]], [remaining - datum[:chunk_size], 0].max, content_length)
359
- remaining -= datum[:chunk_size]
360
- i += datum[:chunk_size]
351
+ unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(datum[:response][:status])
352
+
353
+ # check to see if expects was set and matched
354
+ expected_status = !datum.has_key?(:expects) || [*datum[:expects]].include?(datum[:response][:status])
355
+
356
+ # if expects matched and there is a block, use it
357
+ if expected_status && datum.has_key?(:response_block)
358
+ if transfer_encoding_chunked
359
+ # 2 == "/r/n".length
360
+ while (chunk_size = socket.readline.chop!.to_i(16)) > 0
361
+ datum[:response_block].call(socket.read(chunk_size + 2).chop!, nil, nil)
362
+ end
363
+ socket.read(2)
364
+ elsif remaining = content_length
365
+ while remaining > 0
366
+ datum[:response_block].call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
367
+ remaining -= datum[:chunk_size]
368
+ end
369
+ else
370
+ while remaining = socket.read(datum[:chunk_size])
371
+ datum[:response_block].call(remaining, remaining.length, content_length)
372
+ end
373
+ end
374
+ else # no block or unexpected status
375
+ if transfer_encoding_chunked
376
+ while (chunk_size = socket.readline.chop!.to_i(16)) > 0
377
+ datum[:response][:body] << socket.read(chunk_size + 2).chop! # 2 == "/r/n".length
378
+ end
379
+ socket.read(2) # 2 == "/r/n".length
380
+ elsif remaining = content_length
381
+ while remaining > 0
382
+ datum[:response][:body] << socket.read([datum[:chunk_size], remaining].min)
383
+ remaining -= datum[:chunk_size]
384
+ end
385
+ else
386
+ datum[:response][:body] << socket.read
361
387
  end
362
388
  end
363
- return response_datum
364
389
  end
365
390
  end
366
- # if we reach here no stubs matched
367
- raise(Excon::Errors::StubNotFound.new('no stubs matched ' << datum.inspect))
391
+
392
+ datum[:stack].response_call(datum)
393
+ rescue => error
394
+ case error
395
+ when Excon::Errors::HTTPStatusError, Excon::Errors::Timeout
396
+ raise(error)
397
+ else
398
+ raise(Excon::Errors::SocketError.new(error))
399
+ end
368
400
  end
369
401
 
370
402
  def socket
@@ -388,9 +420,8 @@ module Excon
388
420
  end
389
421
  {
390
422
  :host => uri.host,
391
- :host_port => '' << uri.host << ':' << uri.port.to_s,
392
423
  :password => uri.password,
393
- :port => uri.port,
424
+ :port => uri.port.to_s,
394
425
  :scheme => uri.scheme,
395
426
  :user => uri.user
396
427
  }
@@ -29,7 +29,7 @@ module Excon
29
29
 
30
30
  REDACTED = 'REDACTED'
31
31
 
32
- VERSION = '0.17.0'
32
+ VERSION = '0.18.0'
33
33
 
34
34
  unless ::IO.const_defined?(:WaitReadable)
35
35
  class ::IO
@@ -120,15 +120,17 @@ module Excon
120
120
  503 => [Excon::Errors::ServiceUnavailable, 'Service Unavailable'],
121
121
  504 => [Excon::Errors::GatewayTimeout, 'Gateway Timeout']
122
122
  }
123
- error, message = @errors[response.status] || [Excon::Errors::HTTPStatusError, 'Unknown']
123
+
124
+ error, message = @errors[response[:status]] || [Excon::Errors::HTTPStatusError, 'Unknown']
124
125
 
125
126
  # scrub authorization
127
+ request = request.dup
128
+ request.reject! {|key, value| [:connection, :stack].include?(key)}
126
129
  if request[:headers].has_key?('Authorization')
127
- request = request.dup
128
130
  request[:headers] = request[:headers].dup
129
131
  request[:headers]['Authorization'] = REDACTED
130
132
  end
131
- error.new("Expected(#{request[:expects].inspect}) <=> Actual(#{response.status} #{message})\n request => #{request.inspect}\n response => #{response.inspect}", request, response)
133
+ error.new("Expected(#{request[:expects].inspect}) <=> Actual(#{response[:status]} #{message})\n request => #{request.inspect}\n response => #{response.inspect}", request, response)
132
134
  end
133
135
 
134
136
  end
@@ -0,0 +1,19 @@
1
+ module Excon
2
+ module Middleware
3
+ class Base
4
+ def initialize(stack)
5
+ @stack = stack
6
+ end
7
+
8
+ def request_call(datum)
9
+ # do stuff
10
+ @stack.request_call(datum)
11
+ end
12
+
13
+ def response_call(datum)
14
+ @stack.response_call(datum)
15
+ # do stuff
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,17 +1,16 @@
1
1
  module Excon
2
2
  module Middleware
3
- class Expects
4
- def initialize(app)
5
- @app = app
6
- end
7
-
8
- def call(datum)
9
- response_datum = @app.call(datum)
10
-
11
- if datum.has_key?(:expects) && ![*datum[:expects]].include?(response_datum[:status])
12
- raise(Excon::Errors.status_error(datum, Excon::Response.new(response_datum)))
3
+ class Expects < Excon::Middleware::Base
4
+ def response_call(datum)
5
+ if datum.has_key?(:expects) && ![*datum[:expects]].include?(datum[:response][:status])
6
+ raise(
7
+ Excon::Errors.status_error(
8
+ datum.reject {|key,value| key == :response},
9
+ datum[:response]
10
+ )
11
+ )
13
12
  else
14
- response_datum
13
+ @stack.response_call(datum)
15
14
  end
16
15
  end
17
16
  end
@@ -1,25 +1,26 @@
1
1
  module Excon
2
2
  module Middleware
3
- class Instrumentor
4
- def initialize(app)
5
- @app = app
6
- end
7
-
8
- def call(datum)
3
+ class Instrumentor < Excon::Middleware::Base
4
+ def request_call(datum)
9
5
  if datum.has_key?(:instrumentor)
10
6
  if datum[:retries_remaining] < datum[:retry_limit]
11
7
  event_name = "#{datum[:instrumentor_name]}.retry"
12
8
  else
13
9
  event_name = "#{datum[:instrumentor_name]}.request"
14
10
  end
15
- response_datum = datum[:instrumentor].instrument(event_name, datum) do
16
- @app.call(datum)
11
+ datum[:instrumentor].instrument(event_name, datum) do
12
+ @stack.request_call(datum)
17
13
  end
18
- datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.response", response_datum)
19
- response_datum
20
14
  else
21
- @app.call(datum)
15
+ @stack.request_call(datum)
16
+ end
17
+ end
18
+
19
+ def response_call(datum)
20
+ if datum.has_key?(:instrumentor)
21
+ datum[:instrumentor].instrument("#{datum[:instrumentor_name]}.response", datum[:response])
22
22
  end
23
+ @stack.response_call(datum)
23
24
  end
24
25
  end
25
26
  end
@@ -0,0 +1,84 @@
1
+ module Excon
2
+ module Middleware
3
+ class Mock < Excon::Middleware::Base
4
+ def request_call(datum)
5
+ if datum[:mock]
6
+ # convert File/Tempfile body to string before matching:
7
+ unless datum[:body].nil? || datum[:body].is_a?(String)
8
+ if datum[:body].respond_to?(:binmode)
9
+ datum[:body].binmode
10
+ end
11
+ if datum[:body].respond_to?(:rewind)
12
+ datum[:body].rewind
13
+ end
14
+ datum[:body] = datum[:body].read
15
+ end
16
+
17
+ datum[:captures] = {:headers => {}} # setup data to hold captures
18
+ Excon.stubs.each do |stub, response|
19
+ headers_match = !stub.has_key?(:headers) || stub[:headers].keys.all? do |key|
20
+ case value = stub[:headers][key]
21
+ when Regexp
22
+ if match = value.match(datum[:headers][key])
23
+ datum[:captures][:headers][key] = match.captures
24
+ end
25
+ match
26
+ else
27
+ value == datum[:headers][key]
28
+ end
29
+ end
30
+ non_headers_match = (stub.keys - [:headers]).all? do |key|
31
+ case value = stub[key]
32
+ when Regexp
33
+ if match = value.match(datum[key])
34
+ datum[:captures][key] = match.captures
35
+ end
36
+ match
37
+ else
38
+ value == datum[key]
39
+ end
40
+ end
41
+ if headers_match && non_headers_match
42
+ datum[:response] = {
43
+ :body => '',
44
+ :headers => {},
45
+ :status => 200,
46
+ :remote_ip => '127.0.0.1'
47
+ }
48
+
49
+ stub_datum = case response
50
+ when Proc
51
+ response.call(datum)
52
+ else
53
+ response
54
+ end
55
+
56
+ datum[:response].merge!(stub_datum.reject {|key,value| key == :headers})
57
+ if stub_datum.has_key?(:headers)
58
+ datum[:response][:headers].merge!(stub_datum[:headers])
59
+ end
60
+
61
+ if datum[:expects] && ![*datum[:expects]].include?(datum[:response][:status])
62
+ # don't pass stuff into a block if there was an error
63
+ elsif datum.has_key?(:response_block) && datum[:response].has_key?(:body)
64
+ body = datum[:response].delete(:body)
65
+ content_length = remaining = body.bytesize
66
+ i = 0
67
+ while i < body.length
68
+ datum[:response_block].call(body[i, datum[:chunk_size]], [remaining - datum[:chunk_size], 0].max, content_length)
69
+ remaining -= datum[:chunk_size]
70
+ i += datum[:chunk_size]
71
+ end
72
+ end
73
+ return @stack.request_call(datum)
74
+ end
75
+ end
76
+ # if we reach here no stubs matched
77
+ raise(Excon::Errors::StubNotFound.new('no stubs matched ' << datum.inspect))
78
+ else
79
+ @stack.request_call(datum)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,15 +1,21 @@
1
1
  module Excon
2
2
  class Response
3
3
 
4
- attr_accessor :body, :headers, :status, :remote_ip
4
+ attr_accessor :body, :data, :headers, :status, :remote_ip
5
5
 
6
- def data
7
- {
8
- :body => body,
9
- :headers => headers,
10
- :status => status,
11
- :remote_ip => remote_ip
12
- }
6
+ def initialize(params={})
7
+ @data = {
8
+ :body => '',
9
+ :headers => {}
10
+ }.merge(params)
11
+ @body = @data[:body]
12
+ @headers = @data[:headers]
13
+ @status = @data[:status]
14
+ @remote_ip = @data[:remote_ip]
15
+ end
16
+
17
+ def [](key)
18
+ @data[key]
13
19
  end
14
20
 
15
21
  def params
@@ -17,75 +23,6 @@ module Excon
17
23
  data
18
24
  end
19
25
 
20
- def initialize(params={})
21
- @body = params[:body] || ''
22
- @headers = params[:headers] || {}
23
- @status = params[:status]
24
- @remote_ip = params[:remote_ip]
25
- end
26
-
27
- def self.parse(socket, datum={})
28
- response_datum = {
29
- :body => '',
30
- :headers => {},
31
- :status => socket.read(12)[9, 11].to_i,
32
- :remote_ip => socket.remote_ip
33
- }
34
- socket.readline # read the rest of the status line and CRLF
35
-
36
- until ((data = socket.readline).chop!).empty?
37
- key, value = data.split(/:\s*/, 2)
38
- response_datum[:headers][key] = ([*response_datum[:headers][key]] << value).compact.join(', ')
39
- if key.casecmp('Content-Length') == 0
40
- content_length = value.to_i
41
- elsif (key.casecmp('Transfer-Encoding') == 0) && (value.casecmp('chunked') == 0)
42
- transfer_encoding_chunked = true
43
- end
44
- end
45
-
46
- unless (['HEAD', 'CONNECT'].include?(datum[:method].to_s.upcase)) || NO_ENTITY.include?(response_datum[:status])
47
-
48
- # check to see if expects was set and matched
49
- expected_status = !datum.has_key?(:expects) || [*datum[:expects]].include?(response_datum[:status])
50
-
51
- # if expects matched and there is a block, use it
52
- if expected_status && datum.has_key?(:response_block)
53
- if transfer_encoding_chunked
54
- # 2 == "/r/n".length
55
- while (chunk_size = socket.readline.chop!.to_i(16)) > 0
56
- datum[:response_block].call(socket.read(chunk_size + 2).chop!, nil, nil)
57
- end
58
- socket.read(2)
59
- elsif remaining = content_length
60
- while remaining > 0
61
- datum[:response_block].call(socket.read([datum[:chunk_size], remaining].min), [remaining - datum[:chunk_size], 0].max, content_length)
62
- remaining -= datum[:chunk_size]
63
- end
64
- else
65
- while remaining = socket.read(datum[:chunk_size])
66
- datum[:response_block].call(remaining, remaining.length, content_length)
67
- end
68
- end
69
- else # no block or unexpected status
70
- if transfer_encoding_chunked
71
- while (chunk_size = socket.readline.chop!.to_i(16)) > 0
72
- response_datum[:body] << socket.read(chunk_size + 2).chop! # 2 == "/r/n".length
73
- end
74
- socket.read(2) # 2 == "/r/n".length
75
- elsif remaining = content_length
76
- while remaining > 0
77
- response_datum[:body] << socket.read([datum[:chunk_size], remaining].min)
78
- remaining -= datum[:chunk_size]
79
- end
80
- else
81
- response_datum[:body] << socket.read
82
- end
83
- end
84
- end
85
-
86
- response_datum
87
- end
88
-
89
26
  # Retrieve a specific header value. Header names are treated case-insensitively.
90
27
  # @param [String] name Header name
91
28
  def get_header(name)
@@ -107,7 +107,7 @@ Shindo.tests('Excon stubs') do
107
107
 
108
108
  tests("stub({:body => File.open(...), :method => :get}, { :status => 200 })") do
109
109
 
110
- file_path = File.join(File.dirname(__FILE__), 'data', 'xs')
110
+ file_path = File.join(File.dirname(__FILE__), '..', 'data', 'xs')
111
111
 
112
112
  Excon.stub(
113
113
  { :body => File.read(file_path), :method => :get },
@@ -17,7 +17,7 @@ Shindo.tests('Excon proxy support') do
17
17
  connection.data[:proxy][:host]
18
18
  end
19
19
 
20
- tests('connection.data[:proxy][:port]').returns(8080) do
20
+ tests('connection.data[:proxy][:port]').returns('8080') do
21
21
  connection.data[:proxy][:port]
22
22
  end
23
23
 
@@ -37,7 +37,7 @@ Shindo.tests('Excon proxy support') do
37
37
  connection.data[:proxy][:host]
38
38
  end
39
39
 
40
- tests('connection.data[:proxy][:port]').returns(8080) do
40
+ tests('connection.data[:proxy][:port]').returns('8080') do
41
41
  connection.data[:proxy][:port]
42
42
  end
43
43
 
@@ -53,7 +53,7 @@ Shindo.tests('Excon proxy support') do
53
53
  connection.data[:proxy][:host]
54
54
  end
55
55
 
56
- tests('connection.data[:proxy][:port]').returns(8081) do
56
+ tests('connection.data[:proxy][:port]').returns('8081') do
57
57
  connection.data[:proxy][:port]
58
58
  end
59
59
 
@@ -69,7 +69,7 @@ Shindo.tests('Excon proxy support') do
69
69
  connection.data[:proxy][:host]
70
70
  end
71
71
 
72
- tests('connection.data[:proxy][:port]').returns(8080) do
72
+ tests('connection.data[:proxy][:port]').returns('8080') do
73
73
  connection.data[:proxy][:port]
74
74
  end
75
75
  end
@@ -89,7 +89,7 @@ Shindo.tests('Excon proxy support') do
89
89
  connection.data[:proxy][:host]
90
90
  end
91
91
 
92
- tests('connection.data[:proxy][:port]').returns(8080) do
92
+ tests('connection.data[:proxy][:port]').returns('8080') do
93
93
  connection.data[:proxy][:port]
94
94
  end
95
95
 
@@ -105,7 +105,7 @@ Shindo.tests('Excon proxy support') do
105
105
  connection.data[:proxy][:host]
106
106
  end
107
107
 
108
- tests('connection.data[:proxy][:port]').returns(8081) do
108
+ tests('connection.data[:proxy][:port]').returns('8081') do
109
109
  connection.data[:proxy][:port]
110
110
  end
111
111
 
@@ -121,7 +121,7 @@ Shindo.tests('Excon proxy support') do
121
121
  connection.data[:proxy][:host]
122
122
  end
123
123
 
124
- tests('connection.data[:proxy][:port]').returns(8080) do
124
+ tests('connection.data[:proxy][:port]').returns('8080') do
125
125
  connection.data[:proxy][:port]
126
126
  end
127
127
  end
@@ -141,7 +141,7 @@ Shindo.tests('Excon proxy support') do
141
141
  connection.data[:proxy][:host]
142
142
  end
143
143
 
144
- tests('connection.data[:proxy][:port]').returns(8080) do
144
+ tests('connection.data[:proxy][:port]').returns('8080') do
145
145
  connection.data[:proxy][:port]
146
146
  end
147
147
 
@@ -0,0 +1,31 @@
1
+ with_rackup('basic.ru') do
2
+ Shindo.tests('requests should succeed') do
3
+
4
+ connection = Excon.new('http://127.0.0.1:9292')
5
+
6
+ tests('HEAD /content-length/100, GET /content-length/100') do
7
+ responses = connection.requests([
8
+ {:method => :head, :path => '/content-length/100'},
9
+ {:method => :get, :path => '/content-length/100'}
10
+ ])
11
+
12
+ tests('head body is empty').returns('') do
13
+ responses.first.body
14
+ end
15
+
16
+ tests('head content length is 100').returns('100') do
17
+ responses.first.headers['Content-Length']
18
+ end
19
+
20
+ tests('get body is non-empty').returns('x' * 100) do
21
+ responses.last.body
22
+ end
23
+
24
+ tests('get content length is 100').returns('100') do
25
+ responses.last.headers['Content-Length']
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -19,6 +19,10 @@ def basic_tests(url = 'http://127.0.0.1:9292')
19
19
  response.status
20
20
  end
21
21
 
22
+ tests('response[:status]').returns(200) do
23
+ response[:status]
24
+ end
25
+
22
26
  tests("response.headers['Connection']").returns('Keep-Alive') do
23
27
  response.headers['Connection']
24
28
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2013-02-01 00:00:00.000000000 Z
14
+ date: 2013-02-21 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -177,8 +177,10 @@ files:
177
177
  - lib/excon/connection.rb
178
178
  - lib/excon/constants.rb
179
179
  - lib/excon/errors.rb
180
+ - lib/excon/middlewares/base.rb
180
181
  - lib/excon/middlewares/expects.rb
181
182
  - lib/excon/middlewares/instrumentor.rb
183
+ - lib/excon/middlewares/mock.rb
182
184
  - lib/excon/response.rb
183
185
  - lib/excon/socket.rb
184
186
  - lib/excon/ssl_socket.rb
@@ -190,6 +192,7 @@ files:
190
192
  - tests/header_tests.rb
191
193
  - tests/idempotent_tests.rb
192
194
  - tests/middlewares/instrumentation_tests.rb
195
+ - tests/middlewares/mock_tests.rb
193
196
  - tests/proxy_tests.rb
194
197
  - tests/query_string_tests.rb
195
198
  - tests/rackups/basic.rb
@@ -205,9 +208,9 @@ files:
205
208
  - tests/rackups/timeout.ru
206
209
  - tests/request_headers_tests.rb
207
210
  - tests/request_method_tests.rb
211
+ - tests/requests_tests.rb
208
212
  - tests/servers/bad.rb
209
213
  - tests/servers/eof.rb
210
- - tests/stub_tests.rb
211
214
  - tests/test_helper.rb
212
215
  - tests/thread_safety_tests.rb
213
216
  - tests/timeout_tests.rb
@@ -226,7 +229,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
226
229
  version: '0'
227
230
  segments:
228
231
  - 0
229
- hash: 4091555892439791383
232
+ hash: 4265625222715917717
230
233
  required_rubygems_version: !ruby/object:Gem::Requirement
231
234
  none: false
232
235
  requirements:
@@ -235,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
235
238
  version: '0'
236
239
  requirements: []
237
240
  rubyforge_project: excon
238
- rubygems_version: 1.8.24
241
+ rubygems_version: 1.8.23
239
242
  signing_key:
240
243
  specification_version: 2
241
244
  summary: speed, persistence, http(s)