francois-rest-client 1.1.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.
@@ -0,0 +1,241 @@
1
+ require 'tempfile'
2
+
3
+ module RestClient
4
+ # This class is used internally by RestClient to send the request, but you can also
5
+ # access it internally if you'd like to use a method not directly supported by the
6
+ # main API. For example:
7
+ #
8
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
9
+ #
10
+ class Request
11
+ attr_reader :method, :url, :payload, :headers,
12
+ :cookies, :user, :password, :timeout, :open_timeout,
13
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
14
+ :raw_response
15
+
16
+ def self.execute(args)
17
+ new(args).execute
18
+ end
19
+
20
+ def initialize(args)
21
+ @method = args[:method] or raise ArgumentError, "must pass :method"
22
+ @url = args[:url] or raise ArgumentError, "must pass :url"
23
+ @headers = args[:headers] || {}
24
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
25
+ @payload = Payload.generate(args[:payload])
26
+ @user = args[:user]
27
+ @password = args[:password]
28
+ @timeout = args[:timeout]
29
+ @open_timeout = args[:open_timeout]
30
+ @raw_response = args[:raw_response] || false
31
+ @verify_ssl = args[:verify_ssl] || false
32
+ @ssl_client_cert = args[:ssl_client_cert] || nil
33
+ @ssl_client_key = args[:ssl_client_key] || nil
34
+ @ssl_ca_file = args[:ssl_ca_file] || nil
35
+ @tf = nil # If you are a raw request, this is your tempfile
36
+ end
37
+
38
+ def execute
39
+ execute_inner
40
+ rescue Redirect => e
41
+ @url = e.url
42
+ @method = :get
43
+ @payload = nil
44
+ execute
45
+ end
46
+
47
+ def execute_inner
48
+ uri = parse_url_with_auth(url)
49
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
50
+ end
51
+
52
+ def make_headers(user_headers)
53
+ unless @cookies.empty?
54
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
55
+ end
56
+
57
+ headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
58
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
59
+ final
60
+ end
61
+
62
+ headers.merge!(@payload.headers) if @payload
63
+ headers
64
+ end
65
+
66
+ def net_http_class
67
+ if RestClient.proxy
68
+ proxy_uri = URI.parse(RestClient.proxy)
69
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
70
+ else
71
+ Net::HTTP
72
+ end
73
+ end
74
+
75
+ def net_http_request_class(method)
76
+ Net::HTTP.const_get(method.to_s.capitalize)
77
+ end
78
+
79
+ def parse_url(url)
80
+ url = "http://#{url}" unless url.match(/^http/)
81
+ URI.parse(url)
82
+ end
83
+
84
+ def parse_url_with_auth(url)
85
+ uri = parse_url(url)
86
+ @user = uri.user if uri.user
87
+ @password = uri.password if uri.password
88
+ uri
89
+ end
90
+
91
+ def process_payload(p=nil, parent_key=nil)
92
+ unless p.is_a?(Hash)
93
+ p
94
+ else
95
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
96
+ p.keys.map do |k|
97
+ key = parent_key ? "#{parent_key}[#{k}]" : k
98
+ if p[k].is_a? Hash
99
+ process_payload(p[k], key)
100
+ else
101
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
102
+ "#{key}=#{value}"
103
+ end
104
+ end.join("&")
105
+ end
106
+ end
107
+
108
+ def transmit(uri, req, payload)
109
+ setup_credentials(req)
110
+
111
+ net = net_http_class.new(uri.host, uri.port)
112
+ net.use_ssl = uri.is_a?(URI::HTTPS)
113
+ if @verify_ssl == false
114
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
115
+ elsif @verify_ssl.is_a? Integer
116
+ net.verify_mode = @verify_ssl
117
+ end
118
+ net.cert = @ssl_client_cert if @ssl_client_cert
119
+ net.key = @ssl_client_key if @ssl_client_key
120
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
121
+ net.read_timeout = @timeout if @timeout
122
+ net.open_timeout = @open_timeout if @open_timeout
123
+
124
+ display_log request_log
125
+
126
+ net.start do |http|
127
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
128
+ result = process_result(res)
129
+ display_log response_log(res)
130
+
131
+ if result.kind_of?(String) or @method == :head
132
+ Response.new(result, res)
133
+ elsif @raw_response
134
+ RawResponse.new(@tf, res)
135
+ else
136
+ nil
137
+ end
138
+ end
139
+ rescue EOFError
140
+ raise RestClient::ServerBrokeConnection
141
+ rescue Timeout::Error
142
+ raise RestClient::RequestTimeout
143
+ end
144
+
145
+ def setup_credentials(req)
146
+ req.basic_auth(user, password) if user
147
+ end
148
+
149
+ def fetch_body(http_response)
150
+ if @raw_response
151
+ # Taken from Chef, which as in turn...
152
+ # Stolen from http://www.ruby-forum.com/topic/166423
153
+ # Kudos to _why!
154
+ @tf = Tempfile.new("rest-client")
155
+ size, total = 0, http_response.header['Content-Length'].to_i
156
+ http_response.read_body do |chunk|
157
+ @tf.write(chunk)
158
+ size += chunk.size
159
+ if size == 0
160
+ display_log("#{@method} #{@url} done (0 length file)")
161
+ elsif total == 0
162
+ display_log("#{@method} #{@url} (zero content length)")
163
+ else
164
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
165
+ end
166
+ end
167
+ @tf.close
168
+ @tf
169
+ else
170
+ http_response.read_body
171
+ end
172
+ http_response
173
+ end
174
+
175
+ def process_result(res)
176
+ if res.code =~ /\A2\d{2}\z/
177
+ # We don't decode raw requests
178
+ unless @raw_response
179
+ self.class.decode res['content-encoding'], res.body if res.body
180
+ end
181
+ elsif %w(301 302 303).include? res.code
182
+ url = res.header['Location']
183
+
184
+ if url !~ /^http/
185
+ uri = URI.parse(@url)
186
+ uri.path = "/#{url}".squeeze('/')
187
+ url = uri.to_s
188
+ end
189
+
190
+ raise Redirect, url
191
+ elsif res.code == "304"
192
+ raise NotModified, res
193
+ elsif res.code == "401"
194
+ raise Unauthorized, res
195
+ elsif res.code == "404"
196
+ raise ResourceNotFound, res
197
+ else
198
+ raise RequestFailed, res
199
+ end
200
+ end
201
+
202
+ def self.decode(content_encoding, body)
203
+ if content_encoding == 'gzip' and not body.empty?
204
+ Zlib::GzipReader.new(StringIO.new(body)).read
205
+ elsif content_encoding == 'deflate'
206
+ Zlib::Inflate.new.inflate(body)
207
+ else
208
+ body
209
+ end
210
+ end
211
+
212
+ def request_log
213
+ out = []
214
+ out << "RestClient.#{method} #{url.inspect}"
215
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
216
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
217
+ out.join(', ')
218
+ end
219
+
220
+ def response_log(res)
221
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
222
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
223
+ end
224
+
225
+ def display_log(msg)
226
+ return unless log_to = RestClient.log
227
+
228
+ if log_to == 'stdout'
229
+ STDOUT.puts msg
230
+ elsif log_to == 'stderr'
231
+ STDERR.puts msg
232
+ else
233
+ File.open(log_to, 'a') { |f| f.puts msg }
234
+ end
235
+ end
236
+
237
+ def default_headers
238
+ { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,146 @@
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 => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = RestClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil)
40
+ @url = url
41
+ if options.class == Hash
42
+ @options = options
43
+ else # compatibility with previous versions
44
+ @options = { :user => options, :password => backwards_compatibility }
45
+ end
46
+ end
47
+
48
+ def get(additional_headers={}, &b)
49
+ headers = (options[:headers] || {}).merge(additional_headers)
50
+ Request.execute(options.merge(
51
+ :method => :get,
52
+ :url => url,
53
+ :headers => headers), &b)
54
+ end
55
+
56
+ def post(payload, additional_headers={}, &b)
57
+ headers = (options[:headers] || {}).merge(additional_headers)
58
+ Request.execute(options.merge(
59
+ :method => :post,
60
+ :url => url,
61
+ :payload => payload,
62
+ :headers => headers), &b)
63
+ end
64
+
65
+ def put(payload, additional_headers={}, &b)
66
+ headers = (options[:headers] || {}).merge(additional_headers)
67
+ Request.execute(options.merge(
68
+ :method => :put,
69
+ :url => url,
70
+ :payload => payload,
71
+ :headers => headers), &b)
72
+ end
73
+
74
+ def delete(additional_headers={}, &b)
75
+ headers = (options[:headers] || {}).merge(additional_headers)
76
+ Request.execute(options.merge(
77
+ :method => :delete,
78
+ :url => url,
79
+ :headers => headers), &b)
80
+ end
81
+
82
+ def to_s
83
+ url
84
+ end
85
+
86
+ def user
87
+ options[:user]
88
+ end
89
+
90
+ def password
91
+ options[:password]
92
+ end
93
+
94
+ def headers
95
+ options[:headers] || {}
96
+ end
97
+
98
+ def timeout
99
+ options[:timeout]
100
+ end
101
+
102
+ def open_timeout
103
+ options[:open_timeout]
104
+ end
105
+
106
+ # Construct a subresource, preserving authentication.
107
+ #
108
+ # Example:
109
+ #
110
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
111
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
112
+ #
113
+ # This is especially useful if you wish to define your site in one place and
114
+ # call it in multiple locations:
115
+ #
116
+ # def orders
117
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
118
+ # end
119
+ #
120
+ # orders.get # GET http://example.com/orders
121
+ # orders['1'].get # GET http://example.com/orders/1
122
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
123
+ #
124
+ # Nest resources as far as you want:
125
+ #
126
+ # site = RestClient::Resource.new('http://example.com')
127
+ # posts = site['posts']
128
+ # first_post = posts['1']
129
+ # comments = first_post['comments']
130
+ # comments.post 'Hello', :content_type => 'text/plain'
131
+ #
132
+ def [](suburl)
133
+ self.class.new(concat_urls(url, suburl), options)
134
+ end
135
+
136
+ def concat_urls(url, suburl) # :nodoc:
137
+ url = url.to_s
138
+ suburl = suburl.to_s
139
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
140
+ url + suburl
141
+ else
142
+ "#{url}/#{suburl}"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/mixin/response'
2
+
3
+ module RestClient
4
+ # The response from RestClient looks like a string, but is actually one of
5
+ # these. 99% of the time you're making a rest call all you care about is
6
+ # the body, but on the occassion you want to fetch the headers you can:
7
+ #
8
+ # RestClient.get('http://example.com').headers[:content_type]
9
+ #
10
+ class Response < String
11
+
12
+ include RestClient::Mixin::Response
13
+
14
+ def initialize(string, net_http_res)
15
+ @net_http_res = net_http_res
16
+ super(string || "")
17
+ end
18
+
19
+ end
20
+ end
data/lib/restclient.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'uri'
2
+ require 'zlib'
3
+ require 'stringio'
4
+
5
+ begin
6
+ require 'net/https'
7
+ rescue LoadError => e
8
+ raise e unless RUBY_PLATFORM =~ /linux/
9
+ raise LoadError, "no such file to load -- net/https. Try running apt-get install libopenssl-ruby"
10
+ end
11
+
12
+ require File.dirname(__FILE__) + '/restclient/request'
13
+ require File.dirname(__FILE__) + '/restclient/mixin/response'
14
+ require File.dirname(__FILE__) + '/restclient/response'
15
+ require File.dirname(__FILE__) + '/restclient/raw_response'
16
+ require File.dirname(__FILE__) + '/restclient/resource'
17
+ require File.dirname(__FILE__) + '/restclient/exceptions'
18
+ require File.dirname(__FILE__) + '/restclient/payload'
19
+ require File.dirname(__FILE__) + '/restclient/net_http_ext'
20
+
21
+ # This module's static methods are the entry point for using the REST client.
22
+ #
23
+ # # GET
24
+ # xml = RestClient.get 'http://example.com/resource'
25
+ # jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
26
+ #
27
+ # # authentication and SSL
28
+ # RestClient.get 'https://user:password@example.com/private/resource'
29
+ #
30
+ # # POST or PUT with a hash sends parameters as a urlencoded form body
31
+ # RestClient.post 'http://example.com/resource', :param1 => 'one'
32
+ #
33
+ # # nest hash parameters
34
+ # RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
35
+ #
36
+ # # POST and PUT with raw payloads
37
+ # RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
38
+ # RestClient.post 'http://example.com/resource.xml', xml_doc
39
+ # RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
40
+ #
41
+ # # DELETE
42
+ # RestClient.delete 'http://example.com/resource'
43
+ #
44
+ # # retreive the response http code and headers
45
+ # res = RestClient.get 'http://example.com/some.jpg'
46
+ # res.code # => 200
47
+ # res.headers[:content_type] # => 'image/jpg'
48
+ #
49
+ # # HEAD
50
+ # RestClient.head('http://example.com').headers
51
+ #
52
+ # To use with a proxy, just set RestClient.proxy to the proper http proxy:
53
+ #
54
+ # RestClient.proxy = "http://proxy.example.com/"
55
+ #
56
+ # Or inherit the proxy from the environment:
57
+ #
58
+ # RestClient.proxy = ENV['http_proxy']
59
+ #
60
+ # For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
61
+ #
62
+ # >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
63
+ # => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
64
+ #
65
+ module RestClient
66
+ def self.get(url, headers={})
67
+ Request.execute(:method => :get, :url => url, :headers => headers)
68
+ end
69
+
70
+ def self.post(url, payload, headers={})
71
+ Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers)
72
+ end
73
+
74
+ def self.put(url, payload, headers={})
75
+ Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers)
76
+ end
77
+
78
+ def self.delete(url, headers={})
79
+ Request.execute(:method => :delete, :url => url, :headers => headers)
80
+ end
81
+
82
+ def self.head(url, headers={})
83
+ Request.execute(:method => :head, :url => url, :headers => headers)
84
+ end
85
+
86
+ class << self
87
+ attr_accessor :proxy
88
+ end
89
+
90
+ # Print log of RestClient calls. Value can be stdout, stderr, or a filename.
91
+ # You can also configure logging by the environment variable RESTCLIENT_LOG.
92
+ def self.log=(log)
93
+ @@log = log
94
+ end
95
+
96
+ def self.log # :nodoc:
97
+ return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
98
+ return @@log if defined? @@log
99
+ nil
100
+ end
101
+
102
+ def self.version
103
+ version_path = File.dirname(__FILE__) + "/../VERSION"
104
+ return File.read(version_path).chomp if File.file?(version_path)
105
+ "0.0.0"
106
+ end
107
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ begin
5
+ require "ruby-debug"
6
+ rescue LoadError
7
+ # NOP, ignore
8
+ end
9
+
10
+ require File.dirname(__FILE__) + '/../lib/restclient'
@@ -0,0 +1,65 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe RestClient::Exception do
4
+ it "sets the exception message to ErrorMessage" do
5
+ RestClient::ResourceNotFound.new.message.should == 'Resource not found'
6
+ end
7
+
8
+ it "contains exceptions in RestClient" do
9
+ RestClient::Unauthorized.new.should be_a_kind_of(RestClient::Exception)
10
+ RestClient::ServerBrokeConnection.new.should be_a_kind_of(RestClient::Exception)
11
+ end
12
+ end
13
+
14
+ describe RestClient::RequestFailed do
15
+ before do
16
+ @response = mock('HTTP Response', :code => '502')
17
+ end
18
+
19
+ it "stores the http response on the exception" do
20
+ begin
21
+ raise RestClient::RequestFailed, :response
22
+ rescue RestClient::RequestFailed => e
23
+ e.response.should == :response
24
+ end
25
+ end
26
+
27
+ it "http_code convenience method for fetching the code as an integer" do
28
+ RestClient::RequestFailed.new(@response).http_code.should == 502
29
+ end
30
+
31
+ it "http_body convenience method for fetching the body (decoding when necessary)" do
32
+ @response.stub!(:[]).with('content-encoding').and_return('gzip')
33
+ @response.stub!(:body).and_return('compressed body')
34
+ RestClient::Request.should_receive(:decode).with('gzip', 'compressed body').and_return('plain body')
35
+ RestClient::RequestFailed.new(@response).http_body.should == 'plain body'
36
+ end
37
+
38
+ it "shows the status code in the message" do
39
+ RestClient::RequestFailed.new(@response).to_s.should match(/502/)
40
+ end
41
+ end
42
+
43
+ describe RestClient::ResourceNotFound do
44
+ it "also has the http response attached" do
45
+ begin
46
+ raise RestClient::ResourceNotFound, :response
47
+ rescue RestClient::ResourceNotFound => e
48
+ e.response.should == :response
49
+ end
50
+ end
51
+ end
52
+
53
+ describe "backwards compatibility" do
54
+ it "alias RestClient::Request::Redirect to RestClient::Redirect" do
55
+ RestClient::Request::Redirect.should == RestClient::Redirect
56
+ end
57
+
58
+ it "alias RestClient::Request::Unauthorized to RestClient::Unauthorized" do
59
+ RestClient::Request::Unauthorized.should == RestClient::Unauthorized
60
+ end
61
+
62
+ it "alias RestClient::Request::RequestFailed to RestClient::RequestFailed" do
63
+ RestClient::Request::RequestFailed.should == RestClient::RequestFailed
64
+ end
65
+ end
Binary file
@@ -0,0 +1,46 @@
1
+ require File.dirname(__FILE__) + '/../base'
2
+
3
+ class MockResponse
4
+ include RestClient::Mixin::Response
5
+
6
+ def initialize(body, res)
7
+ @net_http_res = res
8
+ @body = @body
9
+ end
10
+ end
11
+
12
+ describe RestClient::Mixin::Response do
13
+ before do
14
+ @net_http_res = mock('net http response')
15
+ @response = MockResponse.new('abc', @net_http_res)
16
+ end
17
+
18
+ it "fetches the numeric response code" do
19
+ @net_http_res.should_receive(:code).and_return('200')
20
+ @response.code.should == 200
21
+ end
22
+
23
+ it "beautifies the headers by turning the keys to symbols" do
24
+ h = RestClient::Response.beautify_headers('content-type' => [ 'x' ])
25
+ h.keys.first.should == :content_type
26
+ end
27
+
28
+ it "beautifies the headers by turning the values to strings instead of one-element arrays" do
29
+ h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] )
30
+ h.values.first.should == 'text/html'
31
+ end
32
+
33
+ it "fetches the headers" do
34
+ @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
35
+ @response.headers.should == { :content_type => 'text/html' }
36
+ end
37
+
38
+ it "extracts cookies from response headers" do
39
+ @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
40
+ @response.cookies.should == { 'session_id' => '1' }
41
+ end
42
+
43
+ it "can access the net http result directly" do
44
+ @response.net_http_res.should == @net_http_res
45
+ end
46
+ end