From edeafb34dc864530c21de03182743fef5eb214b1 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 24 Jan 2009 14:49:01 -0800 Subject: [PATCH 01/53] restclient instead of rest_client; reorganize lib dir --- bin/restclient | 2 +- lib/rest_client.rb | 285 +----------------- lib/restclient.rb | 79 +++++ .../exceptions.rb} | 0 lib/restclient/request.rb | 172 +++++++++++ lib/{ => restclient}/resource.rb | 0 lib/restclient/response.rb | 25 ++ spec/base.rb | 2 +- 8 files changed, 280 insertions(+), 285 deletions(-) create mode 100644 lib/restclient.rb rename lib/{request_errors.rb => restclient/exceptions.rb} (100%) create mode 100644 lib/restclient/request.rb rename lib/{ => restclient}/resource.rb (100%) create mode 100644 lib/restclient/response.rb diff --git a/bin/restclient b/bin/restclient index 872653f..aed4b93 100755 --- a/bin/restclient +++ b/bin/restclient @@ -1,7 +1,7 @@ #!/usr/bin/env ruby $:.unshift File.dirname(__FILE__) + "/../lib" -require 'rest_client' +require 'restclient' require "yaml" diff --git a/lib/rest_client.rb b/lib/rest_client.rb index a03c1de..c35d16f 100644 --- a/lib/rest_client.rb +++ b/lib/rest_client.rb @@ -1,283 +1,2 @@ -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' -# -# To use with a proxy, just set RestClient.proxy to the proper http proxy: -# -# RestClient.proxy = "http://proxy.example.com/" -# -# Or inherit the proxy from the environment: -# -# RestClient.proxy = ENV['http_proxy'] -# -# 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, - :url => url, - :headers => headers) - end - - def self.post(url, payload, headers={}) - Request.execute(:method => :post, - :url => url, - :payload => payload, - :headers => headers) - end - - def self.put(url, payload, headers={}) - Request.execute(:method => :put, - :url => url, - :payload => payload, - :headers => headers) - end - - def self.delete(url, headers={}) - Request.execute(:method => :delete, - :url => url, - :headers => headers) - end - - class < e - @url = e.url - execute - end - - def execute_inner - uri = parse_url_with_auth(url) - transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload - end - - def make_headers(user_headers) - default_headers.merge(user_headers).inject({}) do |final, (key, value)| - final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s - final - end - end - - def net_http_class - if RestClient.proxy - proxy_uri = URI.parse(RestClient.proxy) - Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password) - else - Net::HTTP - end - end - - def net_http_request_class(method) - Net::HTTP.const_get(method.to_s.capitalize) - end - - def parse_url(url) - url = "http://#{url}" unless url.match(/^http/) - URI.parse(url) - end - - def parse_url_with_auth(url) - uri = parse_url(url) - @user = uri.user if uri.user - @password = uri.password if uri.password - uri - end - - def process_payload(p=nil, parent_key=nil) - unless p.is_a?(Hash) - p - else - @headers[:content_type] ||= 'application/x-www-form-urlencoded' - p.keys.map do |k| - 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 - - def transmit(uri, req, payload) - setup_credentials(req) - - net = net_http_class.new(uri.host, uri.port) - net.use_ssl = uri.is_a?(URI::HTTPS) - net.verify_mode = OpenSSL::SSL::VERIFY_NONE - - display_log request_log - - net.start do |http| - http.read_timeout = @timeout if @timeout - res = http.request(req, payload) - display_log response_log(res) - string = process_result(res) - if string - Response.new(string, res) - else - nil - end - end - rescue EOFError - raise RestClient::ServerBrokeConnection - rescue Timeout::Error - raise RestClient::RequestTimeout - end - - def setup_credentials(req) - req.basic_auth(user, password) if user - end - - def process_result(res) - if %w(200 201 202).include? res.code - decode res['content-encoding'], res.body - elsif %w(301 302 303).include? res.code - url = res.header['Location'] - - if url !~ /^http/ - uri = URI.parse(@url) - uri.path = "/#{url}".squeeze('/') - url = uri.to_s - end - - raise Redirect, url - elsif res.code == "304" - raise NotModified - elsif res.code == "401" - raise Unauthorized, res - elsif res.code == "404" - raise ResourceNotFound, res - else - raise RequestFailed, 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) ? res.body.size : nil} 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_encoding => 'gzip, deflate' } - end - end - - class Response < String - attr_reader :net_http_res - - def initialize(string, net_http_res) - @net_http_res = net_http_res - super string - end - - def code - @code ||= @net_http_res.code.to_i - end - - def headers - @headers ||= self.class.beautify_headers(@net_http_res.to_hash) - end - - def self.beautify_headers(headers) - headers.inject({}) do |out, (key, value)| - out[key.gsub(/-/, '_').to_sym] = value.first - out - end - end - end -end +# This file exists for backward compatbility with require 'rest_client' +require File.dirname(__FILE__) + '/restclient' diff --git a/lib/restclient.rb b/lib/restclient.rb new file mode 100644 index 0000000..0cad8bf --- /dev/null +++ b/lib/restclient.rb @@ -0,0 +1,79 @@ +require 'uri' +require 'net/https' +require 'zlib' +require 'stringio' + +require File.dirname(__FILE__) + '/restclient/request' +require File.dirname(__FILE__) + '/restclient/response' +require File.dirname(__FILE__) + '/restclient/resource' +require File.dirname(__FILE__) + '/restclient/exceptions' + +# 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' +# +# To use with a proxy, just set RestClient.proxy to the proper http proxy: +# +# RestClient.proxy = "http://proxy.example.com/" +# +# Or inherit the proxy from the environment: +# +# RestClient.proxy = ENV['http_proxy'] +# +# 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, :url => url, :headers => headers) + end + + def self.post(url, payload, headers={}) + Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers) + end + + def self.put(url, payload, headers={}) + Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers) + end + + def self.delete(url, headers={}) + Request.execute(:method => :delete, :url => url, :headers => headers) + end + + 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 +end diff --git a/lib/request_errors.rb b/lib/restclient/exceptions.rb similarity index 100% rename from lib/request_errors.rb rename to lib/restclient/exceptions.rb diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb new file mode 100644 index 0000000..bc9880d --- /dev/null +++ b/lib/restclient/request.rb @@ -0,0 +1,172 @@ +module RestClient + class Request + attr_reader :method, :url, :payload, :headers, :user, :password, :timeout + + def self.execute(args) + new(args).execute + end + + def initialize(args) + @method = args[:method] or raise ArgumentError, "must pass :method" + @url = args[:url] or raise ArgumentError, "must pass :url" + @headers = args[:headers] || {} + @payload = process_payload(args[:payload]) + @user = args[:user] + @password = args[:password] + @timeout = args[:timeout] + end + + def execute + execute_inner + rescue Redirect => e + @url = e.url + execute + end + + def execute_inner + uri = parse_url_with_auth(url) + transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload + end + + def make_headers(user_headers) + default_headers.merge(user_headers).inject({}) do |final, (key, value)| + final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s + final + end + end + + def net_http_class + if RestClient.proxy + proxy_uri = URI.parse(RestClient.proxy) + Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password) + else + Net::HTTP + end + end + + def net_http_request_class(method) + Net::HTTP.const_get(method.to_s.capitalize) + end + + def parse_url(url) + url = "http://#{url}" unless url.match(/^http/) + URI.parse(url) + end + + def parse_url_with_auth(url) + uri = parse_url(url) + @user = uri.user if uri.user + @password = uri.password if uri.password + uri + end + + def process_payload(p=nil, parent_key=nil) + unless p.is_a?(Hash) + p + else + @headers[:content_type] ||= 'application/x-www-form-urlencoded' + p.keys.map do |k| + 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 + + def transmit(uri, req, payload) + setup_credentials(req) + + net = net_http_class.new(uri.host, uri.port) + net.use_ssl = uri.is_a?(URI::HTTPS) + net.verify_mode = OpenSSL::SSL::VERIFY_NONE + + display_log request_log + + net.start do |http| + http.read_timeout = @timeout if @timeout + res = http.request(req, payload) + display_log response_log(res) + string = process_result(res) + if string + Response.new(string, res) + else + nil + end + end + rescue EOFError + raise RestClient::ServerBrokeConnection + rescue Timeout::Error + raise RestClient::RequestTimeout + end + + def setup_credentials(req) + req.basic_auth(user, password) if user + end + + def process_result(res) + if %w(200 201 202).include? res.code + decode res['content-encoding'], res.body + elsif %w(301 302 303).include? res.code + url = res.header['Location'] + + if url !~ /^http/ + uri = URI.parse(@url) + uri.path = "/#{url}".squeeze('/') + url = uri.to_s + end + + raise Redirect, url + elsif res.code == "304" + raise NotModified + elsif res.code == "401" + raise Unauthorized, res + elsif res.code == "404" + raise ResourceNotFound, res + else + raise RequestFailed, 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) ? res.body.size : nil} 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_encoding => 'gzip, deflate' } + end + end +end diff --git a/lib/resource.rb b/lib/restclient/resource.rb similarity index 100% rename from lib/resource.rb rename to lib/restclient/resource.rb diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb new file mode 100644 index 0000000..263b1ec --- /dev/null +++ b/lib/restclient/response.rb @@ -0,0 +1,25 @@ +module RestClient + class Response < String + attr_reader :net_http_res + + def initialize(string, net_http_res) + @net_http_res = net_http_res + super string + end + + def code + @code ||= @net_http_res.code.to_i + end + + def headers + @headers ||= self.class.beautify_headers(@net_http_res.to_hash) + end + + def self.beautify_headers(headers) + headers.inject({}) do |out, (key, value)| + out[key.gsub(/-/, '_').to_sym] = value.first + out + end + end + end +end diff --git a/spec/base.rb b/spec/base.rb index 192612c..c27e8a4 100644 --- a/spec/base.rb +++ b/spec/base.rb @@ -1,4 +1,4 @@ require 'rubygems' require 'spec' -require File.dirname(__FILE__) + '/../lib/rest_client' +require File.dirname(__FILE__) + '/../lib/restclient' From be753027929b85568e90d04a2b22ba105ea67b19 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 24 Jan 2009 14:55:18 -0800 Subject: [PATCH 02/53] organize specs to match lib --- rest-client.gemspec | 6 +- ...uest_errors_spec.rb => exceptions_spec.rb} | 0 spec/request_spec.rb | 296 ++++++++++++++ spec/response_spec.rb | 36 ++ spec/rest_client_spec.rb | 378 ------------------ spec/restclient_spec.rb | 48 +++ 6 files changed, 384 insertions(+), 380 deletions(-) rename spec/{request_errors_spec.rb => exceptions_spec.rb} (100%) create mode 100644 spec/request_spec.rb create mode 100644 spec/response_spec.rb delete mode 100644 spec/rest_client_spec.rb create mode 100644 spec/restclient_spec.rb diff --git a/rest-client.gemspec b/rest-client.gemspec index f9ab549..df83c85 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -10,8 +10,10 @@ Gem::Specification.new do |s| s.has_rdoc = true s.platform = Gem::Platform::RUBY s.files = %w(Rakefile README.rdoc rest-client.gemspec - lib/request_errors.rb lib/resource.rb lib/rest_client.rb - spec/base.rb spec/request_errors_spec.rb spec/resource_spec.rb spec/rest_client_spec.rb + lib/restclient/request.rb lib/restclient/response.rb + lib/restclient/exceptions.rb lib/restclient/resource.rb + spec/base.rb spec/request_spec.rb spec/response_spec.rb + spec/exceptions_spec.rb spec/resource_spec.rb spec/restclient_spec.rb bin/restclient) s.executables = ['restclient'] s.require_path = "lib" diff --git a/spec/request_errors_spec.rb b/spec/exceptions_spec.rb similarity index 100% rename from spec/request_errors_spec.rb rename to spec/exceptions_spec.rb diff --git a/spec/request_spec.rb b/spec/request_spec.rb new file mode 100644 index 0000000..59afb94 --- /dev/null +++ b/spec/request_spec.rb @@ -0,0 +1,296 @@ +require File.dirname(__FILE__) + '/base' + +describe RestClient::Request do + before do + @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload') + + @uri = mock("uri") + @uri.stub!(:request_uri).and_return('/resource') + @uri.stub!(:host).and_return('some') + @uri.stub!(:port).and_return(80) + + @net = mock("net::http base") + @http = mock("net::http connection") + Net::HTTP.stub!(:new).and_return(@net) + @net.stub!(:start).and_yield(@http) + @net.stub!(:use_ssl=) + @net.stub!(:verify_mode=) + end + + it "requests xml mimetype" do + @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 + + it "parses a url into a URI object" do + URI.should_receive(:parse).with('http://example.com/resource') + @request.parse_url('http://example.com/resource') + end + + it "adds http:// to the front of resources specified in the syntax example.com/resource" do + URI.should_receive(:parse).with('http://example.com/resource') + @request.parse_url('example.com/resource') + end + + it "extracts the username and password when parsing http://user:password@example.com/" do + URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1')) + @request.parse_url_with_auth('http://joe:pass1@example.com/resource') + @request.user.should == 'joe' + @request.password.should == 'pass1' + end + + 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 + URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) + @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2') + @request.parse_url_with_auth('http://example.com/resource') + @request.user.should == 'beth' + @request.password.should == 'pass2' + end + + it "determines the Net::HTTP class to instantiate by the method name" do + @request.net_http_request_class(:put).should == Net::HTTP::Put + end + + it "merges user headers with the default headers" do + @request.should_receive(:default_headers).and_return({ '1' => '2' }) + @request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' } + end + + it "prefers the user header when the same header exists in the defaults" do + @request.should_receive(:default_headers).and_return({ '1' => '2' }) + @request.make_headers('1' => '3').should == { '1' => '3' } + end + + it "converts header symbols from :content_type to 'Content-type'" do + @request.should_receive(:default_headers).and_return({}) + @request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' } + end + + it "converts header values to strings" do + @request.make_headers('A' => 1)['A'].should == '1' + end + + it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do + @request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri) + klass = mock("net:http class") + @request.should_receive(:net_http_request_class).with(:put).and_return(klass) + klass.should_receive(:new).and_return('result') + @request.should_receive(:transmit).with(@uri, 'result', 'payload') + @request.execute_inner + end + + 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 + + it "uses SSL when the URI refers to a https address" do + @uri.stub!(:is_a?).with(URI::HTTPS).and_return(true) + @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 "sends nil payloads" do + @http.should_receive(:request).with('req', nil) + @request.should_receive(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', nil) + end + + it "passes non-hash payloads straight through" do + @request.process_payload("x").should == "x" + end + + it "converts a hash payload to urlencoded data" do + @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' + end + + 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') + @request.should_receive(:setup_credentials).with('req') + + @request.transmit(@uri, 'req', nil) + end + + it "does not attempt to send any credentials if user is nil" do + @request.stub!(:user).and_return(nil) + req = mock("request") + req.should_not_receive(:basic_auth) + @request.setup_credentials(req) + end + + it "setup credentials when there's a user" do + @request.stub!(:user).and_return('joe') + @request.stub!(:password).and_return('mypass') + req = mock("request") + req.should_receive(:basic_auth).with('joe', 'mypass') + @request.setup_credentials(req) + end + + it "catches EOFError and shows the more informative ServerBrokeConnection" do + @http.stub!(:request).and_raise(EOFError) + lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection) + end + + it "execute calls execute_inner" do + @request.should_receive(:execute_inner) + @request.execute + end + + it "class method execute wraps constructor" do + req = mock("rest request") + RestClient::Request.should_receive(:new).with(1 => 2).and_return(req) + req.should_receive(:execute) + RestClient::Request.execute(1 => 2) + end + + it "raises a Redirect with the new location when the response is in the 30x range" do + res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' }) + lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'} + end + + it "handles redirects with relative paths" do + res = mock('response', :code => '301', :header => { 'Location' => 'index' }) + lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } + end + + it "handles redirects with absolute paths" do + @request.instance_variable_set('@url', 'http://some/place/else') + res = mock('response', :code => '301', :header => { 'Location' => '/index' }) + lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } + end + + it "raises Unauthorized when the response is 401" do + res = mock('response', :code => '401') + lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized) + end + + it "raises ResourceNotFound when the response is 404" do + res = mock('response', :code => '404') + lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound) + end + + it "raises RequestFailed otherwise" do + res = mock('response', :code => '500') + lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed) + end + + it "creates a proxy class if a proxy url is given" do + RestClient.stub!(:proxy).and_return("http://example.com/") + @request.net_http_class.should include(Net::HTTP::ProxyDelta) + end + + it "creates a non-proxy class if a proxy url is not given" do + @request.net_http_class.should_not include(Net::HTTP::ProxyDelta) + 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 + + it "set read_timeout" do + @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + + @http.should_receive(:read_timeout=).with(123) + + @request.transmit(@uri, 'req', nil) + end +end diff --git a/spec/response_spec.rb b/spec/response_spec.rb new file mode 100644 index 0000000..e6663a6 --- /dev/null +++ b/spec/response_spec.rb @@ -0,0 +1,36 @@ +require File.dirname(__FILE__) + '/base' + +describe RestClient::Response do + before do + @net_http_res = mock('net http response') + @response = RestClient::Response.new('abc', @net_http_res) + end + + it "behaves like string" do + @response.should == 'abc' + end + + it "fetches the numeric response code" do + @net_http_res.should_receive(:code).and_return('200') + @response.code.should == 200 + end + + it "beautifies the headers by turning the keys to symbols" do + h = RestClient::Response.beautify_headers('content-type' => [ 'x' ]) + h.keys.first.should == :content_type + end + + it "beautifies the headers by turning the values to strings instead of one-element arrays" do + h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] ) + h.values.first.should == 'text/html' + end + + it "fetches the headers" do + @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ]) + @response.headers.should == { :content_type => 'text/html' } + end + + it "can access the net http result directly" do + @response.net_http_res.should == @net_http_res + end +end diff --git a/spec/rest_client_spec.rb b/spec/rest_client_spec.rb deleted file mode 100644 index f80d02d..0000000 --- a/spec/rest_client_spec.rb +++ /dev/null @@ -1,378 +0,0 @@ -require File.dirname(__FILE__) + '/base' - -describe RestClient do - context "public API" do - it "GET" do - RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}) - RestClient.get('http://some/resource') - end - - it "POST" do - RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {}) - RestClient.post('http://some/resource', 'payload') - end - - it "PUT" do - RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {}) - RestClient.put('http://some/resource', 'payload') - end - - it "DELETE" do - RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}) - RestClient.delete('http://some/resource') - 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') - - @uri = mock("uri") - @uri.stub!(:request_uri).and_return('/resource') - @uri.stub!(:host).and_return('some') - @uri.stub!(:port).and_return(80) - - @net = mock("net::http base") - @http = mock("net::http connection") - Net::HTTP.stub!(:new).and_return(@net) - @net.stub!(:start).and_yield(@http) - @net.stub!(:use_ssl=) - @net.stub!(:verify_mode=) - end - - it "requests xml mimetype" do - @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 - - it "parses a url into a URI object" do - URI.should_receive(:parse).with('http://example.com/resource') - @request.parse_url('http://example.com/resource') - end - - it "adds http:// to the front of resources specified in the syntax example.com/resource" do - URI.should_receive(:parse).with('http://example.com/resource') - @request.parse_url('example.com/resource') - end - - it "extracts the username and password when parsing http://user:password@example.com/" do - URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1')) - @request.parse_url_with_auth('http://joe:pass1@example.com/resource') - @request.user.should == 'joe' - @request.password.should == 'pass1' - end - - 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 - URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) - @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2') - @request.parse_url_with_auth('http://example.com/resource') - @request.user.should == 'beth' - @request.password.should == 'pass2' - end - - it "determines the Net::HTTP class to instantiate by the method name" do - @request.net_http_request_class(:put).should == Net::HTTP::Put - end - - it "merges user headers with the default headers" do - @request.should_receive(:default_headers).and_return({ '1' => '2' }) - @request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' } - end - - it "prefers the user header when the same header exists in the defaults" do - @request.should_receive(:default_headers).and_return({ '1' => '2' }) - @request.make_headers('1' => '3').should == { '1' => '3' } - end - - it "converts header symbols from :content_type to 'Content-type'" do - @request.should_receive(:default_headers).and_return({}) - @request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' } - end - - it "converts header values to strings" do - @request.make_headers('A' => 1)['A'].should == '1' - end - - it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do - @request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri) - klass = mock("net:http class") - @request.should_receive(:net_http_request_class).with(:put).and_return(klass) - klass.should_receive(:new).and_return('result') - @request.should_receive(:transmit).with(@uri, 'result', 'payload') - @request.execute_inner - end - - 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 - - it "uses SSL when the URI refers to a https address" do - @uri.stub!(:is_a?).with(URI::HTTPS).and_return(true) - @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 "sends nil payloads" do - @http.should_receive(:request).with('req', nil) - @request.should_receive(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', nil) - end - - it "passes non-hash payloads straight through" do - @request.process_payload("x").should == "x" - end - - it "converts a hash payload to urlencoded data" do - @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' - end - - 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') - @request.should_receive(:setup_credentials).with('req') - - @request.transmit(@uri, 'req', nil) - end - - it "does not attempt to send any credentials if user is nil" do - @request.stub!(:user).and_return(nil) - req = mock("request") - req.should_not_receive(:basic_auth) - @request.setup_credentials(req) - end - - it "setup credentials when there's a user" do - @request.stub!(:user).and_return('joe') - @request.stub!(:password).and_return('mypass') - req = mock("request") - req.should_receive(:basic_auth).with('joe', 'mypass') - @request.setup_credentials(req) - end - - it "catches EOFError and shows the more informative ServerBrokeConnection" do - @http.stub!(:request).and_raise(EOFError) - lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection) - end - - it "execute calls execute_inner" do - @request.should_receive(:execute_inner) - @request.execute - end - - it "class method execute wraps constructor" do - req = mock("rest request") - RestClient::Request.should_receive(:new).with(1 => 2).and_return(req) - req.should_receive(:execute) - RestClient::Request.execute(1 => 2) - end - - it "raises a Redirect with the new location when the response is in the 30x range" do - res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' }) - lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'} - end - - it "handles redirects with relative paths" do - res = mock('response', :code => '301', :header => { 'Location' => 'index' }) - lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } - end - - it "handles redirects with absolute paths" do - @request.instance_variable_set('@url', 'http://some/place/else') - res = mock('response', :code => '301', :header => { 'Location' => '/index' }) - lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } - end - - it "raises Unauthorized when the response is 401" do - res = mock('response', :code => '401') - lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized) - end - - it "raises ResourceNotFound when the response is 404" do - res = mock('response', :code => '404') - lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound) - end - - it "raises RequestFailed otherwise" do - res = mock('response', :code => '500') - lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed) - end - - it "creates a proxy class if a proxy url is given" do - RestClient.stub!(:proxy).and_return("http://example.com/") - @request.net_http_class.should include(Net::HTTP::ProxyDelta) - end - - it "creates a non-proxy class if a proxy url is not given" do - @request.net_http_class.should_not include(Net::HTTP::ProxyDelta) - 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 - - it "set read_timeout" do - @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123) - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - - @http.should_receive(:read_timeout=).with(123) - - @request.transmit(@uri, 'req', nil) - end - end - - context RestClient::Response do - before do - @net_http_res = mock('net http response') - @response = RestClient::Response.new('abc', @net_http_res) - end - - it "behaves like string" do - @response.should == 'abc' - end - - it "fetches the numeric response code" do - @net_http_res.should_receive(:code).and_return('200') - @response.code.should == 200 - end - - it "beautifies the headers by turning the keys to symbols" do - h = RestClient::Response.beautify_headers('content-type' => [ 'x' ]) - h.keys.first.should == :content_type - end - - it "beautifies the headers by turning the values to strings instead of one-element arrays" do - h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] ) - h.values.first.should == 'text/html' - end - - it "fetches the headers" do - @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ]) - @response.headers.should == { :content_type => 'text/html' } - end - - it "can access the net http result directly" do - @response.net_http_res.should == @net_http_res - end - end -end diff --git a/spec/restclient_spec.rb b/spec/restclient_spec.rb new file mode 100644 index 0000000..0c339e5 --- /dev/null +++ b/spec/restclient_spec.rb @@ -0,0 +1,48 @@ +require File.dirname(__FILE__) + '/base' + +describe RestClient do + describe "API" do + it "GET" do + RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}) + RestClient.get('http://some/resource') + end + + it "POST" do + RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {}) + RestClient.post('http://some/resource', 'payload') + end + + it "PUT" do + RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {}) + RestClient.put('http://some/resource', 'payload') + end + + it "DELETE" do + RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}) + RestClient.delete('http://some/resource') + end + end + + describe "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 +end From 2ca019caebf8ec5eb8f177ab6366dfaa29678d39 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 24 Jan 2009 17:03:33 -0800 Subject: [PATCH 03/53] document response headers --- lib/restclient.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/restclient.rb b/lib/restclient.rb index 0cad8bf..e2e32b2 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -31,6 +31,10 @@ require File.dirname(__FILE__) + '/restclient/exceptions' # # DELETE # RestClient.delete 'http://example.com/resource' # +# # retreive the response headers +# res = RestClient.get 'http://example.com/some.jpg' +# res.headers[:content_type] # => 'image/jpg' +# # To use with a proxy, just set RestClient.proxy to the proper http proxy: # # RestClient.proxy = "http://proxy.example.com/" From 74cd64d7a71ddfa8fa3547529007131069bfe037 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 24 Jan 2009 17:12:38 -0800 Subject: [PATCH 04/53] rdocs for the reorganized classes --- Rakefile | 5 +++-- lib/restclient/exceptions.rb | 4 +++- lib/restclient/request.rb | 6 ++++++ lib/restclient/response.rb | 10 ++++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Rakefile b/Rakefile index b1f190b..b2aa4b2 100644 --- a/Rakefile +++ b/Rakefile @@ -76,8 +76,9 @@ Rake::RDocTask.new do |t| t.title = "rest-client, fetch RESTful resources effortlessly" t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' t.options << '--charset' << 'utf-8' - t.rdoc_files.include('README') - t.rdoc_files.include('lib/*.rb') + t.rdoc_files.include('README.rdoc') + t.rdoc_files.include('lib/restclient.rb') + t.rdoc_files.include('lib/restclient/*.rb') end CLEAN.include [ 'pkg', '*.gem', '.config' ] diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 3e84e8a..c7303e9 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -44,7 +44,9 @@ module RestClient ErrorMessage = 'Resource not found' end - # The server broke the connection prior to the request completing. + # The server broke the connection prior to the request completing. Usually + # this means it crashed, or sometimes that your network connection was + # severed before it could complete. class ServerBrokeConnection < Exception ErrorMessage = 'Server broke connection' end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index bc9880d..0149f3a 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -1,4 +1,10 @@ module RestClient + # This class is used internally by RestClient to send the request, but you can also + # access it internally if you'd like to use a method not directly supported by the + # main API. For example: + # + # RestClient::Request.execute(:method => :head, :url => 'http://example.com') + # class Request attr_reader :method, :url, :payload, :headers, :user, :password, :timeout diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index 263b1ec..c0169d6 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -1,4 +1,10 @@ module RestClient + # The response from RestClient looks like a string, but is actually one of + # these. 99% of the time you're making a rest call all you care about is + # the body, but on the occassion you want to fetch the headers you can: + # + # RestClient.get('http://example.com').headers[:content_type] + # class Response < String attr_reader :net_http_res @@ -7,10 +13,14 @@ module RestClient super string end + # HTTP status code, always 200 since RestClient throws exceptions for + # other codes. def code @code ||= @net_http_res.code.to_i end + # A hash of the headers, beautified with symbols and underscores. + # e.g. "Content-type" will become :content_type. def headers @headers ||= self.class.beautify_headers(@net_http_res.to_hash) end From 9525a9a1476f0970cabcfb615a48214284427096 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 24 Jan 2009 17:14:52 -0800 Subject: [PATCH 05/53] v0.9 --- Rakefile | 2 +- rest-client.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index b2aa4b2..35d4987 100644 --- a/Rakefile +++ b/Rakefile @@ -31,7 +31,7 @@ require 'rake/gempackagetask' require 'rake/rdoctask' require 'fileutils' -version = "0.8.2" +version = "0.9" name = "rest-client" spec = Gem::Specification.new do |s| diff --git a/rest-client.gemspec b/rest-client.gemspec index df83c85..113da9e 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "rest-client" - s.version = "0.8.2" + s.version = "0.9" 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" From 2e8060c2be7b0af0efedad931a5563b780111bfe Mon Sep 17 00:00:00 2001 From: Dusty Doris Date: Sat, 31 Jan 2009 14:25:05 -0500 Subject: [PATCH 06/53] Added an option for open_timeout. This could be useful if you are not receive a response. An example would be a firewall that simply drops the packet --- README.rdoc | 2 +- lib/restclient/request.rb | 5 ++++- lib/restclient/resource.rb | 8 ++++++++ spec/request_spec.rb | 12 ++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/README.rdoc b/README.rdoc index 196b765..fe25841 100644 --- a/README.rdoc +++ b/README.rdoc @@ -21,7 +21,7 @@ See RestClient module docs for details. resource = RestClient::Resource.new 'http://example.com/resource' resource.get - private_resource = RestClient::Resource.new 'https://example.com/private/resource', :user => 'adam', :password => 'secret', :timeout => 20 + private_resource = RestClient::Resource.new 'https://example.com/private/resource', :user => 'adam', :password => 'secret', :timeout => 20, :open_timeout => 5 private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg' See RestClient::Resource module docs for details. diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 0149f3a..2a23f6d 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -6,7 +6,8 @@ module RestClient # RestClient::Request.execute(:method => :head, :url => 'http://example.com') # class Request - attr_reader :method, :url, :payload, :headers, :user, :password, :timeout + attr_reader :method, :url, :payload, :headers, :user, :password, :timeout, + :open_timeout def self.execute(args) new(args).execute @@ -20,6 +21,7 @@ module RestClient @user = args[:user] @password = args[:password] @timeout = args[:timeout] + @open_timeout = args[:open_timeout] end def execute @@ -94,6 +96,7 @@ module RestClient net.start do |http| http.read_timeout = @timeout if @timeout + http.open_timeout = @open_timeout if @open_timeout res = http.request(req, payload) display_log response_log(res) string = process_result(res) diff --git a/lib/restclient/resource.rb b/lib/restclient/resource.rb index ddfd876..4260e0e 100644 --- a/lib/restclient/resource.rb +++ b/lib/restclient/resource.rb @@ -16,6 +16,10 @@ module RestClient # # RestClient::Resource.new('http://slow', :timeout => 10) # + # With an open timeout (seconds): + # + # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10) + # # You can also use resources to share common headers. For headers keys, # symbols are converted to strings. Example: # @@ -94,6 +98,10 @@ module RestClient def timeout options[:timeout] end + + def open_timeout + options[:open_timeout] + end # Construct a subresource, preserving authentication. # diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 59afb94..45c50d5 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -293,4 +293,16 @@ describe RestClient::Request do @request.transmit(@uri, 'req', nil) end + + it "set open_timeout" do + @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + + @http.should_receive(:open_timeout=).with(123) + + @request.transmit(@uri, 'req', nil) + end + end From e53c3e12b1c3b162a77c68a71c933025b77c6798 Mon Sep 17 00:00:00 2001 From: Dusty Doris Date: Sat, 31 Jan 2009 14:43:45 -0500 Subject: [PATCH 07/53] timeout wasn't being set, had to move it before the net.start block --- lib/restclient/request.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 2a23f6d..a6213c1 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -91,12 +91,10 @@ module RestClient net = net_http_class.new(uri.host, uri.port) net.use_ssl = uri.is_a?(URI::HTTPS) net.verify_mode = OpenSSL::SSL::VERIFY_NONE - + net.read_timeout = @timeout if @timeout + net.open_timeout = @open_timeout if @open_timeout display_log request_log - net.start do |http| - http.read_timeout = @timeout if @timeout - http.open_timeout = @open_timeout if @open_timeout res = http.request(req, payload) display_log response_log(res) string = process_result(res) From 9cfa20255b4329fb5baf21176deb7d1d94566ce1 Mon Sep 17 00:00:00 2001 From: Dusty Doris Date: Sat, 31 Jan 2009 14:46:49 -0500 Subject: [PATCH 08/53] moved the spec on timeout to @net from @http --- spec/request_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 45c50d5..018c167 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -289,7 +289,7 @@ describe RestClient::Request do @request.stub!(:process_result) @request.stub!(:response_log) - @http.should_receive(:read_timeout=).with(123) + @net.should_receive(:read_timeout=).with(123) @request.transmit(@uri, 'req', nil) end @@ -300,7 +300,7 @@ describe RestClient::Request do @request.stub!(:process_result) @request.stub!(:response_log) - @http.should_receive(:open_timeout=).with(123) + @net.should_receive(:open_timeout=).with(123) @request.transmit(@uri, 'req', nil) end From 32c645151880c1712aa613c6b0c0f81688339596 Mon Sep 17 00:00:00 2001 From: Lennon Day-Reynolds Date: Thu, 5 Feb 2009 16:00:11 -0800 Subject: [PATCH 09/53] added basic cookies support to Request and Response classes --- lib/restclient/request.rb | 7 ++++++- lib/restclient/response.rb | 11 +++++++++++ spec/request_spec.rb | 7 +++++++ spec/response_spec.rb | 5 +++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 0149f3a..aacb585 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -6,7 +6,7 @@ module RestClient # RestClient::Request.execute(:method => :head, :url => 'http://example.com') # class Request - attr_reader :method, :url, :payload, :headers, :user, :password, :timeout + attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout def self.execute(args) new(args).execute @@ -16,6 +16,7 @@ module RestClient @method = args[:method] or raise ArgumentError, "must pass :method" @url = args[:url] or raise ArgumentError, "must pass :url" @headers = args[:headers] || {} + @cookies = @headers.delete(:cookies) || args[:cookies] || {} @payload = process_payload(args[:payload]) @user = args[:user] @password = args[:password] @@ -35,6 +36,10 @@ module RestClient end def make_headers(user_headers) + unless @cookies.empty? + user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ') + end + default_headers.merge(user_headers).inject({}) do |final, (key, value)| final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s final diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index c0169d6..c78b103 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -25,6 +25,17 @@ module RestClient @headers ||= self.class.beautify_headers(@net_http_res.to_hash) end + # Hash of cookies extracted from response headers + def cookies + @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c| + key, val = raw_c.split('=') + unless %w(expires domain path secure).member?(key) + out[key] = val + end + out + end + end + def self.beautify_headers(headers) headers.inject({}) do |out, (key, value)| out[key.gsub(/-/, '_').to_sym] = value.first diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 59afb94..acf495c 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -66,6 +66,13 @@ describe RestClient::Request do @request.password.should == 'pass2' end + it "correctly formats cookies provided to the constructor" do + URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) + @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' }) + @request.should_receive(:default_headers).and_return({'foo' => 'bar'}) + headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'} + end + it "determines the Net::HTTP class to instantiate by the method name" do @request.net_http_request_class(:put).should == Net::HTTP::Put end diff --git a/spec/response_spec.rb b/spec/response_spec.rb index e6663a6..e711b8d 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -30,6 +30,11 @@ describe RestClient::Response do @response.headers.should == { :content_type => 'text/html' } end + it "extracts headers from response headers" do + @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) + @response.cookies.should == { 'session_id' => '1' } + end + it "can access the net http result directly" do @response.net_http_res.should == @net_http_res end From be6ca03030e90e8db9d7773c0e01d50552b104f0 Mon Sep 17 00:00:00 2001 From: Lennon Day-Reynolds Date: Thu, 5 Feb 2009 16:02:17 -0800 Subject: [PATCH 10/53] fix formatting/typos in specs --- spec/request_spec.rb | 2 +- spec/response_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/request_spec.rb b/spec/request_spec.rb index acf495c..2f5a488 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -67,7 +67,7 @@ describe RestClient::Request do end it "correctly formats cookies provided to the constructor" do - URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) + URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' }) @request.should_receive(:default_headers).and_return({'foo' => 'bar'}) headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'} diff --git a/spec/response_spec.rb b/spec/response_spec.rb index e711b8d..6ac33f9 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -30,7 +30,7 @@ describe RestClient::Response do @response.headers.should == { :content_type => 'text/html' } end - it "extracts headers from response headers" do + it "extracts cookies from response headers" do @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) @response.cookies.should == { 'session_id' => '1' } end From 7949eeb509b9e78bf17b2f8198131a779e10b63a Mon Sep 17 00:00:00 2001 From: Lennon Day-Reynolds Date: Thu, 5 Feb 2009 16:23:42 -0800 Subject: [PATCH 11/53] added cookie examples to README --- README.rdoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.rdoc b/README.rdoc index 196b765..d461158 100644 --- a/README.rdoc +++ b/README.rdoc @@ -105,6 +105,22 @@ use whatever proxy the system is configured to use: RestClient.proxy = ENV['http_proxy'] +== Cookies + +Request and Response objects know about HTTP cookies, and will automatically +extract and set headers for them as needed: + + response = RestClient.get 'http://example.com/action_which_sets_session_id' + response.cookies + # => {"_applicatioN_session_id" => "1234"} + + response2 = RestClient.post( + 'http://localhost:3000/', + {:param1 => "foo"}, + {:cookies => {:session_id => "1234"}} + ) + # ...response body + == Meta Written by Adam Wiggins (adam at heroku dot com) From 35e67132820bf94d94cd7e10b666377cb00cbd9e Mon Sep 17 00:00:00 2001 From: James Edward Gray II Date: Fri, 6 Feb 2009 11:13:25 -0600 Subject: [PATCH 12/53] Addressing an issue that causes some successful responses to raise RequestFailed. --- lib/restclient/request.rb | 2 +- spec/request_spec.rb | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 0149f3a..7c5f7af 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -114,7 +114,7 @@ module RestClient end def process_result(res) - if %w(200 201 202).include? res.code + if res.code =~ /\A2\d{2}\z/ decode res['content-encoding'], res.body elsif %w(301 302 303).include? res.code url = res.header['Location'] diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 59afb94..59475a7 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -33,13 +33,23 @@ describe RestClient::Request 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 + 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 + + it "doesn't classify successful requests as failed" do + 203.upto(206) do |code| + res = mock("result") + res.stub!(:code).and_return(code.to_s) + res.stub!(:body).and_return("") + res.stub!(:[]).with('content-encoding').and_return(nil) + @request.process_result(res).should be_empty + end + end it "parses a url into a URI object" do URI.should_receive(:parse).with('http://example.com/resource') From 040544e952a9126d7a98cefe011ef12a634c8feb Mon Sep 17 00:00:00 2001 From: James Edward Gray II Date: Fri, 6 Feb 2009 11:34:03 -0600 Subject: [PATCH 13/53] Resolving an issue where a Content-Encoding header mixed with an empty body would raise an exception as Zlib choked on the data. --- lib/restclient/request.rb | 2 +- spec/request_spec.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 7c5f7af..3857c72 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -138,7 +138,7 @@ module RestClient end def decode(content_encoding, body) - if content_encoding == 'gzip' + if content_encoding == 'gzip' and not body.empty? Zlib::GzipReader.new(StringIO.new(body)).read elsif content_encoding == 'deflate' Zlib::Inflate.new.inflate(body) diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 59475a7..5b32df0 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -28,6 +28,10 @@ describe RestClient::Request do 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 "ingores gzip for empty bodies" do + @request.decode('gzip', '').should be_empty + 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" From 31a4909c624f4357b1c9d968c1842d585c435e52 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 7 Feb 2009 14:15:50 -0800 Subject: [PATCH 14/53] credits update --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index fe25841..e94c4d7 100644 --- a/README.rdoc +++ b/README.rdoc @@ -111,7 +111,7 @@ 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, Blake Mizerany, Brian Donovan, Ivan -Makfinsky, Marc-André Cournoyer, Coda Hale, and Tetsuo Watanabe +Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, and Dusty Doris Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From eda4c3571c17b25be1eb33f7deddf0269d566567 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 7 Feb 2009 14:23:29 -0800 Subject: [PATCH 15/53] credits update --- README.rdoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 0ef56b0..371d65c 100644 --- a/README.rdoc +++ b/README.rdoc @@ -127,7 +127,8 @@ 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, Blake Mizerany, Brian Donovan, Ivan -Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, and Dusty Doris +Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, and +Lennon Day-Reynolds Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From b5ab20ad518225c855f3780a6a4c2106890b3491 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 7 Feb 2009 14:52:31 -0800 Subject: [PATCH 16/53] credits update --- README.rdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rdoc b/README.rdoc index 371d65c..698a6d9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -127,8 +127,8 @@ 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, Blake Mizerany, Brian Donovan, Ivan -Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, and -Lennon Day-Reynolds +Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, +Lennon Day-Reynolds, and James Edward Gray II Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From 109ad553dcb7409ab6403db1b4d8addb3336741a Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Tue, 10 Feb 2009 13:55:54 -0800 Subject: [PATCH 17/53] RestClient.head --- lib/restclient.rb | 10 +++++++++- lib/restclient/request.rb | 5 ++++- lib/restclient/response.rb | 2 +- spec/request_spec.rb | 1 - spec/response_spec.rb | 4 ++++ spec/restclient_spec.rb | 5 +++++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/restclient.rb b/lib/restclient.rb index e2e32b2..f94ed43 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -31,10 +31,14 @@ require File.dirname(__FILE__) + '/restclient/exceptions' # # DELETE # RestClient.delete 'http://example.com/resource' # -# # retreive the response headers +# # retreive the response http code and headers # res = RestClient.get 'http://example.com/some.jpg' +# res.code # => 200 # res.headers[:content_type] # => 'image/jpg' # +# # HEAD +# RestClient.head('http://example.com').headers +# # To use with a proxy, just set RestClient.proxy to the proper http proxy: # # RestClient.proxy = "http://proxy.example.com/" @@ -65,6 +69,10 @@ module RestClient Request.execute(:method => :delete, :url => url, :headers => headers) end + def self.head(url, headers={}) + Request.execute(:method => :head, :url => url, :headers => headers) + end + class << self attr_accessor :proxy end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index aafed27..133d841 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -97,12 +97,15 @@ module RestClient net.verify_mode = OpenSSL::SSL::VERIFY_NONE net.read_timeout = @timeout if @timeout net.open_timeout = @open_timeout if @open_timeout + display_log request_log + net.start do |http| res = http.request(req, payload) display_log response_log(res) string = process_result(res) - if string + + if string or @method == :head Response.new(string, res) else nil diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index c78b103..b3a9cf5 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -10,7 +10,7 @@ module RestClient def initialize(string, net_http_res) @net_http_res = net_http_res - super string + super(string || "") end # HTTP status code, always 200 since RestClient throws exceptions for diff --git a/spec/request_spec.rb b/spec/request_spec.rb index cb7fdca..03da45d 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -325,5 +325,4 @@ describe RestClient::Request do @request.transmit(@uri, 'req', nil) end - end diff --git a/spec/response_spec.rb b/spec/response_spec.rb index 6ac33f9..81e1dbb 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -38,4 +38,8 @@ describe RestClient::Response do it "can access the net http result directly" do @response.net_http_res.should == @net_http_res end + + it "accepts nil strings and sets it to empty for the case of HEAD" do + RestClient::Response.new(nil, @net_http_res).should == "" + end end diff --git a/spec/restclient_spec.rb b/spec/restclient_spec.rb index 0c339e5..f1d7ba0 100644 --- a/spec/restclient_spec.rb +++ b/spec/restclient_spec.rb @@ -21,6 +21,11 @@ describe RestClient do RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}) RestClient.delete('http://some/resource') end + + it "HEAD" do + RestClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {}) + RestClient.head('http://some/resource') + end end describe "logging" do From 785ca179daeca205a711fa03908050ba9f7a8e6e Mon Sep 17 00:00:00 2001 From: Cyril Rohr Date: Mon, 2 Feb 2009 15:09:30 +0100 Subject: [PATCH 18/53] NotModified Exception should contain the response so that clients can have access to the response HTTP headers --- lib/restclient/exceptions.rb | 2 +- lib/restclient/request.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index c7303e9..5f4ae31 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -30,7 +30,7 @@ module RestClient end end - class NotModified < Exception + class NotModified < ExceptionWithResponse ErrorMessage = 'NotModified' end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 133d841..42fdadc 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -135,7 +135,7 @@ module RestClient raise Redirect, url elsif res.code == "304" - raise NotModified + raise NotModified, res elsif res.code == "401" raise Unauthorized, res elsif res.code == "404" From 63bdd45efdc913e219ec78c2ff29277190adcdfd Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Tue, 10 Feb 2009 14:01:36 -0800 Subject: [PATCH 19/53] credits --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 698a6d9..082d61e 100644 --- a/README.rdoc +++ b/README.rdoc @@ -128,7 +128,7 @@ 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, Blake Mizerany, Brian Donovan, Ivan Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, -Lennon Day-Reynolds, and James Edward Gray II +Lennon Day-Reynolds, James Edward Gray II, and Cyril Rohr Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From f2240dd361ec5a266ddfbb1eb9a6c5e652974baf Mon Sep 17 00:00:00 2001 From: Dylan Egan Date: Fri, 13 Feb 2009 14:43:26 +1100 Subject: [PATCH 20/53] Include missing files. Bump version. --- rest-client.gemspec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest-client.gemspec b/rest-client.gemspec index 113da9e..6289956 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "rest-client" - s.version = "0.9" + s.version = "0.9.1" 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" @@ -10,6 +10,7 @@ Gem::Specification.new do |s| s.has_rdoc = true s.platform = Gem::Platform::RUBY s.files = %w(Rakefile README.rdoc rest-client.gemspec + lib/rest_client.rb lib/restclient.rb lib/restclient/request.rb lib/restclient/response.rb lib/restclient/exceptions.rb lib/restclient/resource.rb spec/base.rb spec/request_spec.rb spec/response_spec.rb From 44cff0393be209b1045cc4d0d91ddf09795dc38d Mon Sep 17 00:00:00 2001 From: Juan Alvarez Date: Thu, 12 Feb 2009 23:08:37 -0600 Subject: [PATCH 21/53] Do not attempt to decompress response bodies for Net::HTTP::Head objects. --- lib/restclient/request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 42fdadc..bf1c263 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -123,7 +123,7 @@ module RestClient def process_result(res) if res.code =~ /\A2\d{2}\z/ - decode res['content-encoding'], res.body + decode res['content-encoding'], res.body if res.body elsif %w(301 302 303).include? res.code url = res.header['Location'] From fb9a69bab78ac0f95e95b636792167709593df18 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Tue, 17 Feb 2009 15:21:39 -0800 Subject: [PATCH 22/53] credits update --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 082d61e..34f31b9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -128,7 +128,7 @@ 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, Blake Mizerany, Brian Donovan, Ivan Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, -Lennon Day-Reynolds, James Edward Gray II, and Cyril Rohr +Lennon Day-Reynolds, James Edward Gray II, Cyril Rohr, and Juan Alvarez Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From f8dd948406590fbadf7888840be8f653a94864dd Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Thu, 12 Mar 2009 17:13:01 -0700 Subject: [PATCH 23/53] readme included in gem --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 35d4987..758c985 100644 --- a/Rakefile +++ b/Rakefile @@ -47,7 +47,7 @@ spec = Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.has_rdoc = true - s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*") + s.files = %w(Rakefile README.rdoc) + Dir.glob("{lib,spec}/**/*") s.executables = ['restclient'] s.require_path = "lib" From 3b01ab47337d135ecd11359cebb49d98fc56f2da Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Thu, 12 Mar 2009 17:13:38 -0700 Subject: [PATCH 24/53] v0.9.2 --- Rakefile | 2 +- rest-client.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 758c985..7b2889c 100644 --- a/Rakefile +++ b/Rakefile @@ -31,7 +31,7 @@ require 'rake/gempackagetask' require 'rake/rdoctask' require 'fileutils' -version = "0.9" +version = "0.9.2" name = "rest-client" spec = Gem::Specification.new do |s| diff --git a/rest-client.gemspec b/rest-client.gemspec index 6289956..51fc2c1 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "rest-client" - s.version = "0.9.1" + s.version = "0.9.2" 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" From 7be2beab34bd1f97e80426e04711f65900b69e18 Mon Sep 17 00:00:00 2001 From: Adam Jacob Date: Mon, 16 Mar 2009 14:09:34 -0700 Subject: [PATCH 25/53] Adding a switch to control ssl verification --- lib/restclient/request.rb | 5 +++-- spec/request_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index bf1c263..38ff2da 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -6,7 +6,7 @@ module RestClient # RestClient::Request.execute(:method => :head, :url => 'http://example.com') # class Request - attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout + attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, :verify_ssl def self.execute(args) new(args).execute @@ -22,6 +22,7 @@ module RestClient @password = args[:password] @timeout = args[:timeout] @open_timeout = args[:open_timeout] + @verify_ssl = args[:verify_ssl] || false end def execute @@ -94,7 +95,7 @@ module RestClient net = net_http_class.new(uri.host, uri.port) net.use_ssl = uri.is_a?(URI::HTTPS) - net.verify_mode = OpenSSL::SSL::VERIFY_NONE + net.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl == false net.read_timeout = @timeout if @timeout net.open_timeout = @open_timeout if @open_timeout diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 03da45d..94fa87d 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -325,4 +325,25 @@ describe RestClient::Request do @request.transmit(@uri, 'req', nil) end + + it "should default to not verifying ssl certificates" do + @request.verify_ssl.should == false + end + + it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do + @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end + + it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do + @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true) + @net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end end From c72955b142f556127bdf0b92187d7e00f12ac088 Mon Sep 17 00:00:00 2001 From: Adam Jacob Date: Mon, 16 Mar 2009 14:26:15 -0700 Subject: [PATCH 26/53] Added SSL client certificate support --- lib/restclient/request.rb | 9 ++++-- spec/request_spec.rb | 62 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 38ff2da..8437014 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -6,8 +6,9 @@ module RestClient # RestClient::Request.execute(:method => :head, :url => 'http://example.com') # class Request - attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, :verify_ssl - + attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, + :verify_ssl, :ssl_client_cert, :ssl_client_key + def self.execute(args) new(args).execute end @@ -23,6 +24,8 @@ module RestClient @timeout = args[:timeout] @open_timeout = args[:open_timeout] @verify_ssl = args[:verify_ssl] || false + @ssl_client_cert = args[:ssl_client_cert] || nil + @ssl_client_key = args[:ssl_client_key] || nil end def execute @@ -96,6 +99,8 @@ module RestClient net = net_http_class.new(uri.host, uri.port) net.use_ssl = uri.is_a?(URI::HTTPS) net.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl == false + net.cert = @ssl_client_cert if @ssl_client_cert + net.key = @ssl_client_key if @ssl_client_key net.read_timeout = @timeout if @timeout net.open_timeout = @open_timeout if @open_timeout diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 94fa87d..bbcb81f 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -346,4 +346,66 @@ describe RestClient::Request do @request.stub!(:response_log) @request.transmit(@uri, 'req', 'payload') end + + it "should default to not having an ssl_client_cert" do + @request.ssl_client_cert.should be(nil) + end + + it "should set the ssl_client_cert if provided" do + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload', + :ssl_client_cert => "whatsupdoc!" + ) + @net.should_receive(:cert=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end + + it "should not set the ssl_client_cert if it is not provided" do + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload' + ) + @net.should_not_receive(:cert=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end + + it "should default to not having an ssl_client_key" do + @request.ssl_client_key.should be(nil) + end + + it "should set the ssl_client_key if provided" do + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload', + :ssl_client_key => "whatsupdoc!" + ) + @net.should_receive(:key=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end + + it "should not set the ssl_client_key if it is not provided" do + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload' + ) + @net.should_not_receive(:key=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end end From 212ae67bfe3ad41d30ddd3b6cac47a0c60d70ee6 Mon Sep 17 00:00:00 2001 From: Adam Jacob Date: Mon, 16 Mar 2009 15:28:20 -0700 Subject: [PATCH 27/53] Added raw_request support, which downloads the body to a tempfile rather than a string --- lib/restclient.rb | 2 ++ lib/restclient/mixin/response.rb | 43 +++++++++++++++++++++++++++++ lib/restclient/raw_response.rb | 30 +++++++++++++++++++++ lib/restclient/request.rb | 36 +++++++++++++++++++++---- lib/restclient/response.rb | 34 +++-------------------- spec/mixin/response_spec.rb | 46 ++++++++++++++++++++++++++++++++ spec/raw_response_spec.rb | 17 ++++++++++++ spec/response_spec.rb | 29 -------------------- 8 files changed, 173 insertions(+), 64 deletions(-) create mode 100644 lib/restclient/mixin/response.rb create mode 100644 lib/restclient/raw_response.rb create mode 100644 spec/mixin/response_spec.rb create mode 100644 spec/raw_response_spec.rb diff --git a/lib/restclient.rb b/lib/restclient.rb index f94ed43..cabe873 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -4,7 +4,9 @@ require 'zlib' require 'stringio' require File.dirname(__FILE__) + '/restclient/request' +require File.dirname(__FILE__) + '/restclient/mixin/response' require File.dirname(__FILE__) + '/restclient/response' +require File.dirname(__FILE__) + '/restclient/raw_response' require File.dirname(__FILE__) + '/restclient/resource' require File.dirname(__FILE__) + '/restclient/exceptions' diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb new file mode 100644 index 0000000..168ee29 --- /dev/null +++ b/lib/restclient/mixin/response.rb @@ -0,0 +1,43 @@ +module RestClient + module Mixin + module Response + attr_reader :net_http_res + + # HTTP status code, always 200 since RestClient throws exceptions for + # other codes. + def code + @code ||= @net_http_res.code.to_i + end + + # A hash of the headers, beautified with symbols and underscores. + # e.g. "Content-type" will become :content_type. + def headers + @headers ||= self.class.beautify_headers(@net_http_res.to_hash) + end + + # Hash of cookies extracted from response headers + def cookies + @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c| + key, val = raw_c.split('=') + unless %w(expires domain path secure).member?(key) + out[key] = val + end + out + end + end + + def self.included(receiver) + receiver.extend(RestClient::Mixin::Response::ClassMethods) + end + + module ClassMethods + def beautify_headers(headers) + headers.inject({}) do |out, (key, value)| + out[key.gsub(/-/, '_').to_sym] = value.first + out + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb new file mode 100644 index 0000000..f4f02e6 --- /dev/null +++ b/lib/restclient/raw_response.rb @@ -0,0 +1,30 @@ +require File.dirname(__FILE__) + '/mixin/response' + +module RestClient + # The response from RestClient on a raw request looks like a string, but is + # actually one of these. 99% of the time you're making a rest call all you + # care about is the body, but on the occassion you want to fetch the + # headers you can: + # + # RestClient.get('http://example.com').headers[:content_type] + # + # In addition, if you do not use the response as a string, you can access + # a Tempfile object at res.file, which contains the path to the raw + # downloaded request body. + class RawResponse + include RestClient::Mixin::Response + + attr_reader :file + + def initialize(tempfile, net_http_res) + @net_http_res = net_http_res + @file = tempfile + end + + def to_s + @file.open + @file.read + end + + end +end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index bf1c263..e2ab396 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -1,3 +1,5 @@ +require 'tempfile' + module RestClient # This class is used internally by RestClient to send the request, but you can also # access it internally if you'd like to use a method not directly supported by the @@ -6,7 +8,7 @@ module RestClient # RestClient::Request.execute(:method => :head, :url => 'http://example.com') # class Request - attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout + attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, :raw_response def self.execute(args) new(args).execute @@ -22,6 +24,7 @@ module RestClient @password = args[:password] @timeout = args[:timeout] @open_timeout = args[:open_timeout] + @raw_response = args[:raw_response] || false end def execute @@ -103,10 +106,12 @@ module RestClient net.start do |http| res = http.request(req, payload) display_log response_log(res) - string = process_result(res) + result = process_result(res) - if string or @method == :head - Response.new(string, res) + if result.kind_of?(String) or @method == :head + Response.new(result, res) + elsif result.kind_of?(Tempfile) + RawResponse.new(result, res) else nil end @@ -123,7 +128,28 @@ module RestClient def process_result(res) if res.code =~ /\A2\d{2}\z/ - decode res['content-encoding'], res.body if res.body + if @raw_response + # Taken from Chef, which as in turn... + # Stolen from http://www.ruby-forum.com/topic/166423 + # Kudos to _why! + tf = Tempfile.new("rest-client") + size, total = 0, res.header['Content-Length'].to_i + res.read_body do |chunk| + tf.write(chunk) + size += chunk.size + if size == 0 + display_log("Request for #{@url} done (0 length file)") + elsif total == 0 + display_log("Request for #{@url} (zero content length)") + else + display_log("Request for #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total]) + end + end + tf.close + tf + else + decode res['content-encoding'], res.body if res.body + end elsif %w(301 302 303).include? res.code url = res.header['Location'] diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index b3a9cf5..0dcba9e 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -1,3 +1,5 @@ +require File.dirname(__FILE__) + '/mixin/response' + module RestClient # The response from RestClient looks like a string, but is actually one of # these. 99% of the time you're making a rest call all you care about is @@ -6,41 +8,13 @@ module RestClient # RestClient.get('http://example.com').headers[:content_type] # class Response < String - attr_reader :net_http_res + + include RestClient::Mixin::Response def initialize(string, net_http_res) @net_http_res = net_http_res super(string || "") end - # HTTP status code, always 200 since RestClient throws exceptions for - # other codes. - def code - @code ||= @net_http_res.code.to_i - end - - # A hash of the headers, beautified with symbols and underscores. - # e.g. "Content-type" will become :content_type. - def headers - @headers ||= self.class.beautify_headers(@net_http_res.to_hash) - end - - # Hash of cookies extracted from response headers - def cookies - @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c| - key, val = raw_c.split('=') - unless %w(expires domain path secure).member?(key) - out[key] = val - end - out - end - end - - def self.beautify_headers(headers) - headers.inject({}) do |out, (key, value)| - out[key.gsub(/-/, '_').to_sym] = value.first - out - end - end end end diff --git a/spec/mixin/response_spec.rb b/spec/mixin/response_spec.rb new file mode 100644 index 0000000..322fca3 --- /dev/null +++ b/spec/mixin/response_spec.rb @@ -0,0 +1,46 @@ +require File.dirname(__FILE__) + '/../base' + +class MockResponse + include RestClient::Mixin::Response + + def initialize(body, res) + @net_http_res = res + @body = @body + end +end + +describe RestClient::Mixin::Response do + before do + @net_http_res = mock('net http response') + @response = MockResponse.new('abc', @net_http_res) + end + + it "fetches the numeric response code" do + @net_http_res.should_receive(:code).and_return('200') + @response.code.should == 200 + end + + it "beautifies the headers by turning the keys to symbols" do + h = RestClient::Response.beautify_headers('content-type' => [ 'x' ]) + h.keys.first.should == :content_type + end + + it "beautifies the headers by turning the values to strings instead of one-element arrays" do + h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] ) + h.values.first.should == 'text/html' + end + + it "fetches the headers" do + @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ]) + @response.headers.should == { :content_type => 'text/html' } + end + + it "extracts cookies from response headers" do + @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) + @response.cookies.should == { 'session_id' => '1' } + end + + it "can access the net http result directly" do + @response.net_http_res.should == @net_http_res + end +end diff --git a/spec/raw_response_spec.rb b/spec/raw_response_spec.rb new file mode 100644 index 0000000..217a914 --- /dev/null +++ b/spec/raw_response_spec.rb @@ -0,0 +1,17 @@ +require File.dirname(__FILE__) + '/base' + +describe RestClient::RawResponse do + before do + @tf = mock("Tempfile", :read => "the answer is 42", :open => true) + @net_http_res = mock('net http response') + @response = RestClient::RawResponse.new(@tf, @net_http_res) + end + + it "behaves like string" do + @response.to_s.should == 'the answer is 42' + end + + it "exposes a Tempfile" do + @response.file.should == @tf + end +end diff --git a/spec/response_spec.rb b/spec/response_spec.rb index 81e1dbb..8b0a9d7 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -10,35 +10,6 @@ describe RestClient::Response do @response.should == 'abc' end - it "fetches the numeric response code" do - @net_http_res.should_receive(:code).and_return('200') - @response.code.should == 200 - end - - it "beautifies the headers by turning the keys to symbols" do - h = RestClient::Response.beautify_headers('content-type' => [ 'x' ]) - h.keys.first.should == :content_type - end - - it "beautifies the headers by turning the values to strings instead of one-element arrays" do - h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] ) - h.values.first.should == 'text/html' - end - - it "fetches the headers" do - @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ]) - @response.headers.should == { :content_type => 'text/html' } - end - - it "extracts cookies from response headers" do - @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) - @response.cookies.should == { 'session_id' => '1' } - end - - it "can access the net http result directly" do - @response.net_http_res.should == @net_http_res - end - it "accepts nil strings and sets it to empty for the case of HEAD" do RestClient::Response.new(nil, @net_http_res).should == "" end From e4430c10f31c59385af55c03bde1b0bdc79daec1 Mon Sep 17 00:00:00 2001 From: Adam Jacob Date: Mon, 16 Mar 2009 16:11:06 -0700 Subject: [PATCH 28/53] Processing the request body one time --- lib/restclient/request.rb | 65 ++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index e0cdad1..ff0cc75 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -29,6 +29,7 @@ module RestClient @verify_ssl = args[:verify_ssl] || false @ssl_client_cert = args[:ssl_client_cert] || nil @ssl_client_key = args[:ssl_client_key] || nil + @tf = nil # If you are a raw request, this is your tempfile end def execute @@ -110,14 +111,14 @@ module RestClient display_log request_log net.start do |http| - res = http.request(req, payload) - display_log response_log(res) + res = http.request(req, payload) { |http_response| fetch_body(http_response) } result = process_result(res) + display_log response_log(res) if result.kind_of?(String) or @method == :head Response.new(result, res) - elsif result.kind_of?(Tempfile) - RawResponse.new(result, res) + elsif @raw_response + RawResponse.new(@tf, res) else nil end @@ -131,31 +132,39 @@ module RestClient def setup_credentials(req) req.basic_auth(user, password) if user end + + def fetch_body(http_response) + if @raw_response + # Taken from Chef, which as in turn... + # Stolen from http://www.ruby-forum.com/topic/166423 + # Kudos to _why! + @tf = Tempfile.new("rest-client") + size, total = 0, http_response.header['Content-Length'].to_i + http_response.read_body do |chunk| + @tf.write(chunk) + size += chunk.size + if size == 0 + display_log("#{@method} #{@url} done (0 length file)") + elsif total == 0 + display_log("#{@method} #{@url} (zero content length)") + else + display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total]) + end + end + @tf.close + @tf + else + http_response.read_body + end + http_response + end def process_result(res) - if res.code =~ /\A2\d{2}\z/ - if @raw_response - # Taken from Chef, which as in turn... - # Stolen from http://www.ruby-forum.com/topic/166423 - # Kudos to _why! - tf = Tempfile.new("rest-client") - size, total = 0, res.header['Content-Length'].to_i - res.read_body do |chunk| - tf.write(chunk) - size += chunk.size - if size == 0 - display_log("Request for #{@url} done (0 length file)") - elsif total == 0 - display_log("Request for #{@url} (zero content length)") - else - display_log("Request for #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total]) - end - end - tf.close - tf - else - decode res['content-encoding'], res.body if res.body - end + if res.code =~ /\A2\d{2}\z/ + # We don't decode raw requests + unless @raw_response + decode res['content-encoding'], res.body if res.body + end elsif %w(301 302 303).include? res.code url = res.header['Location'] @@ -196,7 +205,7 @@ module RestClient end def response_log(res) - "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{(res.body) ? res.body.size : nil} bytes" + "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{(res.body) ? res['Content-Length'].to_i : nil} bytes" end def display_log(msg) From c6a2265f934fcb23d2505f6e85e6ba443a3be2d9 Mon Sep 17 00:00:00 2001 From: Adam Jacob Date: Mon, 16 Mar 2009 16:59:12 -0700 Subject: [PATCH 29/53] Fixing the response_log method to report the size on raw requests --- lib/restclient/request.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index ff0cc75..6050b8b 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -205,7 +205,8 @@ module RestClient end def response_log(res) - "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{(res.body) ? res['Content-Length'].to_i : nil} bytes" + size = @raw_response ? File.size(@tf.path) : res.body.size + "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes" end def display_log(msg) From 075c6746f24a68afebbbd653531810d0d4549b58 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Fri, 20 Mar 2009 19:19:21 -0700 Subject: [PATCH 30/53] whitespace fixes --- lib/restclient/mixin/response.rb | 78 ++++++------- lib/restclient/raw_response.rb | 12 +- lib/restclient/request.rb | 80 ++++++------- lib/restclient/resource.rb | 4 +- lib/restclient/response.rb | 2 +- spec/mixin/response_spec.rb | 20 ++-- spec/raw_response_spec.rb | 8 +- spec/request_spec.rb | 190 +++++++++++++++---------------- 8 files changed, 198 insertions(+), 196 deletions(-) diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb index 168ee29..102a692 100644 --- a/lib/restclient/mixin/response.rb +++ b/lib/restclient/mixin/response.rb @@ -1,43 +1,43 @@ module RestClient - module Mixin - module Response - attr_reader :net_http_res - - # HTTP status code, always 200 since RestClient throws exceptions for - # other codes. - def code - @code ||= @net_http_res.code.to_i - end + module Mixin + module Response + attr_reader :net_http_res - # A hash of the headers, beautified with symbols and underscores. - # e.g. "Content-type" will become :content_type. - def headers - @headers ||= self.class.beautify_headers(@net_http_res.to_hash) - end + # HTTP status code, always 200 since RestClient throws exceptions for + # other codes. + def code + @code ||= @net_http_res.code.to_i + end - # Hash of cookies extracted from response headers - def cookies - @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c| - key, val = raw_c.split('=') - unless %w(expires domain path secure).member?(key) - out[key] = val - end - out - end - end - - def self.included(receiver) - receiver.extend(RestClient::Mixin::Response::ClassMethods) - end + # A hash of the headers, beautified with symbols and underscores. + # e.g. "Content-type" will become :content_type. + def headers + @headers ||= self.class.beautify_headers(@net_http_res.to_hash) + end - module ClassMethods - def beautify_headers(headers) - headers.inject({}) do |out, (key, value)| - out[key.gsub(/-/, '_').to_sym] = value.first - out - end - end - end - end - end -end \ No newline at end of file + # Hash of cookies extracted from response headers + def cookies + @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c| + key, val = raw_c.split('=') + unless %w(expires domain path secure).member?(key) + out[key] = val + end + out + end + end + + def self.included(receiver) + receiver.extend(RestClient::Mixin::Response::ClassMethods) + end + + module ClassMethods + def beautify_headers(headers) + headers.inject({}) do |out, (key, value)| + out[key.gsub(/-/, '_').to_sym] = value.first + out + end + end + end + end + end +end diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb index f4f02e6..e31961e 100644 --- a/lib/restclient/raw_response.rb +++ b/lib/restclient/raw_response.rb @@ -12,8 +12,8 @@ module RestClient # a Tempfile object at res.file, which contains the path to the raw # downloaded request body. class RawResponse - include RestClient::Mixin::Response - + include RestClient::Mixin::Response + attr_reader :file def initialize(tempfile, net_http_res) @@ -21,10 +21,10 @@ module RestClient @file = tempfile end - def to_s - @file.open - @file.read - end + def to_s + @file.open + @file.read + end end end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 6050b8b..e372a31 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -6,10 +6,12 @@ module RestClient # main API. For example: # # RestClient::Request.execute(:method => :head, :url => 'http://example.com') - # + # class Request - attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, - :raw_response, :verify_ssl, :ssl_client_cert, :ssl_client_key + attr_reader :method, :url, :payload, :headers, + :cookies, :user, :password, :timeout, :open_timeout, + :verify_ssl, :ssl_client_cert, :ssl_client_key, + :raw_response def self.execute(args) new(args).execute @@ -19,7 +21,7 @@ module RestClient @method = args[:method] or raise ArgumentError, "must pass :method" @url = args[:url] or raise ArgumentError, "must pass :url" @headers = args[:headers] || {} - @cookies = @headers.delete(:cookies) || args[:cookies] || {} + @cookies = @headers.delete(:cookies) || args[:cookies] || {} @payload = process_payload(args[:payload]) @user = args[:user] @password = args[:password] @@ -45,13 +47,13 @@ module RestClient end def make_headers(user_headers) - unless @cookies.empty? - user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ') - end + unless @cookies.empty? + user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ') + end default_headers.merge(user_headers).inject({}) do |final, (key, value)| final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s - final + final end end @@ -118,7 +120,7 @@ module RestClient if result.kind_of?(String) or @method == :head Response.new(result, res) elsif @raw_response - RawResponse.new(@tf, res) + RawResponse.new(@tf, res) else nil end @@ -132,39 +134,39 @@ module RestClient def setup_credentials(req) req.basic_auth(user, password) if user end - + def fetch_body(http_response) - if @raw_response - # Taken from Chef, which as in turn... - # Stolen from http://www.ruby-forum.com/topic/166423 - # Kudos to _why! - @tf = Tempfile.new("rest-client") - size, total = 0, http_response.header['Content-Length'].to_i - http_response.read_body do |chunk| - @tf.write(chunk) - size += chunk.size - if size == 0 - display_log("#{@method} #{@url} done (0 length file)") - elsif total == 0 - display_log("#{@method} #{@url} (zero content length)") - else - display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total]) - end - end - @tf.close - @tf - else - http_response.read_body - end - http_response - end + if @raw_response + # Taken from Chef, which as in turn... + # Stolen from http://www.ruby-forum.com/topic/166423 + # Kudos to _why! + @tf = Tempfile.new("rest-client") + size, total = 0, http_response.header['Content-Length'].to_i + http_response.read_body do |chunk| + @tf.write(chunk) + size += chunk.size + if size == 0 + display_log("#{@method} #{@url} done (0 length file)") + elsif total == 0 + display_log("#{@method} #{@url} (zero content length)") + else + display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total]) + end + end + @tf.close + @tf + else + http_response.read_body + end + http_response + end def process_result(res) if res.code =~ /\A2\d{2}\z/ - # We don't decode raw requests - unless @raw_response - decode res['content-encoding'], res.body if res.body - end + # We don't decode raw requests + unless @raw_response + decode res['content-encoding'], res.body if res.body + end elsif %w(301 302 303).include? res.code url = res.header['Location'] @@ -205,7 +207,7 @@ module RestClient end def response_log(res) - size = @raw_response ? File.size(@tf.path) : res.body.size + size = @raw_response ? File.size(@tf.path) : res.body.size "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes" end diff --git a/lib/restclient/resource.rb b/lib/restclient/resource.rb index 4260e0e..b66ac05 100644 --- a/lib/restclient/resource.rb +++ b/lib/restclient/resource.rb @@ -100,8 +100,8 @@ module RestClient end def open_timeout - options[:open_timeout] - end + options[:open_timeout] + end # Construct a subresource, preserving authentication. # diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index 0dcba9e..07f21db 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -8,7 +8,7 @@ module RestClient # RestClient.get('http://example.com').headers[:content_type] # class Response < String - + include RestClient::Mixin::Response def initialize(string, net_http_res) diff --git a/spec/mixin/response_spec.rb b/spec/mixin/response_spec.rb index 322fca3..ab1bae4 100644 --- a/spec/mixin/response_spec.rb +++ b/spec/mixin/response_spec.rb @@ -1,12 +1,12 @@ require File.dirname(__FILE__) + '/../base' class MockResponse - include RestClient::Mixin::Response - - def initialize(body, res) - @net_http_res = res - @body = @body - end + include RestClient::Mixin::Response + + def initialize(body, res) + @net_http_res = res + @body = @body + end end describe RestClient::Mixin::Response do @@ -35,10 +35,10 @@ describe RestClient::Mixin::Response do @response.headers.should == { :content_type => 'text/html' } end - it "extracts cookies from response headers" do - @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) - @response.cookies.should == { 'session_id' => '1' } - end + it "extracts cookies from response headers" do + @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/']) + @response.cookies.should == { 'session_id' => '1' } + end it "can access the net http result directly" do @response.net_http_res.should == @net_http_res diff --git a/spec/raw_response_spec.rb b/spec/raw_response_spec.rb index 217a914..55e9785 100644 --- a/spec/raw_response_spec.rb +++ b/spec/raw_response_spec.rb @@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/base' describe RestClient::RawResponse do before do - @tf = mock("Tempfile", :read => "the answer is 42", :open => true) + @tf = mock("Tempfile", :read => "the answer is 42", :open => true) @net_http_res = mock('net http response') @response = RestClient::RawResponse.new(@tf, @net_http_res) end @@ -11,7 +11,7 @@ describe RestClient::RawResponse do @response.to_s.should == 'the answer is 42' end - it "exposes a Tempfile" do - @response.file.should == @tf - end + it "exposes a Tempfile" do + @response.file.should == @tf + end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index bbcb81f..3dc9f38 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -28,7 +28,7 @@ describe RestClient::Request do 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 "ingores gzip for empty bodies" do @request.decode('gzip', '').should be_empty end @@ -37,23 +37,23 @@ describe RestClient::Request 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 + 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 - it "doesn't classify successful requests as failed" do - 203.upto(206) do |code| - res = mock("result") - res.stub!(:code).and_return(code.to_s) - res.stub!(:body).and_return("") - res.stub!(:[]).with('content-encoding').and_return(nil) - @request.process_result(res).should be_empty - end - end + it "doesn't classify successful requests as failed" do + 203.upto(206) do |code| + res = mock("result") + res.stub!(:code).and_return(code.to_s) + res.stub!(:body).and_return("") + res.stub!(:[]).with('content-encoding').and_return(nil) + @request.process_result(res).should be_empty + end + end it "parses a url into a URI object" do URI.should_receive(:parse).with('http://example.com/resource') @@ -80,12 +80,12 @@ describe RestClient::Request do @request.password.should == 'pass2' end - it "correctly formats cookies provided to the constructor" do - URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) - @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' }) - @request.should_receive(:default_headers).and_return({'foo' => 'bar'}) - headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'} - end + it "correctly formats cookies provided to the constructor" do + URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil)) + @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' }) + @request.should_receive(:default_headers).and_return({'foo' => 'bar'}) + headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'} + end it "determines the Net::HTTP class to instantiate by the method name" do @request.net_http_request_class(:put).should == Net::HTTP::Put @@ -243,7 +243,7 @@ describe RestClient::Request do end it "creates a non-proxy class if a proxy url is not given" do - @request.net_http_class.should_not include(Net::HTTP::ProxyDelta) + @request.net_http_class.should_not include(Net::HTTP::ProxyDelta) end it "logs a get request" do @@ -303,109 +303,109 @@ describe RestClient::Request do f.should_receive(:puts).with('xyz') @request.display_log('xyz') end - + it "set read_timeout" do @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123) @http.stub!(:request) @request.stub!(:process_result) @request.stub!(:response_log) - + @net.should_receive(:read_timeout=).with(123) - + @request.transmit(@uri, 'req', nil) end - + it "set open_timeout" do @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123) @http.stub!(:request) @request.stub!(:process_result) @request.stub!(:response_log) - + @net.should_receive(:open_timeout=).with(123) - + @request.transmit(@uri, 'req', nil) end - + it "should default to not verifying ssl certificates" do - @request.verify_ssl.should == false + @request.verify_ssl.should == false end - + it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do - @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') + @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') end - + it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do - @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true) - @net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') + @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true) + @net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') end - + it "should default to not having an ssl_client_cert" do - @request.ssl_client_cert.should be(nil) + @request.ssl_client_cert.should be(nil) end - + it "should set the ssl_client_cert if provided" do - @request = RestClient::Request.new( - :method => :put, - :url => 'https://some/resource', - :payload => 'payload', - :ssl_client_cert => "whatsupdoc!" - ) - @net.should_receive(:cert=).with("whatsupdoc!") - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload', + :ssl_client_cert => "whatsupdoc!" + ) + @net.should_receive(:cert=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') end - + it "should not set the ssl_client_cert if it is not provided" do - @request = RestClient::Request.new( - :method => :put, - :url => 'https://some/resource', - :payload => 'payload' - ) - @net.should_not_receive(:cert=).with("whatsupdoc!") - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload' + ) + @net.should_not_receive(:cert=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') end - + it "should default to not having an ssl_client_key" do - @request.ssl_client_key.should be(nil) + @request.ssl_client_key.should be(nil) end - + it "should set the ssl_client_key if provided" do - @request = RestClient::Request.new( - :method => :put, - :url => 'https://some/resource', - :payload => 'payload', - :ssl_client_key => "whatsupdoc!" - ) - @net.should_receive(:key=).with("whatsupdoc!") - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload', + :ssl_client_key => "whatsupdoc!" + ) + @net.should_receive(:key=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') end - + it "should not set the ssl_client_key if it is not provided" do - @request = RestClient::Request.new( - :method => :put, - :url => 'https://some/resource', - :payload => 'payload' - ) - @net.should_not_receive(:key=).with("whatsupdoc!") - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload' + ) + @net.should_not_receive(:key=).with("whatsupdoc!") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') end end From 569b389b37d5af3e80312bb2d4becddccfda276b Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Fri, 20 Mar 2009 19:37:25 -0700 Subject: [PATCH 31/53] readme for ssl client certs --- README.rdoc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 34f31b9..6c11be7 100644 --- a/README.rdoc +++ b/README.rdoc @@ -121,6 +121,12 @@ extract and set headers for them as needed: ) # ...response body +== SSL Client Certificates + + RestClient.get('https://example.com', :ssl_client_cert => File.read('cert.pem'), :ssl_client_key => File.read('key.pem') + +Self-signed certificates can be generated with the openssl command-line tool. + == Meta Written by Adam Wiggins (adam at heroku dot com) @@ -128,7 +134,8 @@ 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, Blake Mizerany, Brian Donovan, Ivan Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, -Lennon Day-Reynolds, James Edward Gray II, Cyril Rohr, and Juan Alvarez +Lennon Day-Reynolds, James Edward Gray II, Cyril Rohr, Juan Alvarez, and Adam +Jacob Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From 3fb5c51d1e2fb522b90a8570a04b91dd3e620484 Mon Sep 17 00:00:00 2001 From: Cyril Rohr Date: Mon, 30 Mar 2009 14:03:40 +0200 Subject: [PATCH 32/53] Can now pass a CA_FILE for SSL peer verification. Fixed README part related to SSL Client Certificates. --- README.rdoc | 8 +++++++- lib/restclient/request.rb | 4 +++- spec/request_spec.rb | 31 +++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/README.rdoc b/README.rdoc index 6c11be7..c005e9f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -123,7 +123,13 @@ extract and set headers for them as needed: == SSL Client Certificates - RestClient.get('https://example.com', :ssl_client_cert => File.read('cert.pem'), :ssl_client_key => File.read('key.pem') + RestClient::Resource.new( + 'https://example.com', + :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read("cert.pem")), + :ssl_client_key => OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"), + :ssl_ca_file => "ca_certificate.pem", + :verify_ssl => OpenSSL::SSL::VERIFY_PEER + ).get Self-signed certificates can be generated with the openssl command-line tool. diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index e372a31..45ed910 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -10,7 +10,7 @@ module RestClient class Request attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, - :verify_ssl, :ssl_client_cert, :ssl_client_key, + :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file :raw_response def self.execute(args) @@ -31,6 +31,7 @@ module RestClient @verify_ssl = args[:verify_ssl] || false @ssl_client_cert = args[:ssl_client_cert] || nil @ssl_client_key = args[:ssl_client_key] || nil + @ssl_ca_file = args[:ssl_ca_file] || nil @tf = nil # If you are a raw request, this is your tempfile end @@ -107,6 +108,7 @@ module RestClient net.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl == false net.cert = @ssl_client_cert if @ssl_client_cert net.key = @ssl_client_key if @ssl_client_key + net.ca_file = @ssl_ca_file if @ssl_ca_file net.read_timeout = @timeout if @timeout net.open_timeout = @open_timeout if @open_timeout diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 3dc9f38..461ebe9 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -408,4 +408,35 @@ describe RestClient::Request do @request.stub!(:response_log) @request.transmit(@uri, 'req', 'payload') end + + it "should default to not having an ssl_ca_file" do + @request.ssl_ca_file.should be(nil) + end + + it "should set the ssl_ca_file if provided" do + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload', + :ssl_ca_file => "Certificate Authority File" + ) + @net.should_receive(:ca_file=).with("Certificate Authority File") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end + + it "should not set the ssl_ca_file if it is not provided" do + @request = RestClient::Request.new( + :method => :put, + :url => 'https://some/resource', + :payload => 'payload' + ) + @net.should_not_receive(:ca_file=).with("Certificate Authority File") + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end end From d24758aaf5fa642d858f7fc26cb93a61d9e8da99 Mon Sep 17 00:00:00 2001 From: James Edward Gray II Date: Mon, 6 Apr 2009 18:48:03 -0500 Subject: [PATCH 33/53] Adding a missing comma which restores a reader method and silences a warning. --- lib/restclient/request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 45ed910..7d69bd8 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -10,7 +10,7 @@ module RestClient class Request attr_reader :method, :url, :payload, :headers, :cookies, :user, :password, :timeout, :open_timeout, - :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file + :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :raw_response def self.execute(args) From 97698bcd63bfbc43da485fcbe80a999faef8a6e6 Mon Sep 17 00:00:00 2001 From: James Edward Gray II Date: Tue, 26 May 2009 15:51:13 -0500 Subject: [PATCH 34/53] Making it possible to actually set OpenSSL's verify_mode using OpenSSL constants. This makes the example in the README function and makes it possible to set more complex schemes like :verify_mode => OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT. --- lib/restclient/request.rb | 6 +++++- spec/request_spec.rb | 28 +++++++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 7d69bd8..b7ff76b 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -105,7 +105,11 @@ module RestClient net = net_http_class.new(uri.host, uri.port) net.use_ssl = uri.is_a?(URI::HTTPS) - net.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl == false + if @verify_ssl == false + net.verify_mode = OpenSSL::SSL::VERIFY_NONE + elsif @verify_ssl.is_a? Integer + net.verify_mode = @verify_ssl + end net.cert = @ssl_client_cert if @ssl_client_cert net.key = @ssl_client_key if @ssl_client_key net.ca_file = @ssl_ca_file if @ssl_ca_file diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 461ebe9..c56f158 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -330,13 +330,13 @@ describe RestClient::Request do @request.verify_ssl.should == false end - it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do - @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) - @http.stub!(:request) - @request.stub!(:process_result) - @request.stub!(:response_log) - @request.transmit(@uri, 'req', 'payload') - end + it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do + @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do @request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true) @@ -347,6 +347,20 @@ describe RestClient::Request do @request.transmit(@uri, 'req', 'payload') end + it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do + mode = OpenSSL::SSL::VERIFY_PEER | + OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + @request = RestClient::Request.new( :method => :put, + :url => 'https://some/resource', + :payload => 'payload', + :verify_ssl => mode ) + @net.should_receive(:verify_mode=).with(mode) + @http.stub!(:request) + @request.stub!(:process_result) + @request.stub!(:response_log) + @request.transmit(@uri, 'req', 'payload') + end + it "should default to not having an ssl_client_cert" do @request.ssl_client_cert.should be(nil) end From fe05bb0bb74c047e6a59fbe64fdb1bd54ec10d22 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Thu, 28 May 2009 19:02:21 -0700 Subject: [PATCH 35/53] v1.0 --- Rakefile | 2 +- rest-client.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 7b2889c..78c4d9c 100644 --- a/Rakefile +++ b/Rakefile @@ -31,7 +31,7 @@ require 'rake/gempackagetask' require 'rake/rdoctask' require 'fileutils' -version = "0.9.2" +version = "1.0" name = "rest-client" spec = Gem::Specification.new do |s| diff --git a/rest-client.gemspec b/rest-client.gemspec index 51fc2c1..7edf56e 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "rest-client" - s.version = "0.9.2" + s.version = "1.0" 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" From 5f03b02fed1e52df639bd799bbde47701108f2e9 Mon Sep 17 00:00:00 2001 From: Paul Dlug Date: Sat, 30 May 2009 11:14:50 -0400 Subject: [PATCH 36/53] Handle nil response body in response_log --- lib/restclient/request.rb | 2 +- spec/request_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index b7ff76b..3160979 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -213,7 +213,7 @@ module RestClient end def response_log(res) - size = @raw_response ? File.size(@tf.path) : res.body.size + size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size) "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes" end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index c56f158..0330caa 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -278,6 +278,12 @@ describe RestClient::Request do @request.response_log(res).should == "# => 200 OK | 4 bytes" end + it "logs a response with a nil body" do + res = mock('result', :code => '200', :class => Net::HTTPOK, :body => nil) + res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8') + @request.response_log(res).should == "# => 200 OK | text/html 0 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') From 07a39684878e497a3065ad1228fe240ea1fcf6fb Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sun, 31 May 2009 13:32:32 -0700 Subject: [PATCH 37/53] credits update --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index c005e9f..e68dac1 100644 --- a/README.rdoc +++ b/README.rdoc @@ -141,7 +141,7 @@ Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, Blake Mizerany, Brian Donovan, Ivan Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, Lennon Day-Reynolds, James Edward Gray II, Cyril Rohr, Juan Alvarez, and Adam -Jacob +Jacob, and Paul Dlug Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From 482ea2301e71ef9e70615a87d4b7b5b53ac8ee64 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Mon, 1 Jun 2009 15:53:39 -0700 Subject: [PATCH 38/53] add mixin to gemspec --- rest-client.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/rest-client.gemspec b/rest-client.gemspec index 7edf56e..7ab5382 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -13,6 +13,7 @@ Gem::Specification.new do |s| lib/rest_client.rb lib/restclient.rb lib/restclient/request.rb lib/restclient/response.rb lib/restclient/exceptions.rb lib/restclient/resource.rb + lib/restclient/mixin/response.rb spec/base.rb spec/request_spec.rb spec/response_spec.rb spec/exceptions_spec.rb spec/resource_spec.rb spec/restclient_spec.rb bin/restclient) From 7abe01b16effd4305ff2c68295ae9557c3f0c965 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Fri, 5 Jun 2009 16:42:36 -0700 Subject: [PATCH 39/53] v1.0.1 --- Rakefile | 2 +- rest-client.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 78c4d9c..bb52c98 100644 --- a/Rakefile +++ b/Rakefile @@ -31,7 +31,7 @@ require 'rake/gempackagetask' require 'rake/rdoctask' require 'fileutils' -version = "1.0" +version = "1.0.1" name = "rest-client" spec = Gem::Specification.new do |s| diff --git a/rest-client.gemspec b/rest-client.gemspec index 7ab5382..41f8a05 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "rest-client" - s.version = "1.0" + s.version = "1.0.1" 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" From 6e542547db0a9fd72240632e970b9e63b71474db Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sat, 20 Jun 2009 15:19:56 -0700 Subject: [PATCH 40/53] use jeweler (v1.0.2) --- Rakefile | 69 ++++++++++++------------------------- VERSION | 1 + rest-client.gemspec | 84 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 86 insertions(+), 68 deletions(-) create mode 100644 VERSION diff --git a/Rakefile b/Rakefile index bb52c98..6b4dbf1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,24 @@ require 'rake' + +require 'jeweler' + +Jeweler::Tasks.new do |s| + s.name = "rest-client" + s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete." + s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions." + s.author = "Adam Wiggins" + s.email = "adam@heroku.com" + s.homepage = "http://rest-client.heroku.com/" + s.rubyforge_project = "rest-client" + s.has_rdoc = true + s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"] + s.executables = %w(restclient) +end + +Jeweler::RubyforgeTasks.new + +############################ + require 'spec/rake/spectask' desc "Run all specs" @@ -22,54 +42,9 @@ end task :default => :spec -###################################################### +############################ -require 'rake' -require 'rake/testtask' -require 'rake/clean' -require 'rake/gempackagetask' require 'rake/rdoctask' -require 'fileutils' - -version = "1.0.1" -name = "rest-client" - -spec = Gem::Specification.new do |s| - s.name = name - s.version = version - 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" - s.email = "adam@heroku.com" - s.homepage = "http://rest-client.heroku.com/" - s.rubyforge_project = "rest-client" - - s.platform = Gem::Platform::RUBY - s.has_rdoc = true - - s.files = %w(Rakefile README.rdoc) + Dir.glob("{lib,spec}/**/*") - s.executables = ['restclient'] - - s.require_path = "lib" -end - -Rake::GemPackageTask.new(spec) do |p| - p.need_tar = true if RUBY_PLATFORM !~ /mswin/ -end - -task :install => [ :package ] do - sh %{sudo gem install pkg/#{name}-#{version}.gem} -end - -task :uninstall => [ :clean ] do - sh %{sudo gem uninstall #{name}} -end - -Rake::TestTask.new do |t| - t.libs << "spec" - t.test_files = FileList['spec/*_spec.rb'] - t.verbose = true -end Rake::RDocTask.new do |t| t.rdoc_dir = 'rdoc' @@ -81,5 +56,3 @@ Rake::RDocTask.new do |t| t.rdoc_files.include('lib/restclient/*.rb') end -CLEAN.include [ 'pkg', '*.gem', '.config' ] - diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6d7de6e --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.2 diff --git a/rest-client.gemspec b/rest-client.gemspec index 41f8a05..6938046 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,22 +1,66 @@ +# -*- encoding: utf-8 -*- + Gem::Specification.new do |s| - s.name = "rest-client" - s.version = "1.0.1" - 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" - s.email = "adam@heroku.com" - s.rubyforge_project = "rest-client" - s.homepage = "http://rest-client.heroku.com/" - s.has_rdoc = true - s.platform = Gem::Platform::RUBY - s.files = %w(Rakefile README.rdoc rest-client.gemspec - lib/rest_client.rb lib/restclient.rb - lib/restclient/request.rb lib/restclient/response.rb - lib/restclient/exceptions.rb lib/restclient/resource.rb - lib/restclient/mixin/response.rb - spec/base.rb spec/request_spec.rb spec/response_spec.rb - spec/exceptions_spec.rb spec/resource_spec.rb spec/restclient_spec.rb - bin/restclient) - s.executables = ['restclient'] - s.require_path = "lib" + s.name = %q{rest-client} + s.version = "1.0.2" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.authors = ["Adam Wiggins"] + s.date = %q{2009-06-20} + s.default_executable = %q{restclient} + s.description = %q{A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.} + s.email = %q{adam@heroku.com} + s.executables = ["restclient"] + s.extra_rdoc_files = [ + "README.rdoc" + ] + s.files = [ + "README.rdoc", + "Rakefile", + "VERSION", + "bin/restclient", + "lib/rest_client.rb", + "lib/restclient.rb", + "lib/restclient/exceptions.rb", + "lib/restclient/mixin/response.rb", + "lib/restclient/raw_response.rb", + "lib/restclient/request.rb", + "lib/restclient/resource.rb", + "lib/restclient/response.rb", + "spec/base.rb", + "spec/exceptions_spec.rb", + "spec/mixin/response_spec.rb", + "spec/raw_response_spec.rb", + "spec/request_spec.rb", + "spec/resource_spec.rb", + "spec/response_spec.rb", + "spec/restclient_spec.rb" + ] + s.has_rdoc = true + s.homepage = %q{http://rest-client.heroku.com/} + s.rdoc_options = ["--charset=UTF-8"] + s.require_paths = ["lib"] + s.rubyforge_project = %q{rest-client} + s.rubygems_version = %q{1.3.1} + s.summary = %q{Simple REST client for Ruby, inspired by microframework syntax for specifying actions.} + s.test_files = [ + "spec/base.rb", + "spec/exceptions_spec.rb", + "spec/mixin/response_spec.rb", + "spec/raw_response_spec.rb", + "spec/request_spec.rb", + "spec/resource_spec.rb", + "spec/response_spec.rb", + "spec/restclient_spec.rb" + ] + + if s.respond_to? :specification_version then + current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION + s.specification_version = 2 + + if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then + else + end + else + end end From 2db1f0bc3338e28146b86c605664956e1e3534fe Mon Sep 17 00:00:00 2001 From: Brad Ediger Date: Wed, 17 Jun 2009 14:55:16 -0500 Subject: [PATCH 41/53] 30x redirects should use GET, not the original HTTP verb --- lib/restclient/request.rb | 2 ++ spec/request_spec.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 3160979..f303144 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -39,6 +39,8 @@ module RestClient execute_inner rescue Redirect => e @url = e.url + @method = :get + @payload = nil execute end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 0330caa..ca4dbe5 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -222,6 +222,21 @@ describe RestClient::Request do lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } end + it "uses GET and clears payload when following 30x redirects" do + url = "http://example.com/redirected" + + @request.should_receive(:execute_inner).once.ordered. + and_raise(RestClient::Redirect.new(url)) + + @request.should_receive(:execute_inner).once.ordered do + @request.url.should == url + @request.method.should == :get + @request.payload.should be_nil + end + + @request.execute + end + it "raises Unauthorized when the response is 401" do res = mock('response', :code => '401') lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized) From 2cab7a173a00147e23aea64adc4c9c2c558c0e99 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Mon, 22 Jun 2009 20:58:30 -0700 Subject: [PATCH 42/53] whitespace --- spec/request_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/request_spec.rb b/spec/request_spec.rb index ca4dbe5..1a34668 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -225,12 +225,11 @@ describe RestClient::Request do it "uses GET and clears payload when following 30x redirects" do url = "http://example.com/redirected" - @request.should_receive(:execute_inner).once.ordered. - and_raise(RestClient::Redirect.new(url)) + @request.should_receive(:execute_inner).once.ordered.and_raise(RestClient::Redirect.new(url)) @request.should_receive(:execute_inner).once.ordered do - @request.url.should == url - @request.method.should == :get + @request.url.should == url + @request.method.should == :get @request.payload.should be_nil end From 6935bb98ca5096ec0e96ac336060c0c005d572e6 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Mon, 22 Jun 2009 20:58:55 -0700 Subject: [PATCH 43/53] credits update --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index e68dac1..91b1111 100644 --- a/README.rdoc +++ b/README.rdoc @@ -141,7 +141,7 @@ Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, Blake Mizerany, Brian Donovan, Ivan Makfinsky, Marc-André Cournoyer, Coda Hale, Tetsuo Watanabe, Dusty Doris, Lennon Day-Reynolds, James Edward Gray II, Cyril Rohr, Juan Alvarez, and Adam -Jacob, and Paul Dlug +Jacob, Paul Dlug, and Brad Ediger Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From 742bc77a842dbf7f0de7826046ff38dfaab0633b Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Wed, 24 Jun 2009 12:11:02 -0700 Subject: [PATCH 44/53] detect when net/https is not available and suggest installing libopenssl-ruby --- lib/restclient.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/restclient.rb b/lib/restclient.rb index cabe873..7dfcfe9 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -1,8 +1,14 @@ require 'uri' -require 'net/https' require 'zlib' require 'stringio' +begin + require 'net/https' +rescue LoadError => e + raise e unless RUBY_PLATFORM =~ /linux/ + raise LoadError, "no such file to load -- net/https. Try running apt-get install libopenssl-ruby" +end + require File.dirname(__FILE__) + '/restclient/request' require File.dirname(__FILE__) + '/restclient/mixin/response' require File.dirname(__FILE__) + '/restclient/response' From cb0dd1fd3b50439ea16553d5b6ccecd699e7f253 Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Mon, 29 Jun 2009 16:22:54 -0700 Subject: [PATCH 45/53] decode response body for exceptions too (401, 404, etc) --- lib/restclient/exceptions.rb | 4 ++++ lib/restclient/request.rb | 4 ++-- spec/exceptions_spec.rb | 15 +++++++++++++-- spec/request_spec.rb | 8 ++++---- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 5f4ae31..1d0ce91 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -18,6 +18,10 @@ module RestClient def http_code @response.code.to_i if @response end + + def http_body + RestClient::Request.decode(@response['content-encoding'], @response.body) if @response + end end # A redirect was encountered; caught by execute to retry with the new url. diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index f303144..71f1b05 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -173,7 +173,7 @@ module RestClient if res.code =~ /\A2\d{2}\z/ # We don't decode raw requests unless @raw_response - decode res['content-encoding'], res.body if res.body + self.class.decode res['content-encoding'], res.body if res.body end elsif %w(301 302 303).include? res.code url = res.header['Location'] @@ -196,7 +196,7 @@ module RestClient end end - def decode(content_encoding, body) + def self.decode(content_encoding, body) if content_encoding == 'gzip' and not body.empty? Zlib::GzipReader.new(StringIO.new(body)).read elsif content_encoding == 'deflate' diff --git a/spec/exceptions_spec.rb b/spec/exceptions_spec.rb index fe6449b..b23b647 100644 --- a/spec/exceptions_spec.rb +++ b/spec/exceptions_spec.rb @@ -12,6 +12,10 @@ describe RestClient::Exception do end describe RestClient::RequestFailed do + before do + @response = mock('HTTP Response', :code => '502') + end + it "stores the http response on the exception" do begin raise RestClient::RequestFailed, :response @@ -21,11 +25,18 @@ describe RestClient::RequestFailed do end it "http_code convenience method for fetching the code as an integer" do - RestClient::RequestFailed.new(mock('res', :code => '502')).http_code.should == 502 + RestClient::RequestFailed.new(@response).http_code.should == 502 + end + + it "http_body convenience method for fetching the body (decoding when necessary)" do + @response.stub!(:[]).with('content-encoding').and_return('gzip') + @response.stub!(:body).and_return('compressed body') + RestClient::Request.should_receive(:decode).with('gzip', 'compressed body').and_return('plain body') + RestClient::RequestFailed.new(@response).http_body.should == 'plain body' end it "shows the status code in the message" do - RestClient::RequestFailed.new(mock('res', :code => '502')).to_s.should match(/502/) + RestClient::RequestFailed.new(@response).to_s.should match(/502/) end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 1a34668..47842e2 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -22,19 +22,19 @@ describe RestClient::Request do end it "decodes an uncompressed result body by passing it straight through" do - @request.decode(nil, 'xyz').should == 'xyz' + RestClient::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" + RestClient::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 "ingores gzip for empty bodies" do - @request.decode('gzip', '').should be_empty + RestClient::Request.decode('gzip', '').should be_empty 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" + RestClient::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 From 1f55b447e962c9eb93db78f615f9b2e4da0a033b Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Mon, 29 Jun 2009 16:25:52 -0700 Subject: [PATCH 46/53] v1.0.3 --- VERSION | 2 +- rest-client.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 6d7de6e..21e8796 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.2 +1.0.3 diff --git a/rest-client.gemspec b/rest-client.gemspec index 6938046..a173c16 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -2,11 +2,11 @@ Gem::Specification.new do |s| s.name = %q{rest-client} - s.version = "1.0.2" + s.version = "1.0.3" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Wiggins"] - s.date = %q{2009-06-20} + s.date = %q{2009-06-29} s.default_executable = %q{restclient} s.description = %q{A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.} s.email = %q{adam@heroku.com} From b2dda517e44565bc72ef119416e2166486a6bdf0 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Wed, 22 Jul 2009 14:54:33 -0700 Subject: [PATCH 47/53] accept */* but prefer xml for backward compatibility --- lib/restclient/request.rb | 2 +- spec/request_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 71f1b05..b0138f4 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -232,7 +232,7 @@ module RestClient end def default_headers - { :accept => 'application/xml', :accept_encoding => 'gzip, deflate' } + { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' } end end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 47842e2..d6c334c 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -17,8 +17,8 @@ describe RestClient::Request do @net.stub!(:verify_mode=) end - it "requests xml mimetype" do - @request.default_headers[:accept].should == 'application/xml' + it "accept */* mimetype, preferring xml" do + @request.default_headers[:accept].should == '*/*; q=0.5, application/xml' end it "decodes an uncompressed result body by passing it straight through" do From c637668f2dbc23466df04c7dbeabf4ce40e4f7bf Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Wed, 29 Jul 2009 18:03:10 -0700 Subject: [PATCH 48/53] v1.0.4 --- VERSION | 2 +- rest-client.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 21e8796..ee90284 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.3 +1.0.4 diff --git a/rest-client.gemspec b/rest-client.gemspec index a173c16..9480907 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -2,11 +2,11 @@ Gem::Specification.new do |s| s.name = %q{rest-client} - s.version = "1.0.3" + s.version = "1.0.4" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Wiggins"] - s.date = %q{2009-06-29} + s.date = %q{2009-07-29} s.default_executable = %q{restclient} s.description = %q{A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.} s.email = %q{adam@heroku.com} From c6e6931198be788ab24aff7bf0a6be69ece48209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beausoleil?= Date: Wed, 12 Aug 2009 11:02:44 -0400 Subject: [PATCH 49/53] Only supply the basename to prevent data leakage to server. --- lib/restclient/payload.rb | 2 +- spec/payload_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/restclient/payload.rb b/lib/restclient/payload.rb index e896be0..304b6ca 100644 --- a/lib/restclient/payload.rb +++ b/lib/restclient/payload.rb @@ -92,7 +92,7 @@ module RestClient def create_file_field(s, k, v) begin - s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}") + s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{File.basename(v.path)}\"#{EOL}") s.write("Content-Type: #{mime_for(v.path)}#{EOL}") s.write(EOL) while data = v.read(8124) diff --git a/spec/payload_spec.rb b/spec/payload_spec.rb index 35564ee..906a851 100644 --- a/spec/payload_spec.rb +++ b/spec/payload_spec.rb @@ -36,7 +36,7 @@ EOS m = RestClient::Payload::Multipart.new({:foo => f}) m.to_s.should == <<-EOS --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="foo"; filename="./spec/master_shake.jpg"\r +Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r Content-Type: image/jpeg\r \r #{IO.read(f.path)}\r From e0534468e64ef7b6cf3c34c1d498c2caa34433a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beausoleil?= Date: Wed, 12 Aug 2009 11:04:41 -0400 Subject: [PATCH 50/53] Version bump to 1.1.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ee90284..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.4 +1.1.0 From f5e7d82c5b225fefb73942a4bad4ead1fc156ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beausoleil?= Date: Wed, 12 Aug 2009 11:04:56 -0400 Subject: [PATCH 51/53] Regenerated gemspec for version 1.1.0 --- rest-client.gemspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest-client.gemspec b/rest-client.gemspec index c4a38d9..53b3e67 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = %q{rest-client} - s.version = "1.0.4" + s.version = "1.1.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Wiggins"] @@ -20,11 +20,11 @@ Gem::Specification.new do |s| "VERSION", "bin/restclient", "lib/rest_client.rb", - "lib/rest_client/net_http_ext.rb", - "lib/rest_client/payload.rb", "lib/restclient.rb", "lib/restclient/exceptions.rb", "lib/restclient/mixin/response.rb", + "lib/restclient/net_http_ext.rb", + "lib/restclient/payload.rb", "lib/restclient/raw_response.rb", "lib/restclient/request.rb", "lib/restclient/resource.rb", From 9b7bdffa194a37c65c78f0f711fd9303ceda1d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beausoleil?= Date: Wed, 12 Aug 2009 11:33:12 -0400 Subject: [PATCH 52/53] Nested files should still end up as multipart --- lib/restclient/payload.rb | 14 ++++++++++++-- spec/payload_spec.rb | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/restclient/payload.rb b/lib/restclient/payload.rb index 304b6ca..4001bd7 100644 --- a/lib/restclient/payload.rb +++ b/lib/restclient/payload.rb @@ -8,14 +8,24 @@ module RestClient def generate(params) if params.is_a?(String) Base.new(params) - elsif params.delete(:multipart) == true || - params.any? { |_,v| v.respond_to?(:path) && v.respond_to?(:read) } + elsif params.delete(:multipart) == true || has_file?(params) Multipart.new(params) else UrlEncoded.new(params) end end + def has_file?(params) + params.any? do |_, v| + case v + when Hash + has_file?(v) + else + v.respond_to?(:path) && v.respond_to?(:read) + end + end + end + class Base def initialize(params) build_stream(params) diff --git a/spec/payload_spec.rb b/spec/payload_spec.rb index 906a851..1128659 100644 --- a/spec/payload_spec.rb +++ b/spec/payload_spec.rb @@ -63,5 +63,10 @@ EOS it "should return data if no of the above" do RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base) end + + it "should recognize nested multipart payloads" do + f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") + RestClient::Payload.generate({"foo" => {"file" => f}}).should be_kind_of(RestClient::Payload::Multipart) + end end end From c31dadab25d13d652c57177715a07352d3637c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Beausoleil?= Date: Wed, 12 Aug 2009 11:36:23 -0400 Subject: [PATCH 53/53] Version bump to 1.2.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9084fa2..6085e94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.2.1