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 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.