davetron5000-rest-client 0.5.3
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.
- data/README.rdoc +63 -0
- data/Rakefile +63 -0
- data/lib/request_errors.rb +57 -0
- data/lib/resource.rb +103 -0
- data/lib/rest_client.rb +142 -0
- data/rest-client.gemspec +16 -0
- data/spec/base.rb +4 -0
- data/spec/request_errors_spec.rb +27 -0
- data/spec/resource_spec.rb +52 -0
- data/spec/rest_client_spec.rb +202 -0
- metadata +62 -0
data/README.rdoc
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
= REST Client -- simple DSL for accessing REST resources
|
2
|
+
|
3
|
+
A simple REST client for Ruby, inspired by the Sinatra's microframework style
|
4
|
+
of specifying actions: get, put, post, delete.
|
5
|
+
|
6
|
+
<b>This version is a bit out of date, consider the updated one, linked below</b>
|
7
|
+
|
8
|
+
* RDoc - http://davetron5000.github.com/rest-client
|
9
|
+
* Update version you should use - http://github.com/adamwiggins/rest-client/tree/master
|
10
|
+
|
11
|
+
== Usage: Raw URL
|
12
|
+
|
13
|
+
require 'rest_client'
|
14
|
+
|
15
|
+
xml = RestClient.get 'http://example.com/resource'
|
16
|
+
jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
|
17
|
+
|
18
|
+
private_resource = RestClient.get 'https://user:password@example.com/private/resource'
|
19
|
+
|
20
|
+
RestClient.put 'http://example.com/resource', File.read('my.pdf'), :content_type => 'application/pdf'
|
21
|
+
|
22
|
+
RestClient.post 'http://example.com/resource', xml, :content_type => 'application/xml'
|
23
|
+
|
24
|
+
RestClient.delete 'http://example.com/resource'
|
25
|
+
|
26
|
+
See RestClient module docs for details.
|
27
|
+
|
28
|
+
== Usage: ActiveResource-Style
|
29
|
+
|
30
|
+
resource = RestClient::Resource.new 'http://example.com/resource'
|
31
|
+
resource.get
|
32
|
+
|
33
|
+
private_resource = RestClient::Resource.new 'http://example.com/private/resource', 'user', 'pass'
|
34
|
+
private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
|
35
|
+
|
36
|
+
See RestClient::Resource module docs for details.
|
37
|
+
|
38
|
+
== Usage: Resource Nesting
|
39
|
+
|
40
|
+
site = RestClient::Resource.new('http://example.com')
|
41
|
+
site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
|
42
|
+
|
43
|
+
See RestClient::Resource docs for details.
|
44
|
+
|
45
|
+
== Shell
|
46
|
+
|
47
|
+
Require rest_client from within irb to access RestClient interactively, like
|
48
|
+
using curl at the command line. Better yet, require gem from within your
|
49
|
+
~/.rush/env.rb and have instant access to it from within your rush
|
50
|
+
(http://rush.heroku.com) sessions.
|
51
|
+
|
52
|
+
== Meta
|
53
|
+
|
54
|
+
Written by Adam Wiggins (adam at heroku dot com)
|
55
|
+
|
56
|
+
Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, and Rafael Souza
|
57
|
+
|
58
|
+
Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
|
59
|
+
|
60
|
+
http://rest-client.heroku.com
|
61
|
+
|
62
|
+
http://github.com/adamwiggins/rest-client
|
63
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'hanna/rdoctask'
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/clean'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'rake/rdoctask'
|
7
|
+
require 'rake/testtask'
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
|
10
|
+
$: << '../grancher/lib'
|
11
|
+
require 'grancher/task'
|
12
|
+
|
13
|
+
Grancher::Task.new do |g|
|
14
|
+
g.branch = 'gh-pages'
|
15
|
+
g.push_to = 'origin'
|
16
|
+
g.directory 'html'
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Run all specs"
|
20
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
21
|
+
t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
|
22
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Print specdocs"
|
26
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
27
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
28
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
desc "Run all examples with RCov"
|
32
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
33
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
34
|
+
t.rcov = true
|
35
|
+
t.rcov_opts = ['--exclude', 'examples']
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
spec = eval(File.read('rest-client.gemspec'))
|
40
|
+
|
41
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
42
|
+
pkg.need_tar = true
|
43
|
+
end
|
44
|
+
|
45
|
+
Rake::TestTask.new do |t|
|
46
|
+
t.libs << "spec"
|
47
|
+
t.test_files = FileList['spec/*_spec.rb']
|
48
|
+
t.verbose = true
|
49
|
+
end
|
50
|
+
|
51
|
+
Rake::RDocTask.new do |t|
|
52
|
+
t.title = "rest-client, fetch RESTful resources effortlessly"
|
53
|
+
t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
54
|
+
t.options << '--charset' << 'utf-8'
|
55
|
+
t.rdoc_files.include('README.rdoc')
|
56
|
+
t.rdoc_files.include('lib/*.rb')
|
57
|
+
end
|
58
|
+
|
59
|
+
CLEAN.include [ 'pkg', '*.gem', '.config', 'html' ]
|
60
|
+
|
61
|
+
task :default => :spec
|
62
|
+
|
63
|
+
task :publish_rdoc => [:rdoc,:publish]
|
@@ -0,0 +1,57 @@
|
|
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
|
+
# No resource was found at the given URL.
|
11
|
+
class ResourceNotFound < RuntimeError; end
|
12
|
+
|
13
|
+
# The server broke the connection prior to the request completing.
|
14
|
+
class ServerBrokeConnection < RuntimeError; end
|
15
|
+
|
16
|
+
# The server took too long to respond.
|
17
|
+
class RequestTimeout < RuntimeError; end
|
18
|
+
|
19
|
+
# The request failed, meaning the remote HTTP server returned a code other
|
20
|
+
# than success, unauthorized, or redirect.
|
21
|
+
#
|
22
|
+
# The exception message attempts to extract the error from the XML, using
|
23
|
+
# format returned by Rails: <errors><error>some message</error></errors>
|
24
|
+
#
|
25
|
+
# You can get the status code by e.http_code, or see anything about the
|
26
|
+
# response via e.response. For example, the entire result body (which is
|
27
|
+
# probably an HTML error page) is e.response.body.
|
28
|
+
class RequestFailed < RuntimeError
|
29
|
+
attr_accessor :response
|
30
|
+
|
31
|
+
def initialize(response=nil)
|
32
|
+
@response = response
|
33
|
+
end
|
34
|
+
|
35
|
+
def http_code
|
36
|
+
@response.code.to_i if @response
|
37
|
+
end
|
38
|
+
|
39
|
+
def message(default="Unknown error, HTTP status code #{http_code}")
|
40
|
+
return default unless @response
|
41
|
+
parse_error_xml rescue default
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_error_xml
|
45
|
+
xml_errors = REXML::Document.new(@response.body).elements.to_a("//errors/error")
|
46
|
+
xml_errors.empty? ? raise : xml_errors.map { |a| a.text }.join(" / ")
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# backwards compatibility
|
56
|
+
class RestClient::Request
|
57
|
+
end
|
data/lib/resource.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module RestClient
|
2
|
+
# A class that can be instantiated for access to a RESTful resource,
|
3
|
+
# including authentication.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# resource = RestClient::Resource.new('http://some/resource')
|
8
|
+
# jpg = resource.get(:accept => 'image/jpg')
|
9
|
+
#
|
10
|
+
# With HTTP basic authentication:
|
11
|
+
#
|
12
|
+
# resource = RestClient::Resource.new('http://protected/resource', 'user', 'pass')
|
13
|
+
# resource.delete
|
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
|
+
#
|
20
|
+
class Resource
|
21
|
+
attr_reader :url, :user, :password
|
22
|
+
|
23
|
+
def initialize(url, user=nil, password=nil)
|
24
|
+
@url = url
|
25
|
+
@user = user
|
26
|
+
@password = password
|
27
|
+
end
|
28
|
+
|
29
|
+
def get(headers={})
|
30
|
+
Request.execute(:method => :get,
|
31
|
+
:url => url,
|
32
|
+
:user => user,
|
33
|
+
:password => password,
|
34
|
+
:headers => headers)
|
35
|
+
end
|
36
|
+
|
37
|
+
def post(payload, headers={})
|
38
|
+
Request.execute(:method => :post,
|
39
|
+
:url => url,
|
40
|
+
:payload => payload,
|
41
|
+
:user => user,
|
42
|
+
:password => password,
|
43
|
+
:headers => headers)
|
44
|
+
end
|
45
|
+
|
46
|
+
def put(payload, headers={})
|
47
|
+
Request.execute(:method => :put,
|
48
|
+
:url => url,
|
49
|
+
:payload => payload,
|
50
|
+
:user => user,
|
51
|
+
:password => password,
|
52
|
+
:headers => headers)
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete(headers={})
|
56
|
+
Request.execute(:method => :delete,
|
57
|
+
:url => url,
|
58
|
+
:user => user,
|
59
|
+
:password => password,
|
60
|
+
:headers => headers)
|
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 orders
|
74
|
+
# RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# orders.get # GET http://example.com/orders
|
78
|
+
# orders['1'].get # GET http://example.com/orders/1
|
79
|
+
# orders['1/items'].delete # DELETE http://example.com/orders/1/items
|
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
|
+
url = url.to_s
|
95
|
+
suburl = suburl.to_s
|
96
|
+
if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
|
97
|
+
url + suburl
|
98
|
+
else
|
99
|
+
"#{url}/#{suburl}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/rest_client.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + '/resource'
|
5
|
+
require File.dirname(__FILE__) + '/request_errors'
|
6
|
+
|
7
|
+
# This module's static methods are the entry point for using the REST client.
|
8
|
+
module RestClient
|
9
|
+
def self.get(url, headers={})
|
10
|
+
Request.execute(:method => :get,
|
11
|
+
:url => url,
|
12
|
+
:headers => headers)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.post(url, payload, headers={})
|
16
|
+
Request.execute(:method => :post,
|
17
|
+
:url => url,
|
18
|
+
:payload => payload,
|
19
|
+
:headers => headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.put(url, payload, headers={})
|
23
|
+
Request.execute(:method => :put,
|
24
|
+
:url => url,
|
25
|
+
:payload => payload,
|
26
|
+
:headers => headers)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.delete(url, headers={})
|
30
|
+
Request.execute(:method => :delete,
|
31
|
+
:url => url,
|
32
|
+
:headers => headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Internal class used to build and execute the request.
|
36
|
+
class Request
|
37
|
+
class << self
|
38
|
+
attr_accessor :default_headers, :timeout_threshold
|
39
|
+
end
|
40
|
+
|
41
|
+
self.default_headers = { :accept => 'application/xml' }
|
42
|
+
|
43
|
+
# Set a custom open and read timeout for Net::HTTP instances.
|
44
|
+
self.timeout_threshold = nil
|
45
|
+
|
46
|
+
attr_reader :method, :url, :payload, :headers, :user, :password
|
47
|
+
|
48
|
+
def self.execute(args)
|
49
|
+
new(args).execute
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(args)
|
53
|
+
@method = args[:method] or raise ArgumentError, "must pass :method"
|
54
|
+
@url = args[:url] or raise ArgumentError, "must pass :url"
|
55
|
+
@headers = args[:headers] || {}
|
56
|
+
@payload = process_payload(args[:payload])
|
57
|
+
@user = args[:user]
|
58
|
+
@password = args[:password]
|
59
|
+
end
|
60
|
+
|
61
|
+
def execute
|
62
|
+
execute_inner
|
63
|
+
rescue Redirect => e
|
64
|
+
@url = e.message
|
65
|
+
execute
|
66
|
+
end
|
67
|
+
|
68
|
+
def execute_inner
|
69
|
+
uri = parse_url_with_auth(url)
|
70
|
+
transmit uri, net_http_class(method).new(uri.request_uri, make_headers(headers)), payload
|
71
|
+
end
|
72
|
+
|
73
|
+
def make_headers(user_headers)
|
74
|
+
merged = self.class.default_headers.merge(user_headers)
|
75
|
+
merged.inject({}) do |final, (key, value)|
|
76
|
+
final.update(key.to_s.gsub(/_/, '-').capitalize => value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def net_http_class(method)
|
81
|
+
Net::HTTP.const_get(method.to_s.capitalize)
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_url(url)
|
85
|
+
url = "http://#{url}" unless url.match(/^http/)
|
86
|
+
URI.parse(url)
|
87
|
+
end
|
88
|
+
|
89
|
+
def parse_url_with_auth(url)
|
90
|
+
uri = parse_url(url)
|
91
|
+
@user = uri.user if uri.user
|
92
|
+
@password = uri.password if uri.password
|
93
|
+
uri
|
94
|
+
end
|
95
|
+
|
96
|
+
def process_payload(p=nil)
|
97
|
+
unless p.is_a?(Hash)
|
98
|
+
p
|
99
|
+
else
|
100
|
+
@headers[:content_type] = 'application/x-www-form-urlencoded'
|
101
|
+
p.keys.map do |k|
|
102
|
+
v = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
103
|
+
"#{k}=#{v}"
|
104
|
+
end.join("&")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def transmit(uri, req, payload)
|
109
|
+
setup_credentials(req)
|
110
|
+
|
111
|
+
net = Net::HTTP.new(uri.host, uri.port)
|
112
|
+
net.read_timeout = net.open_timeout = self.class.timeout_threshold if self.class.timeout_threshold
|
113
|
+
net.use_ssl = uri.is_a?(URI::HTTPS)
|
114
|
+
|
115
|
+
net.start do |http|
|
116
|
+
process_result http.request(req, payload || "")
|
117
|
+
end
|
118
|
+
rescue EOFError
|
119
|
+
raise RestClient::ServerBrokeConnection
|
120
|
+
rescue Timeout::Error
|
121
|
+
raise RestClient::RequestTimeout
|
122
|
+
end
|
123
|
+
|
124
|
+
def setup_credentials(req)
|
125
|
+
req.basic_auth(user, password) if user
|
126
|
+
end
|
127
|
+
|
128
|
+
def process_result(res)
|
129
|
+
if %w(200 201 202).include? res.code
|
130
|
+
res.body
|
131
|
+
elsif %w(301 302 303).include? res.code
|
132
|
+
raise Redirect, res.header['Location']
|
133
|
+
elsif res.code == "401"
|
134
|
+
raise Unauthorized
|
135
|
+
elsif res.code == "404"
|
136
|
+
raise ResourceNotFound
|
137
|
+
else
|
138
|
+
raise RequestFailed, res
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/rest-client.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "rest-client"
|
3
|
+
s.version = "0.5.3"
|
4
|
+
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
|
5
|
+
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
6
|
+
s.author = "Adam Wiggins"
|
7
|
+
s.email = "adam@heroku.com"
|
8
|
+
s.rubyforge_project = "rest-client"
|
9
|
+
s.homepage = "http://rest-client.heroku.com/"
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.files = %w(Rakefile README.rdoc rest-client.gemspec
|
13
|
+
lib/request_errors.rb lib/resource.rb lib/rest_client.rb
|
14
|
+
spec/base.rb spec/request_errors_spec.rb spec/resource_spec.rb spec/rest_client_spec.rb)
|
15
|
+
s.require_path = "lib"
|
16
|
+
end
|
data/spec/base.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe RestClient::RequestFailed do
|
4
|
+
before do
|
5
|
+
@error = RestClient::RequestFailed.new
|
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
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe RestClient::Resource do
|
4
|
+
before do
|
5
|
+
@resource = RestClient::Resource.new('http://some/resource', 'jane', 'mypass')
|
6
|
+
end
|
7
|
+
|
8
|
+
it "GET" do
|
9
|
+
RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}, :user => 'jane', :password => 'mypass')
|
10
|
+
@resource.get
|
11
|
+
end
|
12
|
+
|
13
|
+
it "POST" do
|
14
|
+
RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => { :content_type => 'image/jpg' }, :user => 'jane', :password => 'mypass')
|
15
|
+
@resource.post 'abc', :content_type => 'image/jpg'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "PUT" do
|
19
|
+
RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => { :content_type => 'image/jpg' }, :user => 'jane', :password => 'mypass')
|
20
|
+
@resource.put 'abc', :content_type => 'image/jpg'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "DELETE" do
|
24
|
+
RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}, :user => 'jane', :password => 'mypass')
|
25
|
+
@resource.delete
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can instantiate with no user/password" do
|
29
|
+
@resource = RestClient::Resource.new('http://some/resource')
|
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, :posts + 1 => '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
|
52
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
describe RestClient do
|
4
|
+
context "public API" do
|
5
|
+
it "GET" do
|
6
|
+
RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
|
7
|
+
RestClient.get('http://some/resource')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "POST" do
|
11
|
+
RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
|
12
|
+
RestClient.post('http://some/resource', 'payload')
|
13
|
+
end
|
14
|
+
|
15
|
+
it "PUT" do
|
16
|
+
RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
|
17
|
+
RestClient.put('http://some/resource', 'payload')
|
18
|
+
end
|
19
|
+
|
20
|
+
it "DELETE" do
|
21
|
+
RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
|
22
|
+
RestClient.delete('http://some/resource')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context RestClient::Request do
|
27
|
+
before do
|
28
|
+
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
|
29
|
+
|
30
|
+
@uri = mock("uri")
|
31
|
+
@uri.stub!(:request_uri).and_return('/resource')
|
32
|
+
@uri.stub!(:host).and_return('some')
|
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=)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "requests xml mimetype" do
|
43
|
+
RestClient::Request.default_headers[:accept].should == 'application/xml'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "processes a successful result" do
|
47
|
+
res = mock("result")
|
48
|
+
res.stub!(:code).and_return("200")
|
49
|
+
res.stub!(:body).and_return('body')
|
50
|
+
@request.process_result(res).should == 'body'
|
51
|
+
end
|
52
|
+
|
53
|
+
it "parses a url into a URI object" do
|
54
|
+
URI.should_receive(:parse).with('http://example.com/resource')
|
55
|
+
@request.parse_url('http://example.com/resource')
|
56
|
+
end
|
57
|
+
|
58
|
+
it "adds http:// to the front of resources specified in the syntax example.com/resource" do
|
59
|
+
URI.should_receive(:parse).with('http://example.com/resource')
|
60
|
+
@request.parse_url('example.com/resource')
|
61
|
+
end
|
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
|
+
|
78
|
+
it "determines the Net::HTTP class to instantiate by the method name" do
|
79
|
+
@request.net_http_class(:put).should == Net::HTTP::Put
|
80
|
+
end
|
81
|
+
|
82
|
+
it "merges user headers with the default headers" do
|
83
|
+
RestClient::Request.should_receive(:default_headers).and_return({ '1' => '2' })
|
84
|
+
@request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' }
|
85
|
+
end
|
86
|
+
|
87
|
+
it "prefers the user header when the same header exists in the defaults" do
|
88
|
+
RestClient::Request.should_receive(:default_headers).and_return({ '1' => '2' })
|
89
|
+
@request.make_headers('1' => '3').should == { '1' => '3' }
|
90
|
+
end
|
91
|
+
|
92
|
+
it "converts header symbols from :content_type to 'Content-type'" do
|
93
|
+
RestClient::Request.should_receive(:default_headers).and_return({})
|
94
|
+
@request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' }
|
95
|
+
end
|
96
|
+
|
97
|
+
it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
|
98
|
+
@request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
|
99
|
+
klass = mock("net:http class")
|
100
|
+
@request.should_receive(:net_http_class).with(:put).and_return(klass)
|
101
|
+
klass.should_receive(:new).and_return('result')
|
102
|
+
@request.should_receive(:transmit).with(@uri, 'result', 'payload')
|
103
|
+
@request.execute_inner
|
104
|
+
end
|
105
|
+
|
106
|
+
it "transmits the request with Net::HTTP" do
|
107
|
+
@http.should_receive(:request).with('req', 'payload')
|
108
|
+
@request.should_receive(:process_result)
|
109
|
+
@request.transmit(@uri, 'req', 'payload')
|
110
|
+
end
|
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
|
+
|
120
|
+
it "doesn't send nil payloads" do
|
121
|
+
@http.should_receive(:request).with('req', '')
|
122
|
+
@request.should_receive(:process_result)
|
123
|
+
@request.transmit(@uri, 'req', nil)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "passes non-hash payloads straight through" do
|
127
|
+
@request.process_payload("x").should == "x"
|
128
|
+
end
|
129
|
+
|
130
|
+
it "converts a hash payload to urlencoded data" do
|
131
|
+
@request.process_payload(:a => 'b c+d').should == "a=b%20c%2Bd"
|
132
|
+
end
|
133
|
+
|
134
|
+
it "set urlencoded content_type header on hash payloads" do
|
135
|
+
@request.process_payload(:a => 1)
|
136
|
+
@request.headers[:content_type].should == 'application/x-www-form-urlencoded'
|
137
|
+
end
|
138
|
+
|
139
|
+
it "sets up the credentials prior to the request" do
|
140
|
+
@http.stub!(:request)
|
141
|
+
@request.stub!(:process_result)
|
142
|
+
|
143
|
+
@request.stub!(:user).and_return('joe')
|
144
|
+
@request.stub!(:password).and_return('mypass')
|
145
|
+
@request.should_receive(:setup_credentials).with('req')
|
146
|
+
|
147
|
+
@request.transmit(@uri, 'req', nil)
|
148
|
+
end
|
149
|
+
|
150
|
+
it "does not attempt to send any credentials if user is nil" do
|
151
|
+
@request.stub!(:user).and_return(nil)
|
152
|
+
req = mock("request")
|
153
|
+
req.should_not_receive(:basic_auth)
|
154
|
+
@request.setup_credentials(req)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "setup credentials when there's a user" do
|
158
|
+
@request.stub!(:user).and_return('joe')
|
159
|
+
@request.stub!(:password).and_return('mypass')
|
160
|
+
req = mock("request")
|
161
|
+
req.should_receive(:basic_auth).with('joe', 'mypass')
|
162
|
+
@request.setup_credentials(req)
|
163
|
+
end
|
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
|
+
|
170
|
+
it "execute calls execute_inner" do
|
171
|
+
@request.should_receive(:execute_inner)
|
172
|
+
@request.execute
|
173
|
+
end
|
174
|
+
|
175
|
+
it "class method execute wraps constructor" do
|
176
|
+
req = mock("rest request")
|
177
|
+
RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
|
178
|
+
req.should_receive(:execute)
|
179
|
+
RestClient::Request.execute(1 => 2)
|
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 ResourceNotFound when the response is 404" do
|
193
|
+
res = mock('response', :code => '404')
|
194
|
+
lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
|
195
|
+
end
|
196
|
+
|
197
|
+
it "raises RequestFailed otherwise" do
|
198
|
+
res = mock('response', :code => '500')
|
199
|
+
lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: davetron5000-rest-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Adam Wiggins
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-05 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
17
|
+
email: adam@heroku.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- Rakefile
|
26
|
+
- README.rdoc
|
27
|
+
- rest-client.gemspec
|
28
|
+
- lib/request_errors.rb
|
29
|
+
- lib/resource.rb
|
30
|
+
- lib/rest_client.rb
|
31
|
+
- spec/base.rb
|
32
|
+
- spec/request_errors_spec.rb
|
33
|
+
- spec/resource_spec.rb
|
34
|
+
- spec/rest_client_spec.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://rest-client.heroku.com/
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
version:
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project: rest-client
|
57
|
+
rubygems_version: 1.2.0
|
58
|
+
signing_key:
|
59
|
+
specification_version: 2
|
60
|
+
summary: Simple REST client for Ruby, inspired by microframework syntax for specifying actions.
|
61
|
+
test_files: []
|
62
|
+
|