Permalink
Browse files

Merge commit 'adamwiggins/master'

  • Loading branch information...
2 parents 2a21bf6 + ff041a5 commit 94a50a66ef4412431f68dc824958990fafd0a3b3 Brian Donovan committed Aug 19, 2008
Showing with 230 additions and 24 deletions.
  1. +30 −13 README
  2. +1 −1 Rakefile
  3. +1 −1 bin/restclient
  4. +92 −8 lib/rest_client.rb
  5. +1 −1 rest-client.gemspec
  6. +105 −0 spec/rest_client_spec.rb
View
43 README
@@ -7,14 +7,10 @@ of specifying actions: get, put, post, delete.
require 'rest_client'
- xml = RestClient.get 'http://example.com/resource'
- jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
+ RestClient.get 'http://example.com/resource'
+ RestClient.get 'https://user:password@example.com/private/resource'
- private_resource = RestClient.get 'https://user:password@example.com/private/resource'
-
- RestClient.put 'http://example.com/resource', File.read('my.pdf'), :content_type => 'application/pdf'
-
- RestClient.post 'http://example.com/resource', xml, :content_type => 'application/xml'
+ RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' }
RestClient.delete 'http://example.com/resource'
@@ -57,23 +53,44 @@ Add a user and password for authenticated resources:
Create ~/.restclient for named sessions:
sinatra:
- :url: http://localhost:4567
+ url: http://localhost:4567
rack:
- :url: http://localhost:9292
+ url: http://localhost:9292
private_site:
- :url: http://example.com
- :username: user
- :password: pass
+ url: http://example.com
+ username: user
+ password: pass
Then invoke:
$ restclient private_site
+== Logging
+
+Write calls to a log filename (can also be "stdout" or "stderr"):
+
+ RestClient.log = '/tmp/restclient.log'
+
+Or set an environment variable to avoid modifying the code:
+
+ $ RESTCLIENT_LOG=stdout path/to/my/program
+
+Either produces logs like this:
+
+ RestClient.get "http://some/resource"
+ # => 200 OK | text/html 250 bytes
+ RestClient.put "http://some/resource", "payload"
+ # => 401 Unauthorized | application/xml 340 bytes
+
+Note that these logs are valid Ruby, so you can paste them into the restclient
+shell or a script to replay your sequence of rest calls.
+
== Meta
Written by Adam Wiggins (adam at heroku dot com)
-Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, and Blake Mizerany
+Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur,
+Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, and Blake Mizerany
Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
View
2 Rakefile
@@ -31,7 +31,7 @@ require 'rake/gempackagetask'
require 'rake/rdoctask'
require 'fileutils'
-version = "0.6"
+version = "0.7"
name = "rest-client"
spec = Gem::Specification.new do |s|
View
2 bin/restclient
@@ -16,7 +16,7 @@ end
config = YAML.load(File.read(ENV['HOME'] + "/.restclient")) rescue {}
@url, @username, @password = if c = config[@url]
- [c[:url], c[:username], c[:password]]
+ [c['url'], c['username'], c['password']]
else
[@url, *ARGV]
end
View
100 lib/rest_client.rb
@@ -1,10 +1,39 @@
require 'uri'
require 'net/https'
+require 'zlib'
+require 'stringio'
require File.dirname(__FILE__) + '/resource'
require File.dirname(__FILE__) + '/request_errors'
# This module's static methods are the entry point for using the REST client.
+#
+# # GET
+# xml = RestClient.get 'http://example.com/resource'
+# jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
+#
+# # authentication and SSL
+# RestClient.get 'https://user:password@example.com/private/resource'
+#
+# # POST or PUT with a hash sends parameters as a urlencoded form body
+# RestClient.post 'http://example.com/resource', :param1 => 'one'
+#
+# # nest hash parameters
+# RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
+#
+# # POST and PUT with raw payloads
+# RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
+# RestClient.post 'http://example.com/resource.xml', xml_doc
+# RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
+#
+# # DELETE
+# RestClient.delete 'http://example.com/resource'
+#
+# For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
+#
+# >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
+# => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
+#
module RestClient
def self.get(url, headers={})
Request.execute(:method => :get,
@@ -39,7 +68,19 @@ def self.delete(url, headers={})
class <<self
attr_accessor :proxy
end
-
+
+ # Print log of RestClient calls. Value can be stdout, stderr, or a filename.
+ # You can also configure logging by the environment variable RESTCLIENT_LOG.
+ def self.log=(log)
+ @@log = log
+ end
+
+ def self.log # :nodoc:
+ return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
+ return @@log if defined? @@log
+ nil
+ end
+
# Internal class used to build and execute the request.
class Request
attr_reader :method, :url, :payload, :headers, :user, :password, :proxy
@@ -95,14 +136,19 @@ def parse_url_with_auth(url)
uri
end
- def process_payload(p=nil)
+ def process_payload(p=nil, parent_key=nil)
unless p.is_a?(Hash)
p
else
- @headers[:content_type] = 'application/x-www-form-urlencoded'
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
p.keys.map do |k|
- v = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
- "#{k}=#{v}"
+ key = parent_key ? "#{parent_key}[#{k}]" : k
+ if p[k].is_a? Hash
+ process_payload(p[k], key)
+ else
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
+ "#{key}=#{value}"
+ end
end.join("&")
end
end
@@ -119,8 +165,12 @@ def transmit(uri, req, payload)
net = http_klass.new(uri.host, uri.port)
net.use_ssl = uri.is_a?(URI::HTTPS)
+ display_log request_log
+
net.start do |http|
- process_result http.request(req, payload || "")
+ res = http.request(req, payload || "")
+ display_log response_log(res)
+ process_result res
end
rescue EOFError
raise RestClient::ServerBrokeConnection
@@ -134,7 +184,7 @@ def setup_credentials(req)
def process_result(res)
if %w(200 201 202).include? res.code
- res.body
+ decode res['content-encoding'], res.body
elsif %w(301 302 303).include? res.code
url = res.header['Location']
@@ -154,8 +204,42 @@ def process_result(res)
end
end
+ def decode(content_encoding, body)
+ if content_encoding == 'gzip'
+ Zlib::GzipReader.new(StringIO.new(body)).read
+ elsif content_encoding == 'deflate'
+ Zlib::Inflate.new.inflate(body)
+ else
+ body
+ end
+ end
+
+ def request_log
+ out = []
+ out << "RestClient.#{method} #{url.inspect}"
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
+ out.join(', ')
+ end
+
+ def response_log(res)
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{res.body.size} bytes"
+ end
+
+ def display_log(msg)
+ return unless log_to = RestClient.log
+
+ if log_to == 'stdout'
+ STDOUT.puts msg
+ elsif log_to == 'stderr'
+ STDERR.puts msg
+ else
+ File.open(log_to, 'a') { |f| f.puts msg }
+ end
+ end
+
def default_headers
- { :accept => 'application/xml' }
+ { :accept => 'application/xml', :accept_encoding => 'gzip, deflate' }
end
end
end
View
2 rest-client.gemspec
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = "rest-client"
- s.version = "0.6"
+ s.version = "0.7"
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
s.author = "Adam Wiggins"
View
105 spec/rest_client_spec.rb
@@ -28,6 +28,29 @@
end
end
+ context "logging" do
+ after do
+ RestClient.log = nil
+ end
+
+ it "gets the log source from the RESTCLIENT_LOG environment variable" do
+ ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return('from env')
+ RestClient.log = 'from class method'
+ RestClient.log.should == 'from env'
+ end
+
+ it "sets a destination for log output, used if no environment variable is set" do
+ ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
+ RestClient.log = 'from class method'
+ RestClient.log.should == 'from class method'
+ end
+
+ it "returns nil (no logging) if neither are set (default)" do
+ ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
+ RestClient.log.should == nil
+ end
+ end
+
context RestClient::Request do
before do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
@@ -48,10 +71,23 @@
@request.default_headers[:accept].should == 'application/xml'
end
+ it "decodes an uncompressed result body by passing it straight through" do
+ @request.decode(nil, 'xyz').should == 'xyz'
+ end
+
+ it "decodes a gzip body" do
+ @request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000").should == "i'm gziped\n"
+ end
+
+ it "decodes a deflated body" do
+ @request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should == "some deflated text"
+ end
+
it "processes a successful result" do
res = mock("result")
res.stub!(:code).and_return("200")
res.stub!(:body).and_return('body')
+ res.stub!(:[]).with('content-encoding').and_return(nil)
@request.process_result(res).should == 'body'
end
@@ -111,6 +147,7 @@
it "transmits the request with Net::HTTP" do
@http.should_receive(:request).with('req', 'payload')
@request.should_receive(:process_result)
+ @request.should_receive(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
@@ -119,12 +156,14 @@
@net.should_receive(:use_ssl=).with(true)
@http.stub!(:request)
@request.stub!(:process_result)
+ @request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "doesn't send nil payloads" do
@http.should_receive(:request).with('req', '')
@request.should_receive(:process_result)
+ @request.stub!(:response_log)
@request.transmit(@uri, 'req', nil)
end
@@ -136,6 +175,13 @@
@request.process_payload(:a => 'b c+d').should == "a=b%20c%2Bd"
end
+ it "accepts nested hashes in payload" do
+ payload = @request.process_payload(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }})
+ payload.should include('user[name]=joe')
+ payload.should include('user[location][country]=USA')
+ payload.should include('user[location][state]=CA')
+ end
+
it "set urlencoded content_type header on hash payloads" do
@request.process_payload(:a => 1)
@request.headers[:content_type].should == 'application/x-www-form-urlencoded'
@@ -144,6 +190,7 @@
it "sets up the credentials prior to the request" do
@http.stub!(:request)
@request.stub!(:process_result)
+ @request.stub!(:response_log)
@request.stub!(:user).and_return('joe')
@request.stub!(:password).and_return('mypass')
@@ -246,5 +293,63 @@
request.stub!(:process_result)
request.execute
end
+
+ it "logs a get request" do
+ RestClient::Request.new(:method => :get, :url => 'http://url').request_log.should ==
+ 'RestClient.get "http://url"'
+ end
+
+ it "logs a post request with a small payload" do
+ RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log.should ==
+ 'RestClient.post "http://url", "foo"'
+ end
+
+ it "logs a post request with a large payload" do
+ RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log.should ==
+ 'RestClient.post "http://url", "(1000 byte payload)"'
+ end
+
+ it "logs input headers as a hash" do
+ RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).request_log.should ==
+ 'RestClient.get "http://url", :accept=>"text/plain"'
+ end
+
+ it "logs a response including the status code, content type, and result body size in bytes" do
+ res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
+ res.stub!(:[]).with('Content-type').and_return('text/html')
+ @request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
+ end
+
+ it "logs a response with a nil Content-type" do
+ res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
+ res.stub!(:[]).with('Content-type').and_return(nil)
+ @request.response_log(res).should == "# => 200 OK | 4 bytes"
+ end
+
+ it "strips the charset from the response content type" do
+ res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
+ res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
+ @request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
+ end
+
+ it "displays the log to stdout" do
+ RestClient.stub!(:log).and_return('stdout')
+ STDOUT.should_receive(:puts).with('xyz')
+ @request.display_log('xyz')
+ end
+
+ it "displays the log to stderr" do
+ RestClient.stub!(:log).and_return('stderr')
+ STDERR.should_receive(:puts).with('xyz')
+ @request.display_log('xyz')
+ end
+
+ it "append the log to the requested filename" do
+ RestClient.stub!(:log).and_return('/tmp/restclient.log')
+ f = mock('file handle')
+ File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
+ f.should_receive(:puts).with('xyz')
+ @request.display_log('xyz')
+ end
end
end

0 comments on commit 94a50a6

Please sign in to comment.