rest-client 1.2.0 → 1.3.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.
Potentially problematic release.
This version of rest-client might be problematic. Click here for more details.
- data/README.rdoc +119 -7
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/history.md +28 -0
- data/lib/restclient.rb +63 -20
- data/lib/restclient/exceptions.rb +77 -46
- data/lib/restclient/mixin/response.rb +23 -7
- data/lib/restclient/payload.rb +2 -2
- data/lib/restclient/request.rb +60 -71
- data/lib/restclient/resource.rb +11 -10
- data/spec/exceptions_spec.rb +3 -5
- data/spec/integration_spec.rb +38 -0
- data/spec/mixin/response_spec.rb +1 -1
- data/spec/payload_spec.rb +6 -6
- data/spec/request_spec.rb +357 -332
- data/spec/resource_spec.rb +24 -0
- data/spec/response_spec.rb +51 -0
- data/spec/restclient_spec.rb +21 -11
- metadata +8 -4
@@ -3,8 +3,7 @@ module RestClient
|
|
3
3
|
module Response
|
4
4
|
attr_reader :net_http_res
|
5
5
|
|
6
|
-
# HTTP status code
|
7
|
-
# other codes.
|
6
|
+
# HTTP status code
|
8
7
|
def code
|
9
8
|
@code ||= @net_http_res.code.to_i
|
10
9
|
end
|
@@ -22,15 +21,32 @@ module RestClient
|
|
22
21
|
|
23
22
|
# Hash of cookies extracted from response headers
|
24
23
|
def cookies
|
25
|
-
@cookies ||= (self.headers[:set_cookie] ||
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
@cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content|
|
25
|
+
# correctly parse comma-separated cookies containing HTTP dates (which also contain a comma)
|
26
|
+
cookie_content.split(/,\s*/).inject([""]) { |array, blob|
|
27
|
+
blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob)
|
28
|
+
array
|
29
|
+
}.each do |cookie|
|
30
|
+
next if cookie.empty?
|
31
|
+
key, *val = cookie.split(";").first.split("=")
|
32
|
+
out[key] = val.join("=")
|
29
33
|
end
|
30
34
|
out
|
31
35
|
end
|
32
36
|
end
|
33
37
|
|
38
|
+
# Return the default behavior corresponding to the response code:
|
39
|
+
# the response itself for code in 200..206 and an exception in other cases
|
40
|
+
def return!
|
41
|
+
if (200..206).include? code
|
42
|
+
self
|
43
|
+
elsif Exceptions::EXCEPTIONS_MAP[code]
|
44
|
+
raise Exceptions::EXCEPTIONS_MAP[code], self
|
45
|
+
else
|
46
|
+
raise RequestFailed, self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
34
50
|
def self.included(receiver)
|
35
51
|
receiver.extend(RestClient::Mixin::Response::ClassMethods)
|
36
52
|
end
|
@@ -38,7 +54,7 @@ module RestClient
|
|
38
54
|
module ClassMethods
|
39
55
|
def beautify_headers(headers)
|
40
56
|
headers.inject({}) do |out, (key, value)|
|
41
|
-
out[key.gsub(/-/, '_').downcase.to_sym] = value.first
|
57
|
+
out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first
|
42
58
|
out
|
43
59
|
end
|
44
60
|
end
|
data/lib/restclient/payload.rb
CHANGED
@@ -138,7 +138,7 @@ module RestClient
|
|
138
138
|
end
|
139
139
|
|
140
140
|
def create_regular_field(s, k, v)
|
141
|
-
s.write("Content-Disposition:
|
141
|
+
s.write("Content-Disposition: form-data; name=\"#{k}\"")
|
142
142
|
s.write(EOL)
|
143
143
|
s.write(EOL)
|
144
144
|
s.write(v)
|
@@ -146,7 +146,7 @@ module RestClient
|
|
146
146
|
|
147
147
|
def create_file_field(s, k, v)
|
148
148
|
begin
|
149
|
-
s.write("Content-Disposition:
|
149
|
+
s.write("Content-Disposition: form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
|
150
150
|
s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
|
151
151
|
s.write(EOL)
|
152
152
|
while data = v.read(8124)
|
data/lib/restclient/request.rb
CHANGED
@@ -20,16 +20,18 @@ module RestClient
|
|
20
20
|
# * :timeout and :open_timeout
|
21
21
|
# * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
|
22
22
|
class Request
|
23
|
+
|
23
24
|
attr_reader :method, :url, :payload, :headers, :processed_headers,
|
24
25
|
:cookies, :user, :password, :timeout, :open_timeout,
|
25
26
|
:verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
|
26
27
|
:raw_response
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
|
30
|
+
def self.execute(args, &block)
|
31
|
+
new(args).execute &block
|
30
32
|
end
|
31
33
|
|
32
|
-
def initialize
|
34
|
+
def initialize args
|
33
35
|
@method = args[:method] or raise ArgumentError, "must pass :method"
|
34
36
|
@url = args[:url] or raise ArgumentError, "must pass :url"
|
35
37
|
@headers = args[:headers] || {}
|
@@ -48,23 +50,25 @@ module RestClient
|
|
48
50
|
@processed_headers = make_headers headers
|
49
51
|
end
|
50
52
|
|
51
|
-
def execute
|
52
|
-
execute_inner
|
53
|
+
def execute &block
|
54
|
+
execute_inner &block
|
53
55
|
rescue Redirect => e
|
56
|
+
@processed_headers.delete "Content-Length"
|
57
|
+
@processed_headers.delete "Content-Type"
|
54
58
|
@url = e.url
|
55
59
|
@method = :get
|
56
60
|
@payload = nil
|
57
|
-
execute
|
61
|
+
execute &block
|
58
62
|
end
|
59
63
|
|
60
|
-
def execute_inner
|
64
|
+
def execute_inner &block
|
61
65
|
uri = parse_url_with_auth(url)
|
62
|
-
transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload
|
66
|
+
transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, &block
|
63
67
|
end
|
64
68
|
|
65
69
|
def make_headers user_headers
|
66
70
|
unless @cookies.empty?
|
67
|
-
user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join(
|
71
|
+
user_headers[:cookie] = @cookies.map {|(key, val)| "#{key.to_s}=#{val}" }.sort.join(",")
|
68
72
|
end
|
69
73
|
|
70
74
|
headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
|
@@ -73,13 +77,13 @@ module RestClient
|
|
73
77
|
target_value = value.to_s
|
74
78
|
final[target_key] = MIME::Types.type_for_extension target_value
|
75
79
|
elsif 'ACCEPT' == target_key.upcase
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
80
|
+
# Accept can be composed of several comma-separated values
|
81
|
+
if value.is_a? Array
|
82
|
+
target_values = value
|
83
|
+
else
|
84
|
+
target_values = value.to_s.split ','
|
85
|
+
end
|
86
|
+
final[target_key] = target_values.map{ |ext| MIME::Types.type_for_extension(ext.to_s.strip)}.join(', ')
|
83
87
|
else
|
84
88
|
final[target_key] = value.to_s
|
85
89
|
end
|
@@ -132,8 +136,8 @@ module RestClient
|
|
132
136
|
end
|
133
137
|
end
|
134
138
|
|
135
|
-
def transmit
|
136
|
-
setup_credentials
|
139
|
+
def transmit uri, req, payload, &block
|
140
|
+
setup_credentials req
|
137
141
|
|
138
142
|
net = net_http_class.new(uri.host, uri.port)
|
139
143
|
net.use_ssl = uri.is_a?(URI::HTTPS)
|
@@ -148,20 +152,12 @@ module RestClient
|
|
148
152
|
net.read_timeout = @timeout if @timeout
|
149
153
|
net.open_timeout = @open_timeout if @open_timeout
|
150
154
|
|
151
|
-
|
155
|
+
log_request
|
152
156
|
|
153
157
|
net.start do |http|
|
154
158
|
res = http.request(req, payload) { |http_response| fetch_body(http_response) }
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
if result.kind_of?(String) or @method == :head
|
159
|
-
Response.new(result, res)
|
160
|
-
elsif @raw_response
|
161
|
-
RawResponse.new(@tf, res)
|
162
|
-
else
|
163
|
-
Response.new(nil, res)
|
164
|
-
end
|
159
|
+
log_response res
|
160
|
+
process_result res, &block
|
165
161
|
end
|
166
162
|
rescue EOFError
|
167
163
|
raise RestClient::ServerBrokeConnection
|
@@ -181,14 +177,16 @@ module RestClient
|
|
181
177
|
@tf = Tempfile.new("rest-client")
|
182
178
|
size, total = 0, http_response.header['Content-Length'].to_i
|
183
179
|
http_response.read_body do |chunk|
|
184
|
-
@tf.write
|
180
|
+
@tf.write chunk
|
185
181
|
size += chunk.size
|
186
|
-
if
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
182
|
+
if RestClient.log
|
183
|
+
if size == 0
|
184
|
+
RestClient.log << "#{@method} #{@url} done (0 length file\n)"
|
185
|
+
elsif total == 0
|
186
|
+
RestClient.log << "#{@method} #{@url} (zero content length)\n"
|
187
|
+
else
|
188
|
+
RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total]
|
189
|
+
end
|
192
190
|
end
|
193
191
|
end
|
194
192
|
@tf.close
|
@@ -199,13 +197,17 @@ module RestClient
|
|
199
197
|
http_response
|
200
198
|
end
|
201
199
|
|
202
|
-
def process_result
|
203
|
-
if
|
200
|
+
def process_result res
|
201
|
+
if @raw_response
|
204
202
|
# We don't decode raw requests
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
203
|
+
response = RawResponse.new(@tf, res)
|
204
|
+
else
|
205
|
+
response = Response.new(Request.decode(res['content-encoding'], res.body), res)
|
206
|
+
end
|
207
|
+
|
208
|
+
code = res.code.to_i
|
209
|
+
|
210
|
+
if (301..303).include? code
|
209
211
|
url = res.header['Location']
|
210
212
|
|
211
213
|
if url !~ /^http/
|
@@ -213,53 +215,40 @@ module RestClient
|
|
213
215
|
uri.path = "/#{url}".squeeze('/')
|
214
216
|
url = uri.to_s
|
215
217
|
end
|
216
|
-
|
217
218
|
raise Redirect, url
|
218
|
-
elsif res.code == "304"
|
219
|
-
raise NotModified, res
|
220
|
-
elsif res.code == "401"
|
221
|
-
raise Unauthorized, res
|
222
|
-
elsif res.code == "404"
|
223
|
-
raise ResourceNotFound, res
|
224
219
|
else
|
225
|
-
|
220
|
+
if block_given?
|
221
|
+
yield response
|
222
|
+
else
|
223
|
+
response.return!
|
224
|
+
end
|
226
225
|
end
|
227
226
|
end
|
228
227
|
|
229
|
-
def self.decode
|
228
|
+
def self.decode content_encoding, body
|
230
229
|
if content_encoding == 'gzip' and not body.empty?
|
231
230
|
Zlib::GzipReader.new(StringIO.new(body)).read
|
232
231
|
elsif content_encoding == 'deflate'
|
233
|
-
Zlib::Inflate.new.inflate
|
232
|
+
Zlib::Inflate.new.inflate body
|
234
233
|
else
|
235
234
|
body
|
236
235
|
end
|
237
236
|
end
|
238
237
|
|
239
|
-
def
|
238
|
+
def log_request
|
240
239
|
if RestClient.log
|
241
240
|
out = []
|
242
241
|
out << "RestClient.#{method} #{url.inspect}"
|
243
|
-
out <<
|
244
|
-
out <<
|
245
|
-
out.join(', ')
|
242
|
+
out << payload.short_inspect if payload
|
243
|
+
out << processed_headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '')
|
244
|
+
RestClient.log << out.join(', ') + "\n"
|
246
245
|
end
|
247
246
|
end
|
248
247
|
|
249
|
-
def
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
def display_log(msg)
|
255
|
-
return unless log_to = RestClient.log
|
256
|
-
|
257
|
-
if log_to == 'stdout'
|
258
|
-
STDOUT.puts msg
|
259
|
-
elsif log_to == 'stderr'
|
260
|
-
STDERR.puts msg
|
261
|
-
else
|
262
|
-
File.open(log_to, 'a') { |f| f.puts msg }
|
248
|
+
def log_response res
|
249
|
+
if RestClient.log
|
250
|
+
size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
|
251
|
+
RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
|
263
252
|
end
|
264
253
|
end
|
265
254
|
|
@@ -274,7 +263,7 @@ module MIME
|
|
274
263
|
|
275
264
|
# Return the first found content-type for a value considered as an extension or the value itself
|
276
265
|
def type_for_extension ext
|
277
|
-
candidates =
|
266
|
+
candidates = @extension_index[ext]
|
278
267
|
candidates.empty? ? ext : candidates[0].content_type
|
279
268
|
end
|
280
269
|
|
data/lib/restclient/resource.rb
CHANGED
@@ -34,10 +34,11 @@ module RestClient
|
|
34
34
|
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
35
35
|
#
|
36
36
|
class Resource
|
37
|
-
attr_reader :url, :options
|
37
|
+
attr_reader :url, :options, :block
|
38
38
|
|
39
|
-
def initialize(url, options={}, backwards_compatibility=nil)
|
39
|
+
def initialize(url, options={}, backwards_compatibility=nil, &block)
|
40
40
|
@url = url
|
41
|
+
@block = block
|
41
42
|
if options.class == Hash
|
42
43
|
@options = options
|
43
44
|
else # compatibility with previous versions
|
@@ -45,38 +46,38 @@ module RestClient
|
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
|
-
def get(additional_headers={}, &
|
49
|
+
def get(additional_headers={}, &block)
|
49
50
|
headers = (options[:headers] || {}).merge(additional_headers)
|
50
51
|
Request.execute(options.merge(
|
51
52
|
:method => :get,
|
52
53
|
:url => url,
|
53
|
-
:headers => headers), &
|
54
|
+
:headers => headers), &(block || @block))
|
54
55
|
end
|
55
56
|
|
56
|
-
def post(payload, additional_headers={}, &
|
57
|
+
def post(payload, additional_headers={}, &block)
|
57
58
|
headers = (options[:headers] || {}).merge(additional_headers)
|
58
59
|
Request.execute(options.merge(
|
59
60
|
:method => :post,
|
60
61
|
:url => url,
|
61
62
|
:payload => payload,
|
62
|
-
:headers => headers), &
|
63
|
+
:headers => headers), &(block || @block))
|
63
64
|
end
|
64
65
|
|
65
|
-
def put(payload, additional_headers={}, &
|
66
|
+
def put(payload, additional_headers={}, &block)
|
66
67
|
headers = (options[:headers] || {}).merge(additional_headers)
|
67
68
|
Request.execute(options.merge(
|
68
69
|
:method => :put,
|
69
70
|
:url => url,
|
70
71
|
:payload => payload,
|
71
|
-
:headers => headers), &
|
72
|
+
:headers => headers), &(block || @block))
|
72
73
|
end
|
73
74
|
|
74
|
-
def delete(additional_headers={}, &
|
75
|
+
def delete(additional_headers={}, &block)
|
75
76
|
headers = (options[:headers] || {}).merge(additional_headers)
|
76
77
|
Request.execute(options.merge(
|
77
78
|
:method => :delete,
|
78
79
|
:url => url,
|
79
|
-
:headers => headers), &
|
80
|
+
:headers => headers), &(block || @block))
|
80
81
|
end
|
81
82
|
|
82
83
|
def to_s
|
data/spec/exceptions_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/base'
|
|
2
2
|
|
3
3
|
describe RestClient::Exception do
|
4
4
|
it "sets the exception message to ErrorMessage" do
|
5
|
-
RestClient::ResourceNotFound.new.message.should == 'Resource
|
5
|
+
RestClient::ResourceNotFound.new.message.should == 'Resource Not Found'
|
6
6
|
end
|
7
7
|
|
8
8
|
it "contains exceptions in RestClient" do
|
@@ -29,10 +29,8 @@ describe RestClient::RequestFailed do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it "http_body convenience method for fetching the body (decoding when necessary)" do
|
32
|
-
@response
|
33
|
-
@response
|
34
|
-
RestClient::Request.should_receive(:decode).with('gzip', 'compressed body').and_return('plain body')
|
35
|
-
RestClient::RequestFailed.new(@response).http_body.should == 'plain body'
|
32
|
+
RestClient::RequestFailed.new(@response).http_code.should == 502
|
33
|
+
RestClient::RequestFailed.new(@response).message.should == 'HTTP status code 502'
|
36
34
|
end
|
37
35
|
|
38
36
|
it "shows the status code in the message" do
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
require 'webmock/rspec'
|
4
|
+
include WebMock
|
5
|
+
|
6
|
+
describe RestClient do
|
7
|
+
|
8
|
+
it "a simple request" do
|
9
|
+
body = 'abc'
|
10
|
+
stub_request(:get, "www.example.com").to_return(:body => body, :status => 200)
|
11
|
+
response = RestClient.get "www.example.com"
|
12
|
+
response.code.should == 200
|
13
|
+
response.should == body
|
14
|
+
end
|
15
|
+
|
16
|
+
it "a simple request with gzipped content" do
|
17
|
+
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' } )
|
18
|
+
response = RestClient.get "www.example.com"
|
19
|
+
response.code.should == 200
|
20
|
+
response.should == "i'm gziped\n"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "a 404" do
|
24
|
+
body = "Ho hai ! I'm not here !"
|
25
|
+
stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
|
26
|
+
begin
|
27
|
+
RestClient.get "www.example.com"
|
28
|
+
raise
|
29
|
+
rescue RestClient::ResourceNotFound => e
|
30
|
+
e.http_code.should == 404
|
31
|
+
e.response.code.should == 404
|
32
|
+
e.response.should == body
|
33
|
+
e.http_body.should == body
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
end
|
data/spec/mixin/response_spec.rb
CHANGED
data/spec/payload_spec.rb
CHANGED
@@ -39,11 +39,11 @@ describe RestClient::Payload do
|
|
39
39
|
m = RestClient::Payload::Multipart.new([[:bar, "baz"], [:foo, "bar"]])
|
40
40
|
m.to_s.should == <<-EOS
|
41
41
|
--#{m.boundary}\r
|
42
|
-
Content-Disposition:
|
42
|
+
Content-Disposition: form-data; name="bar"\r
|
43
43
|
\r
|
44
44
|
baz\r
|
45
45
|
--#{m.boundary}\r
|
46
|
-
Content-Disposition:
|
46
|
+
Content-Disposition: form-data; name="foo"\r
|
47
47
|
\r
|
48
48
|
bar\r
|
49
49
|
--#{m.boundary}--\r
|
@@ -55,7 +55,7 @@ bar\r
|
|
55
55
|
m = RestClient::Payload::Multipart.new({:foo => f})
|
56
56
|
m.to_s.should == <<-EOS
|
57
57
|
--#{m.boundary}\r
|
58
|
-
Content-Disposition:
|
58
|
+
Content-Disposition: form-data; name="foo"; filename="master_shake.jpg"\r
|
59
59
|
Content-Type: image/jpeg\r
|
60
60
|
\r
|
61
61
|
#{IO.read(f.path)}\r
|
@@ -70,7 +70,7 @@ Content-Type: image/jpeg\r
|
|
70
70
|
m = RestClient::Payload::Multipart.new({:foo => f})
|
71
71
|
m.to_s.should == <<-EOS
|
72
72
|
--#{m.boundary}\r
|
73
|
-
Content-Disposition:
|
73
|
+
Content-Disposition: form-data; name="foo"; filename="foo.txt"\r
|
74
74
|
Content-Type: text/plain\r
|
75
75
|
\r
|
76
76
|
#{IO.read(f.path)}\r
|
@@ -82,7 +82,7 @@ Content-Type: text/plain\r
|
|
82
82
|
m = RestClient::Payload::Multipart.new({:bar => {:baz => "foo"}})
|
83
83
|
m.to_s.should == <<-EOS
|
84
84
|
--#{m.boundary}\r
|
85
|
-
Content-Disposition:
|
85
|
+
Content-Disposition: form-data; name="bar[baz]"\r
|
86
86
|
\r
|
87
87
|
foo\r
|
88
88
|
--#{m.boundary}--\r
|
@@ -94,7 +94,7 @@ foo\r
|
|
94
94
|
m = RestClient::Payload::Multipart.new({:foo => {:bar => f}})
|
95
95
|
m.to_s.should == <<-EOS
|
96
96
|
--#{m.boundary}\r
|
97
|
-
Content-Disposition:
|
97
|
+
Content-Disposition: form-data; name="foo[bar]"; filename="foo.txt"\r
|
98
98
|
Content-Type: text/plain\r
|
99
99
|
\r
|
100
100
|
#{IO.read(f.path)}\r
|