rest-client 0.4 → 0.5

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

Potentially problematic release.


This version of rest-client might be problematic. Click here for more details.

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.4"
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 (Camping, Sinatra...) style of specifying actions: get, put, post, delete."
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/http'
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 = parse_url(url)
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
- # A redirect was encountered; caught by execute to retry with the new url.
85
- class Redirect < Exception; end
86
-
87
- # Request failed with an unhandled http error code.
88
- class RequestFailed < Exception; end
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 { |k| "#{k}=#{URI.escape(p[k].to_s)}" }.join("&")
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.start(uri.host, uri.port) do |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, error_message(res)
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
@@ -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
@@ -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(:parse_url).with('http://some/resource').and_return(@uri)
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 = mock("net::http connection")
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 = mock("net::http connection")
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 = mock("net::http connection")
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 "does not attempt to send any credentials if user is nil" do
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"
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-04-04 00:00:00 -07:00
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 (Camping, Sinatra...) style of specifying actions: get, put, post, delete."
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.0.0
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.