rest-client 0.4 → 0.5
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/Rakefile +3 -2
- data/lib/request_errors.rb +55 -0
- data/lib/resource.rb +43 -0
- data/lib/rest_client.rb +22 -20
- data/spec/request_errors_spec.rb +41 -0
- data/spec/resource_spec.rb +21 -0
- data/spec/rest_client_spec.rb +55 -12
- metadata +6 -4
data/Rakefile
CHANGED
@@ -3,6 +3,7 @@ require 'spec/rake/spectask'
|
|
3
3
|
|
4
4
|
desc "Run all specs"
|
5
5
|
Spec::Rake::SpecTask.new('spec') do |t|
|
6
|
+
t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
|
6
7
|
t.spec_files = FileList['spec/*_spec.rb']
|
7
8
|
end
|
8
9
|
|
@@ -30,14 +31,14 @@ require 'rake/gempackagetask'
|
|
30
31
|
require 'rake/rdoctask'
|
31
32
|
require 'fileutils'
|
32
33
|
|
33
|
-
version = "0.
|
34
|
+
version = "0.5"
|
34
35
|
name = "rest-client"
|
35
36
|
|
36
37
|
spec = Gem::Specification.new do |s|
|
37
38
|
s.name = name
|
38
39
|
s.version = version
|
39
40
|
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
|
40
|
-
s.description = "A simple REST client for Ruby, inspired by the microframework
|
41
|
+
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
41
42
|
s.author = "Adam Wiggins"
|
42
43
|
s.email = "adam@heroku.com"
|
43
44
|
s.homepage = "http://rest-client.heroku.com/"
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
module RestClient
|
4
|
+
# A redirect was encountered; caught by execute to retry with the new url.
|
5
|
+
class Redirect < RuntimeError; end
|
6
|
+
|
7
|
+
# Authorization is required to access the resource specified.
|
8
|
+
class Unauthorized < RuntimeError; end
|
9
|
+
|
10
|
+
# The server broke the connection prior to the request completing.
|
11
|
+
class ServerBrokeConnection < RuntimeError; end
|
12
|
+
|
13
|
+
# The server took too long to respond.
|
14
|
+
class RequestTimeout < RuntimeError; end
|
15
|
+
|
16
|
+
# The request failed, meaning the remote HTTP server returned a code other
|
17
|
+
# than success, unauthorized, or redirect.
|
18
|
+
#
|
19
|
+
# The exception message attempts to extract the error from the XML, using
|
20
|
+
# format returned by Rails: <errors><error>some message</error></errors>
|
21
|
+
#
|
22
|
+
# You can get the status code by e.http_code, or see anything about the
|
23
|
+
# response via e.response. For example, the entire result body (which is
|
24
|
+
# probably an HTML error page) is e.response.body.
|
25
|
+
class RequestFailed < RuntimeError
|
26
|
+
attr_accessor :response
|
27
|
+
|
28
|
+
def initialize(response)
|
29
|
+
@response = response
|
30
|
+
end
|
31
|
+
|
32
|
+
def http_code
|
33
|
+
@response.code.to_i
|
34
|
+
end
|
35
|
+
|
36
|
+
def message(default="Unknown error, HTTP status code #{http_code}")
|
37
|
+
return "Resource not found" if http_code == 404
|
38
|
+
parse_error_xml rescue default
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_error_xml
|
42
|
+
xml_errors = REXML::Document.new(@response.body).elements.to_a("//errors/error")
|
43
|
+
xml_errors.empty? ? raise : xml_errors.map { |a| a.text }.join(" / ")
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
message
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# backwards compatibility
|
53
|
+
RestClient::Resource::Redirect = RestClient::Redirect
|
54
|
+
RestClient::Resource::Unauthorized = RestClient::Unauthorized
|
55
|
+
RestClient::Resource::RequestFailed = RestClient::RequestFailed
|
data/lib/resource.rb
CHANGED
@@ -12,6 +12,11 @@ module RestClient
|
|
12
12
|
# resource = RestClient::Resource.new('http://protected/resource', 'user', 'pass')
|
13
13
|
# resource.delete
|
14
14
|
#
|
15
|
+
# Use the [] syntax to allocate subresources:
|
16
|
+
#
|
17
|
+
# site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
|
18
|
+
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
19
|
+
#
|
15
20
|
class Resource
|
16
21
|
attr_reader :url, :user, :password
|
17
22
|
|
@@ -54,5 +59,43 @@ module RestClient
|
|
54
59
|
:password => password,
|
55
60
|
:headers => headers)
|
56
61
|
end
|
62
|
+
|
63
|
+
# Construct a subresource, preserving authentication.
|
64
|
+
#
|
65
|
+
# Example:
|
66
|
+
#
|
67
|
+
# site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
|
68
|
+
# site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
69
|
+
#
|
70
|
+
# This is especially useful if you wish to define your site in one place and
|
71
|
+
# call it in multiple locations:
|
72
|
+
#
|
73
|
+
# def product(id)
|
74
|
+
# RestClient::Resource.new('http://example.com/products/#{id}', 'adam', 'mypasswd')
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# product(123).get
|
78
|
+
# product(123).put params.to_xml
|
79
|
+
# product(123).delete
|
80
|
+
#
|
81
|
+
# Nest resources as far as you want:
|
82
|
+
#
|
83
|
+
# site = RestClient::Resource.new('http://example.com')
|
84
|
+
# posts = site['posts']
|
85
|
+
# first_post = posts['1']
|
86
|
+
# comments = first_post['comments']
|
87
|
+
# comments.post 'Hello', :content_type => 'text/plain'
|
88
|
+
#
|
89
|
+
def [](suburl)
|
90
|
+
self.class.new(concat_urls(url, suburl), user, password)
|
91
|
+
end
|
92
|
+
|
93
|
+
def concat_urls(url, suburl) # :nodoc:
|
94
|
+
if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
|
95
|
+
"#{url}#{suburl}"
|
96
|
+
else
|
97
|
+
"#{url}/#{suburl}"
|
98
|
+
end
|
99
|
+
end
|
57
100
|
end
|
58
101
|
end
|
data/lib/rest_client.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'uri'
|
2
|
-
require 'net/
|
2
|
+
require 'net/https'
|
3
3
|
|
4
4
|
require File.dirname(__FILE__) + '/resource'
|
5
|
+
require File.dirname(__FILE__) + '/request_errors'
|
5
6
|
|
6
7
|
# This module's static methods are the entry point for using the REST client.
|
7
8
|
module RestClient
|
@@ -11,9 +12,6 @@ module RestClient
|
|
11
12
|
:headers => headers)
|
12
13
|
end
|
13
14
|
|
14
|
-
# Payload can either be a string or a hash. If it is a hash, your
|
15
|
-
# content-type will be set to www-form-urlencoded and the parameters
|
16
|
-
# converted to a CGI encoded string.
|
17
15
|
def self.post(url, payload, headers={})
|
18
16
|
Request.execute(:method => :post,
|
19
17
|
:url => url,
|
@@ -59,7 +57,7 @@ module RestClient
|
|
59
57
|
end
|
60
58
|
|
61
59
|
def execute_inner
|
62
|
-
uri =
|
60
|
+
uri = parse_url_with_auth(url)
|
63
61
|
transmit uri, net_http_class(method).new(uri.request_uri, make_headers(headers)), payload
|
64
62
|
end
|
65
63
|
|
@@ -81,30 +79,38 @@ module RestClient
|
|
81
79
|
URI.parse(url)
|
82
80
|
end
|
83
81
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# Authorization is required to access the resource specified.
|
91
|
-
class Unauthorized < Exception; end
|
82
|
+
def parse_url_with_auth(url)
|
83
|
+
uri = parse_url(url)
|
84
|
+
@user = uri.user if uri.user
|
85
|
+
@password = uri.password if uri.password
|
86
|
+
uri
|
87
|
+
end
|
92
88
|
|
93
89
|
def process_payload(p=nil)
|
94
90
|
unless p.is_a?(Hash)
|
95
91
|
p
|
96
92
|
else
|
97
93
|
@headers[:content_type] = 'application/x-www-form-urlencoded'
|
98
|
-
p.keys.map
|
94
|
+
p.keys.map do |k|
|
95
|
+
v = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
96
|
+
"#{k}=#{v}"
|
97
|
+
end.join("&")
|
99
98
|
end
|
100
99
|
end
|
101
100
|
|
102
101
|
def transmit(uri, req, payload)
|
103
102
|
setup_credentials(req)
|
104
103
|
|
105
|
-
Net::HTTP.
|
104
|
+
net = Net::HTTP.new(uri.host, uri.port)
|
105
|
+
net.use_ssl = uri.is_a?(URI::HTTPS)
|
106
|
+
|
107
|
+
net.start do |http|
|
106
108
|
process_result http.request(req, payload || "")
|
107
109
|
end
|
110
|
+
rescue EOFError
|
111
|
+
raise RestClient::ServerBrokeConnection
|
112
|
+
rescue Timeout::Error
|
113
|
+
raise RestClient::RequestTimeout
|
108
114
|
end
|
109
115
|
|
110
116
|
def setup_credentials(req)
|
@@ -119,14 +125,10 @@ module RestClient
|
|
119
125
|
elsif res.code == "401"
|
120
126
|
raise Unauthorized
|
121
127
|
else
|
122
|
-
raise RequestFailed,
|
128
|
+
raise RequestFailed, res
|
123
129
|
end
|
124
130
|
end
|
125
131
|
|
126
|
-
def error_message(res)
|
127
|
-
"HTTP code #{res.code}: #{res.body}"
|
128
|
-
end
|
129
|
-
|
130
132
|
def default_headers
|
131
133
|
{ :accept => 'application/xml' }
|
132
134
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe RestClient::RequestFailed do
|
4
|
+
before do
|
5
|
+
@error = RestClient::RequestFailed.new(nil)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "extracts the error message from xml" do
|
9
|
+
@error.response = mock('response', :code => '422', :body => '<errors><error>Error 1</error><error>Error 2</error></errors>')
|
10
|
+
@error.message.should == 'Error 1 / Error 2'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "ignores responses without xml since they might contain sensitive data" do
|
14
|
+
@error.response = mock('response', :code => '500', :body => 'Syntax error in SQL query: SELECT * FROM ...')
|
15
|
+
@error.message.should == 'Unknown error, HTTP status code 500'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "accepts a default error message" do
|
19
|
+
@error.response = mock('response', :code => '500', :body => 'Internal Server Error')
|
20
|
+
@error.message('Custom default message').should == 'Custom default message'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "doesn't show the default error message when there's something in the xml" do
|
24
|
+
@error.response = mock('response', :code => '422', :body => '<errors><error>Specific error message</error></errors>')
|
25
|
+
@error.message('Custom default message').should == 'Specific error message'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "backwards compatibility" do
|
30
|
+
it "alias RestClient::Resource::Redirect to RestClient::Redirect" do
|
31
|
+
RestClient::Resource::Redirect.should == RestClient::Redirect
|
32
|
+
end
|
33
|
+
|
34
|
+
it "alias RestClient::Resource::Unauthorized to RestClient::Unauthorized" do
|
35
|
+
RestClient::Resource::Unauthorized.should == RestClient::Unauthorized
|
36
|
+
end
|
37
|
+
|
38
|
+
it "alias RestClient::Resource::RequestFailed to RestClient::RequestFailed" do
|
39
|
+
RestClient::Resource::RequestFailed.should == RestClient::RequestFailed
|
40
|
+
end
|
41
|
+
end
|
data/spec/resource_spec.rb
CHANGED
@@ -28,4 +28,25 @@ describe RestClient::Resource do
|
|
28
28
|
it "can instantiate with no user/password" do
|
29
29
|
@resource = RestClient::Resource.new('http://some/resource')
|
30
30
|
end
|
31
|
+
|
32
|
+
it "concatinates urls, inserting a slash when it needs one" do
|
33
|
+
@resource.concat_urls('http://example.com', 'resource').should == 'http://example.com/resource'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "concatinates urls, using no slash if the first url ends with a slash" do
|
37
|
+
@resource.concat_urls('http://example.com/', 'resource').should == 'http://example.com/resource'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "concatinates urls, using no slash if the second url starts with a slash" do
|
41
|
+
@resource.concat_urls('http://example.com', '/resource').should == 'http://example.com/resource'
|
42
|
+
end
|
43
|
+
|
44
|
+
it "concatinates even non-string urls (i.e. 'posts' + 1)" do
|
45
|
+
@resource.concat_urls('posts/', 1).should == 'posts/1'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "offers subresources via []" do
|
49
|
+
parent = RestClient::Resource.new('http://example.com')
|
50
|
+
parent['posts'].url.should == 'http://example.com/posts'
|
51
|
+
end
|
31
52
|
end
|
data/spec/rest_client_spec.rb
CHANGED
@@ -31,6 +31,12 @@ describe RestClient do
|
|
31
31
|
@uri.stub!(:request_uri).and_return('/resource')
|
32
32
|
@uri.stub!(:host).and_return('some')
|
33
33
|
@uri.stub!(:port).and_return(80)
|
34
|
+
|
35
|
+
@net = mock("net::http base")
|
36
|
+
@http = mock("net::http connection")
|
37
|
+
Net::HTTP.stub!(:new).and_return(@net)
|
38
|
+
@net.stub!(:start).and_yield(@http)
|
39
|
+
@net.stub!(:use_ssl=)
|
34
40
|
end
|
35
41
|
|
36
42
|
it "requests xml mimetype" do
|
@@ -54,6 +60,21 @@ describe RestClient do
|
|
54
60
|
@request.parse_url('example.com/resource')
|
55
61
|
end
|
56
62
|
|
63
|
+
it "extracts the username and password when parsing http://user:password@example.com/" do
|
64
|
+
URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1'))
|
65
|
+
@request.parse_url_with_auth('http://joe:pass1@example.com/resource')
|
66
|
+
@request.user.should == 'joe'
|
67
|
+
@request.password.should == 'pass1'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "doesn't overwrite user and password (which may have already been set by the Resource constructor) if there is no user/password in the url" do
|
71
|
+
URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
|
72
|
+
@request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2')
|
73
|
+
@request.parse_url_with_auth('http://example.com/resource')
|
74
|
+
@request.user.should == 'beth'
|
75
|
+
@request.password.should == 'pass2'
|
76
|
+
end
|
77
|
+
|
57
78
|
it "determines the Net::HTTP class to instantiate by the method name" do
|
58
79
|
@request.net_http_class(:put).should == Net::HTTP::Put
|
59
80
|
end
|
@@ -74,7 +95,7 @@ describe RestClient do
|
|
74
95
|
end
|
75
96
|
|
76
97
|
it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
|
77
|
-
@request.should_receive(:
|
98
|
+
@request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
|
78
99
|
klass = mock("net:http class")
|
79
100
|
@request.should_receive(:net_http_class).with(:put).and_return(klass)
|
80
101
|
klass.should_receive(:new).and_return('result')
|
@@ -83,17 +104,21 @@ describe RestClient do
|
|
83
104
|
end
|
84
105
|
|
85
106
|
it "transmits the request with Net::HTTP" do
|
86
|
-
http
|
87
|
-
Net::HTTP.should_receive(:start).and_yield(http)
|
88
|
-
http.should_receive(:request).with('req', 'payload')
|
107
|
+
@http.should_receive(:request).with('req', 'payload')
|
89
108
|
@request.should_receive(:process_result)
|
90
109
|
@request.transmit(@uri, 'req', 'payload')
|
91
110
|
end
|
92
111
|
|
112
|
+
it "uses SSL when the URI refers to a https address" do
|
113
|
+
@uri.stub!(:is_a?).with(URI::HTTPS).and_return(true)
|
114
|
+
@net.should_receive(:use_ssl=).with(true)
|
115
|
+
@http.stub!(:request)
|
116
|
+
@request.stub!(:process_result)
|
117
|
+
@request.transmit(@uri, 'req', 'payload')
|
118
|
+
end
|
119
|
+
|
93
120
|
it "doesn't send nil payloads" do
|
94
|
-
http
|
95
|
-
Net::HTTP.should_receive(:start).and_yield(http)
|
96
|
-
http.should_receive(:request).with('req', '')
|
121
|
+
@http.should_receive(:request).with('req', '')
|
97
122
|
@request.should_receive(:process_result)
|
98
123
|
@request.transmit(@uri, 'req', nil)
|
99
124
|
end
|
@@ -103,7 +128,7 @@ describe RestClient do
|
|
103
128
|
end
|
104
129
|
|
105
130
|
it "converts a hash payload to urlencoded data" do
|
106
|
-
@request.process_payload(:a => 'b c').should == "a=b%20c"
|
131
|
+
@request.process_payload(:a => 'b c+d').should == "a=b%20c%2Bd"
|
107
132
|
end
|
108
133
|
|
109
134
|
it "set urlencoded content_type header on hash payloads" do
|
@@ -112,9 +137,7 @@ describe RestClient do
|
|
112
137
|
end
|
113
138
|
|
114
139
|
it "sets up the credentials prior to the request" do
|
115
|
-
http
|
116
|
-
Net::HTTP.should_receive(:start).and_yield(http)
|
117
|
-
http.stub!(:request)
|
140
|
+
@http.stub!(:request)
|
118
141
|
@request.stub!(:process_result)
|
119
142
|
|
120
143
|
@request.stub!(:user).and_return('joe')
|
@@ -131,7 +154,7 @@ describe RestClient do
|
|
131
154
|
@request.setup_credentials(req)
|
132
155
|
end
|
133
156
|
|
134
|
-
it "
|
157
|
+
it "setup credentials when there's a user" do
|
135
158
|
@request.stub!(:user).and_return('joe')
|
136
159
|
@request.stub!(:password).and_return('mypass')
|
137
160
|
req = mock("request")
|
@@ -139,6 +162,11 @@ describe RestClient do
|
|
139
162
|
@request.setup_credentials(req)
|
140
163
|
end
|
141
164
|
|
165
|
+
it "catches EOFError and shows the more informative ServerBrokeConnection" do
|
166
|
+
@http.stub!(:request).and_raise(EOFError)
|
167
|
+
lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
|
168
|
+
end
|
169
|
+
|
142
170
|
it "execute calls execute_inner" do
|
143
171
|
@request.should_receive(:execute_inner)
|
144
172
|
@request.execute
|
@@ -150,5 +178,20 @@ describe RestClient do
|
|
150
178
|
req.should_receive(:execute)
|
151
179
|
RestClient::Request.execute(1 => 2)
|
152
180
|
end
|
181
|
+
|
182
|
+
it "raises a Redirect with the new location when the response is in the 30x range" do
|
183
|
+
res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' })
|
184
|
+
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect, 'http://new/resource')
|
185
|
+
end
|
186
|
+
|
187
|
+
it "raises Unauthorized when the response is 401" do
|
188
|
+
res = mock('response', :code => '401')
|
189
|
+
lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "raises RequestFailed otherwise" do
|
193
|
+
res = mock('response', :code => '500')
|
194
|
+
lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
|
195
|
+
end
|
153
196
|
end
|
154
197
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rest-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "0.
|
4
|
+
version: "0.5"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Wiggins
|
@@ -9,11 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-06-20 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
16
|
-
description: "A simple REST client for Ruby, inspired by the microframework
|
16
|
+
description: "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
17
17
|
email: adam@heroku.com
|
18
18
|
executables: []
|
19
19
|
|
@@ -25,7 +25,9 @@ files:
|
|
25
25
|
- Rakefile
|
26
26
|
- lib/resource.rb
|
27
27
|
- lib/rest_client.rb
|
28
|
+
- lib/request_errors.rb
|
28
29
|
- spec/resource_spec.rb
|
30
|
+
- spec/request_errors_spec.rb
|
29
31
|
- spec/rest_client_spec.rb
|
30
32
|
- spec/base.rb
|
31
33
|
has_rdoc: true
|
@@ -50,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
52
|
requirements: []
|
51
53
|
|
52
54
|
rubyforge_project: rest-client
|
53
|
-
rubygems_version: 1.
|
55
|
+
rubygems_version: 1.1.1
|
54
56
|
signing_key:
|
55
57
|
specification_version: 2
|
56
58
|
summary: Simple REST client for Ruby, inspired by microframework syntax for specifying actions.
|