rest-client 2.0.2 → 2.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -51,7 +51,8 @@ module RestClient
51
51
  Request.execute(options.merge(
52
52
  :method => :get,
53
53
  :url => url,
54
- :headers => headers), &(block || @block))
54
+ :headers => headers,
55
+ :log => log), &(block || @block))
55
56
  end
56
57
 
57
58
  def head(additional_headers={}, &block)
@@ -59,7 +60,8 @@ module RestClient
59
60
  Request.execute(options.merge(
60
61
  :method => :head,
61
62
  :url => url,
62
- :headers => headers), &(block || @block))
63
+ :headers => headers,
64
+ :log => log), &(block || @block))
63
65
  end
64
66
 
65
67
  def post(payload, additional_headers={}, &block)
@@ -68,7 +70,8 @@ module RestClient
68
70
  :method => :post,
69
71
  :url => url,
70
72
  :payload => payload,
71
- :headers => headers), &(block || @block))
73
+ :headers => headers,
74
+ :log => log), &(block || @block))
72
75
  end
73
76
 
74
77
  def put(payload, additional_headers={}, &block)
@@ -77,7 +80,8 @@ module RestClient
77
80
  :method => :put,
78
81
  :url => url,
79
82
  :payload => payload,
80
- :headers => headers), &(block || @block))
83
+ :headers => headers,
84
+ :log => log), &(block || @block))
81
85
  end
82
86
 
83
87
  def patch(payload, additional_headers={}, &block)
@@ -86,7 +90,8 @@ module RestClient
86
90
  :method => :patch,
87
91
  :url => url,
88
92
  :payload => payload,
89
- :headers => headers), &(block || @block))
93
+ :headers => headers,
94
+ :log => log), &(block || @block))
90
95
  end
91
96
 
92
97
  def delete(additional_headers={}, &block)
@@ -94,7 +99,8 @@ module RestClient
94
99
  Request.execute(options.merge(
95
100
  :method => :delete,
96
101
  :url => url,
97
- :headers => headers), &(block || @block))
102
+ :headers => headers,
103
+ :log => log), &(block || @block))
98
104
  end
99
105
 
100
106
  def to_s
@@ -121,6 +127,10 @@ module RestClient
121
127
  options[:open_timeout]
122
128
  end
123
129
 
130
+ def log
131
+ options[:log] || RestClient.log
132
+ end
133
+
124
134
  # Construct a subresource, preserving authentication.
125
135
  #
126
136
  # Example:
@@ -38,15 +38,25 @@ module RestClient
38
38
  "<RestClient::Response #{code.inspect} #{body_truncated(10).inspect}>"
39
39
  end
40
40
 
41
- def self.create(body, net_http_res, request)
41
+ # Initialize a Response object. Because RestClient::Response is
42
+ # (unfortunately) a subclass of String for historical reasons,
43
+ # Response.create is the preferred initializer.
44
+ #
45
+ # @param [String, nil] body The response body from the Net::HTTPResponse
46
+ # @param [Net::HTTPResponse] net_http_res
47
+ # @param [RestClient::Request] request
48
+ # @param [Time] start_time
49
+ def self.create(body, net_http_res, request, start_time=nil)
42
50
  result = self.new(body || '')
43
51
 
44
- result.response_set_vars(net_http_res, request)
52
+ result.response_set_vars(net_http_res, request, start_time)
45
53
  fix_encoding(result)
46
54
 
47
55
  result
48
56
  end
49
57
 
58
+ # Set the String encoding according to the 'Content-Type: charset' header,
59
+ # if possible.
50
60
  def self.fix_encoding(response)
51
61
  charset = RestClient::Utils.get_encoding_from_headers(response.headers)
52
62
  encoding = nil
@@ -54,8 +64,8 @@ module RestClient
54
64
  begin
55
65
  encoding = Encoding.find(charset) if charset
56
66
  rescue ArgumentError
57
- if RestClient.log
58
- RestClient.log << "No such encoding: #{charset.inspect}"
67
+ if response.log
68
+ response.log << "No such encoding: #{charset.inspect}"
59
69
  end
60
70
  end
61
71
 
@@ -1,3 +1,5 @@
1
+ require 'http/accept'
2
+
1
3
  module RestClient
2
4
  # Various utility methods
3
5
  module Utils
@@ -13,8 +15,8 @@ module RestClient
13
15
  #
14
16
  # @param headers [Hash<Symbol,String>]
15
17
  #
16
- # @return [String, nil] encoding Return the string encoding or nil if no
17
- # header is found.
18
+ # @return [String, nil] Return the string encoding or nil if no header is
19
+ # found.
18
20
  #
19
21
  # @example
20
22
  # >> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'})
@@ -24,19 +26,52 @@ module RestClient
24
26
  type_header = headers[:content_type]
25
27
  return nil unless type_header
26
28
 
27
- _content_type, params = cgi_parse_header(type_header)
29
+ # TODO: remove this hack once we drop support for Ruby 2.0
30
+ if RUBY_VERSION.start_with?('2.0')
31
+ _content_type, params = deprecated_cgi_parse_header(type_header)
32
+
33
+ if params.include?('charset')
34
+ return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
35
+ end
36
+
37
+ else
28
38
 
29
- if params.include?('charset')
30
- return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
39
+ begin
40
+ _content_type, params = cgi_parse_header(type_header)
41
+ rescue HTTP::Accept::ParseError
42
+ return nil
43
+ else
44
+ params['charset']
45
+ end
31
46
  end
47
+ end
32
48
 
33
- nil
49
+ # Parse a Content-Type like header.
50
+ #
51
+ # Return the main content-type and a hash of params.
52
+ #
53
+ # @param [String] line
54
+ # @return [Array(String, Hash)]
55
+ #
56
+ def self.cgi_parse_header(line)
57
+ types = HTTP::Accept::MediaTypes.parse(line)
58
+
59
+ if types.empty?
60
+ raise HTTP::Accept::ParseError.new("Found no types in header line")
61
+ end
62
+
63
+ [types.first.mime_type, types.first.parameters]
34
64
  end
35
65
 
36
66
  # Parse semi-colon separated, potentially quoted header string iteratively.
37
67
  #
38
68
  # @private
39
69
  #
70
+ # @deprecated This method is deprecated and only exists to support Ruby
71
+ # 2.0, which is not supported by HTTP::Accept.
72
+ #
73
+ # @todo remove this method when dropping support for Ruby 2.0
74
+ #
40
75
  def self._cgi_parseparam(s)
41
76
  return enum_for(__method__, s) unless block_given?
42
77
 
@@ -66,11 +101,15 @@ module RestClient
66
101
  # probably doesn't read or perform particularly well in ruby.
67
102
  # https://github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331
68
103
  #
69
- #
70
104
  # @param [String] line
71
105
  # @return [Array(String, Hash)]
72
106
  #
73
- def self.cgi_parse_header(line)
107
+ # @deprecated This method is deprecated and only exists to support Ruby
108
+ # 2.0, which is not supported by HTTP::Accept.
109
+ #
110
+ # @todo remove this method when dropping support for Ruby 2.0
111
+ #
112
+ def self.deprecated_cgi_parse_header(line)
74
113
  parts = _cgi_parseparam(';' + line)
75
114
  key = parts.next
76
115
  pdict = {}
@@ -1,6 +1,6 @@
1
1
  module RestClient
2
- VERSION_INFO = [2, 0, 2] unless defined?(self::VERSION_INFO)
3
- VERSION = VERSION_INFO.map(&:to_s).join('.') unless defined?(self::VERSION)
2
+ VERSION_INFO = [2, 1, 0, 'rc1'].freeze
3
+ VERSION = VERSION_INFO.map(&:to_s).join('.').freeze
4
4
 
5
5
  def self.version
6
6
  VERSION
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency('rdoc', '>= 2.4.2', '< 6.0')
24
24
  s.add_development_dependency('rubocop', '~> 0')
25
25
 
26
+ s.add_dependency('http-accept', '>= 1.7.0', '< 2.0')
26
27
  s.add_dependency('http-cookie', '>= 1.0.2', '< 2.0')
27
28
  s.add_dependency('mime-types', '>= 1.16', '< 4.0')
28
29
  s.add_dependency('netrc', '~> 0.8')
Binary file
@@ -1,10 +1,36 @@
1
1
  require 'uri'
2
2
 
3
3
  module Helpers
4
- def response_double(opts={})
5
- double('response', {:to_hash => {}}.merge(opts))
4
+
5
+ # @param [Hash] opts A hash of methods, passed directly to the double
6
+ # definition. Use this to stub other required methods.
7
+ #
8
+ # @return double for Net::HTTPResponse
9
+ def res_double(opts={})
10
+ instance_double('Net::HTTPResponse', {to_hash: {}, body: 'response body'}.merge(opts))
11
+ end
12
+
13
+ # Given a Net::HTTPResponse or double and a Request or double, create a
14
+ # RestClient::Response object.
15
+ #
16
+ # @param net_http_res_double an rspec double for Net::HTTPResponse
17
+ # @param request A RestClient::Request or rspec double
18
+ #
19
+ # @return [RestClient::Response]
20
+ #
21
+ def response_from_res_double(net_http_res_double, request=nil, duration: 1)
22
+ request ||= request_double()
23
+ start_time = Time.now - duration
24
+
25
+ response = RestClient::Response.create(net_http_res_double.body, net_http_res_double, request, start_time)
26
+
27
+ # mock duration to ensure it gets the value we expect
28
+ allow(response).to receive(:duration).and_return(duration)
29
+
30
+ response
6
31
  end
7
32
 
33
+ # Redirect stderr to a string for the duration of the passed block.
8
34
  def fake_stderr
9
35
  original_stderr = $stderr
10
36
  $stderr = StringIO.new
@@ -14,9 +40,15 @@ module Helpers
14
40
  $stderr = original_stderr
15
41
  end
16
42
 
43
+ # Create a double for RestClient::Request
17
44
  def request_double(url: 'http://example.com', method: 'get')
18
- double('request', url: url, uri: URI.parse(url), method: method,
19
- user: nil, password: nil, cookie_jar: HTTP::CookieJar.new,
20
- redirection_history: nil, args: {url: url, method: method})
45
+ instance_double('RestClient::Request',
46
+ url: url, uri: URI.parse(url), method: method, user: nil, password: nil,
47
+ cookie_jar: HTTP::CookieJar.new, redirection_history: nil,
48
+ args: {url: url, method: method})
49
+ end
50
+
51
+ def test_image_path
52
+ File.dirname(__FILE__) + "/ISS.jpg"
21
53
  end
22
54
  end
@@ -1,6 +1,8 @@
1
1
  require_relative '_lib'
2
2
  require 'json'
3
3
 
4
+ require 'zlib'
5
+
4
6
  describe RestClient::Request do
5
7
  before(:all) do
6
8
  WebMock.disable!
@@ -83,5 +85,44 @@ describe RestClient::Request do
83
85
  expect(ex.http_code).to eq 401
84
86
  }
85
87
  end
88
+
89
+ it 'handles gzipped/deflated responses' do
90
+ [['gzip', 'gzipped'], ['deflate', 'deflated']].each do |encoding, var|
91
+ raw = execute_httpbin(encoding, method: :get)
92
+
93
+ begin
94
+ data = JSON.parse(raw)
95
+ rescue
96
+ puts "Failed to parse: " + raw.inspect
97
+ raise
98
+ end
99
+
100
+ expect(data['method']).to eq 'GET'
101
+ expect(data.fetch(var)).to be true
102
+ end
103
+ end
104
+
105
+ it 'does not uncompress response when accept-encoding is set' do
106
+ # == gzip ==
107
+ raw = execute_httpbin('gzip', method: :get, headers: {accept_encoding: 'gzip, deflate'})
108
+
109
+ # check for gzip magic number
110
+ expect(raw.body).to start_with("\x1F\x8B".b)
111
+
112
+ decoded = Zlib::GzipReader.new(StringIO.new(raw.body)).read
113
+ parsed = JSON.parse(decoded)
114
+
115
+ expect(parsed['method']).to eq 'GET'
116
+ expect(parsed.fetch('gzipped')).to be true
117
+
118
+ # == delate ==
119
+ raw = execute_httpbin('deflate', method: :get, headers: {accept_encoding: 'gzip, deflate'})
120
+
121
+ decoded = Zlib::Inflate.new.inflate(raw.body)
122
+ parsed = JSON.parse(decoded)
123
+
124
+ expect(parsed['method']).to eq 'GET'
125
+ expect(parsed.fetch('deflated')).to be true
126
+ end
86
127
  end
87
128
  end
@@ -12,13 +12,6 @@ describe RestClient do
12
12
  expect(response.body).to eq body
13
13
  end
14
14
 
15
- it "a simple request with gzipped content" do
16
- stub_request(:get, "www.example.com").with(:headers => { 'Accept-Encoding' => 'gzip, deflate' }).to_return(:body => "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000", :status => 200, :headers => { 'Content-Encoding' => 'gzip' } )
17
- response = RestClient.get "www.example.com"
18
- expect(response.code).to eq 200
19
- expect(response.body).to eq "i'm gziped\n"
20
- end
21
-
22
15
  it "a 404" do
23
16
  body = "Ho hai ! I'm not here !"
24
17
  stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
@@ -2,21 +2,21 @@ require_relative '_lib'
2
2
 
3
3
  describe RestClient::AbstractResponse, :include_helpers do
4
4
 
5
+ # Sample class implementing AbstractResponse used for testing.
5
6
  class MyAbstractResponse
6
7
 
7
8
  include RestClient::AbstractResponse
8
9
 
9
10
  attr_accessor :size
10
11
 
11
- def initialize net_http_res, request
12
- @net_http_res = net_http_res
13
- @request = request
12
+ def initialize(net_http_res, request)
13
+ response_set_vars(net_http_res, request, Time.now - 1)
14
14
  end
15
15
 
16
16
  end
17
17
 
18
18
  before do
19
- @net_http_res = double('net http response')
19
+ @net_http_res = res_double()
20
20
  @request = request_double(url: 'http://example.com', method: 'get')
21
21
  @response = MyAbstractResponse.new(@net_http_res, @request)
22
22
  end
@@ -92,8 +92,8 @@ describe RestClient::AbstractResponse, :include_helpers do
92
92
  it 'handles cookies when URI scheme is implicit' do
93
93
  net_http_res = double('net http response')
94
94
  expect(net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
95
- request = double(url: 'example.com', uri: URI.parse('http://example.com'),
96
- method: 'get', cookie_jar: HTTP::CookieJar.new)
95
+ request = double('request', url: 'example.com', uri: URI.parse('http://example.com'),
96
+ method: 'get', cookie_jar: HTTP::CookieJar.new, redirection_history: nil)
97
97
  response = MyAbstractResponse.new(net_http_res, request)
98
98
  expect(response.cookie_jar).to be_a HTTP::CookieJar
99
99
 
@@ -135,7 +135,7 @@ describe RestClient::AbstractResponse, :include_helpers do
135
135
  end
136
136
 
137
137
  it "should gracefully handle 302 redirect with no location header" do
138
- @net_http_res = response_double(code: 302, location: nil)
138
+ @net_http_res = res_double(code: 302)
139
139
  @request = request_double()
140
140
  @response = MyAbstractResponse.new(@net_http_res, @request)
141
141
  expect(@response).to receive(:check_max_redirects).and_return('fake-check')
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative '_lib'
4
4
 
5
- describe RestClient::Payload do
5
+ describe RestClient::Payload, :include_helpers do
6
6
  context "Base Payload" do
7
7
  it "should reset stream after to_s" do
8
8
  payload = RestClient::Payload::Base.new('foobar')
@@ -80,7 +80,7 @@ describe RestClient::Payload do
80
80
  end
81
81
 
82
82
  it 'should not error on close if stream already closed' do
83
- m = RestClient::Payload::Multipart.new(:file => File.new(File.join(File.dirname(File.expand_path(__FILE__)), 'master_shake.jpg')))
83
+ m = RestClient::Payload::Multipart.new(:file => File.new(test_image_path))
84
84
  3.times {m.close}
85
85
  end
86
86
 
@@ -111,11 +111,11 @@ baz\r
111
111
  end
112
112
 
113
113
  it "should form properly separated multipart data" do
114
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
114
+ f = File.new(test_image_path)
115
115
  m = RestClient::Payload::Multipart.new({:foo => f})
116
116
  expect(m.to_s).to eq <<-EOS
117
117
  --#{m.boundary}\r
118
- Content-Disposition: form-data; name="foo"; filename="master_shake.jpg"\r
118
+ Content-Disposition: form-data; name="foo"; filename="ISS.jpg"\r
119
119
  Content-Type: image/jpeg\r
120
120
  \r
121
121
  #{File.open(f.path, 'rb'){|bin| bin.read}}\r
@@ -124,11 +124,11 @@ Content-Type: image/jpeg\r
124
124
  end
125
125
 
126
126
  it "should ignore the name attribute when it's not set" do
127
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
127
+ f = File.new(test_image_path)
128
128
  m = RestClient::Payload::Multipart.new({nil => f})
129
129
  expect(m.to_s).to eq <<-EOS
130
130
  --#{m.boundary}\r
131
- Content-Disposition: form-data; filename="master_shake.jpg"\r
131
+ Content-Disposition: form-data; filename="ISS.jpg"\r
132
132
  Content-Type: image/jpeg\r
133
133
  \r
134
134
  #{File.open(f.path, 'rb'){|bin| bin.read}}\r
@@ -137,9 +137,9 @@ Content-Type: image/jpeg\r
137
137
  end
138
138
 
139
139
  it "should detect optional (original) content type and filename" do
140
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
141
- f.instance_eval "def content_type; 'text/plain'; end"
142
- f.instance_eval "def original_filename; 'foo.txt'; end"
140
+ f = File.new(test_image_path)
141
+ expect(f).to receive(:content_type).and_return('text/plain')
142
+ expect(f).to receive(:original_filename).and_return('foo.txt')
143
143
  m = RestClient::Payload::Multipart.new({:foo => f})
144
144
  expect(m.to_s).to eq <<-EOS
145
145
  --#{m.boundary}\r
@@ -161,7 +161,7 @@ foo\r
161
161
  --#{m.boundary}--\r
162
162
  EOS
163
163
 
164
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
164
+ f = File.new(test_image_path)
165
165
  f.instance_eval "def content_type; 'text/plain'; end"
166
166
  f.instance_eval "def original_filename; 'foo.txt'; end"
167
167
  m = RestClient::Payload::Multipart.new({:foo => {:bar => f}})
@@ -177,7 +177,7 @@ Content-Type: text/plain\r
177
177
 
178
178
  it 'should correctly format hex boundary' do
179
179
  allow(SecureRandom).to receive(:base64).with(12).and_return('TGs89+ttw/xna6TV')
180
- f = File.new(File.dirname(__FILE__) + '/master_shake.jpg')
180
+ f = File.new(test_image_path)
181
181
  m = RestClient::Payload::Multipart.new({:foo => f})
182
182
  expect(m.boundary).to eq('-' * 4 + 'RubyFormBoundary' + 'TGs89AttwBxna6TV')
183
183
  end
@@ -186,10 +186,10 @@ Content-Type: text/plain\r
186
186
 
187
187
  context "streamed payloads" do
188
188
  it "should properly determine the size of file payloads" do
189
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
189
+ f = File.new(test_image_path)
190
190
  payload = RestClient::Payload.generate(f)
191
- expect(payload.size).to eq 76_988
192
- expect(payload.length).to eq 76_988
191
+ expect(payload.size).to eq 72_463
192
+ expect(payload.length).to eq 72_463
193
193
  end
194
194
 
195
195
  it "should properly determine the size of other kinds of streaming payloads" do
@@ -209,6 +209,14 @@ Content-Type: text/plain\r
209
209
  f.close
210
210
  end
211
211
  end
212
+
213
+ it "should have a closed? method" do
214
+ f = File.new(test_image_path)
215
+ payload = RestClient::Payload.generate(f)
216
+ expect(payload.closed?).to be_falsey
217
+ payload.close
218
+ expect(payload.closed?).to be_truthy
219
+ end
212
220
  end
213
221
 
214
222
  context "Payload generation" do
@@ -217,7 +225,7 @@ Content-Type: text/plain\r
217
225
  end
218
226
 
219
227
  it "should recognize multipart params" do
220
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
228
+ f = File.new(test_image_path)
221
229
  expect(RestClient::Payload.generate({"foo" => f})).to be_kind_of(RestClient::Payload::Multipart)
222
230
  end
223
231
 
@@ -226,7 +234,7 @@ Content-Type: text/plain\r
226
234
  end
227
235
 
228
236
  it "should handle deeply nested multipart" do
229
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
237
+ f = File.new(test_image_path)
230
238
  params = {foo: RestClient::ParamsArray.new({nested: f})}
231
239
  expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::Multipart)
232
240
  end
@@ -237,17 +245,17 @@ Content-Type: text/plain\r
237
245
  end
238
246
 
239
247
  it "should recognize nested multipart payloads in hashes" do
240
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
248
+ f = File.new(test_image_path)
241
249
  expect(RestClient::Payload.generate({"foo" => {"file" => f}})).to be_kind_of(RestClient::Payload::Multipart)
242
250
  end
243
251
 
244
252
  it "should recognize nested multipart payloads in arrays" do
245
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
253
+ f = File.new(test_image_path)
246
254
  expect(RestClient::Payload.generate({"foo" => [f]})).to be_kind_of(RestClient::Payload::Multipart)
247
255
  end
248
256
 
249
257
  it "should recognize file payloads that can be streamed" do
250
- f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
258
+ f = File.new(test_image_path)
251
259
  expect(RestClient::Payload.generate(f)).to be_kind_of(RestClient::Payload::Streamed)
252
260
  end
253
261
 
@@ -259,5 +267,29 @@ Content-Type: text/plain\r
259
267
  it "shouldn't treat hashes as streameable" do
260
268
  expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded)
261
269
  end
270
+
271
+ it "should recognize multipart payload wrapped in ParamsArray" do
272
+ f = File.new(test_image_path)
273
+ params = RestClient::ParamsArray.new([[:image, f]])
274
+ expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::Multipart)
275
+ end
276
+
277
+ it "should handle non-multipart payload wrapped in ParamsArray" do
278
+ params = RestClient::ParamsArray.new([[:arg, 'value1'], [:arg, 'value2']])
279
+ expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::UrlEncoded)
280
+ end
281
+
282
+ it "should pass through Payload::Base and subclasses unchanged" do
283
+ payloads = [
284
+ RestClient::Payload::Base.new('foobar'),
285
+ RestClient::Payload::UrlEncoded.new({:foo => 'bar'}),
286
+ RestClient::Payload::Streamed.new(File.new(test_image_path)),
287
+ RestClient::Payload::Multipart.new({myfile: File.new(test_image_path)}),
288
+ ]
289
+
290
+ payloads.each do |payload|
291
+ expect(RestClient::Payload.generate(payload)).to equal(payload)
292
+ end
293
+ end
262
294
  end
263
295
  end