From da50d93f51c7f99304b56ad071c602731ab8f92a Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Sun, 3 Jan 2010 22:48:02 +0100 Subject: [PATCH 01/40] Typo in history --- history.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/history.md b/history.md index 843f645..1991104 100644 --- a/history.md +++ b/history.md @@ -3,7 +3,7 @@ - formatting changed from tabs to spaces - logged requests now include generated headers - accept and content-type headers can now be specified using extentions: RestClient.post "http://example.com/resource", { 'x' => 1 }.to_json, :content_type => :json, :accept => :json -- should be 1.1.1 but renammed to 1.2.0 because 1.1.X versions has already been packaged on Debian +- should be 1.1.1 but renamed to 1.2.0 because 1.1.X versions has already been packaged on Debian # 1.1.0 From 79f31b9850b495d04b3b66328964b7509fbc115e Mon Sep 17 00:00:00 2001 From: Cyril Rohr Date: Tue, 5 Jan 2010 11:34:37 +0100 Subject: [PATCH 02/40] use stub! instead of stub --- spec/request_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 15629f3..fbfd360 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -512,7 +512,7 @@ describe RestClient::Request do :payload => 'payload' ) net_http_res = Net::HTTPNoContent.new("", "204", "No Content") - net_http_res.stub(:read_body).and_return(nil) + net_http_res.stub!(:read_body).and_return(nil) @http.should_receive(:request).and_return(@request.fetch_body(net_http_res)) response = @request.transmit(@uri, 'req', 'payload') response.should_not be_nil From 5cee63c615f5b34c932de96e261e6eb2beaa56ae Mon Sep 17 00:00:00 2001 From: Cyril Rohr Date: Tue, 5 Jan 2010 11:35:01 +0100 Subject: [PATCH 03/40] fixed typo: paylod instead of payload --- 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 7651428..7920933 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -241,7 +241,7 @@ module RestClient out = [] out << "RestClient.#{method} #{url.inspect}" out << "headers: #{processed_headers.inspect}" - out << "paylod: #{payload.short_inspect}" if payload + out << "payload: #{payload.short_inspect}" if payload out.join(', ') end end From 16a4dbf9ddfab57c47c27ac1a13e38dccd73efe4 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Tue, 5 Jan 2010 21:03:27 +0100 Subject: [PATCH 04/40] First try(cherry picked from commit eece9eda0487969e582ef97e6a41f86780a53d08) --- lib/restclient.rb | 61 +++++++++++++++++++++++++++++------ lib/restclient/request.rb | 60 +++++++++++++++------------------- spec/request_spec.rb | 68 +++++++++++++++++---------------------- spec/restclient_spec.rb | 32 +++++++++++------- 4 files changed, 129 insertions(+), 92 deletions(-) diff --git a/lib/restclient.rb b/lib/restclient.rb index 8603f84..f0ae125 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -88,16 +88,14 @@ module RestClient attr_accessor :proxy end - # Print log of RestClient calls. Value can be stdout, stderr, or a filename. + # Setup the log for RestClient calls. + # Value should be a logger but can 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 + def self.log= log + if log.is_a? String + warn "[warning] You should set the log with a logger" + end + @@log = create_log log end def self.version @@ -105,4 +103,49 @@ module RestClient return File.read(version_path).chomp if File.file?(version_path) "0.0.0" end + + # Create a log that respond to << like a logger + # param can be 'stdout', 'stderr', a string (then we will log to that file) or a logger (then we return it) + def self.create_log param + if param + if param.is_a? String + if param == 'stdout' + stdout_logger = Class.new do + def << obj + STDOUT.puts obj + end + end + stdout_logger.new + elsif param == 'stderr' + stderr_logger = Class.new do + def << obj + STDERR.puts obj + end + end + stderr_logger.new + else + file_logger = Class.new do + attr_writer :target_file + def << obj + File.open(@target_file, 'a') { |f| f.puts obj } + end + end + logger = file_logger.new + logger.target_file = param + logger + end + else + param + end + end + end + + @@env_log = create_log ENV['RESTCLIENT_LOG'] + + @@log = nil + + def self.log # :nodoc: + @@env_log || @@log + end + end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 7920933..c29a53f 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -73,13 +73,13 @@ module RestClient target_value = value.to_s final[target_key] = MIME::Types.type_for_extension target_value elsif 'ACCEPT' == target_key.upcase - # Accept can be composed of several comma-separated values - if value.is_a? Array - target_values = value - else - target_values = value.to_s.split ',' - end - final[target_key] = target_values.map{ |ext| MIME::Types.type_for_extension(ext.to_s.strip)}.join(', ') + # Accept can be composed of several comma-separated values + if value.is_a? Array + target_values = value + else + target_values = value.to_s.split ',' + end + final[target_key] = target_values.map{ |ext| MIME::Types.type_for_extension(ext.to_s.strip)}.join(', ') else final[target_key] = value.to_s end @@ -148,12 +148,12 @@ module RestClient net.read_timeout = @timeout if @timeout net.open_timeout = @open_timeout if @open_timeout - display_log request_log + log_request net.start do |http| res = http.request(req, payload) { |http_response| fetch_body(http_response) } result = process_result(res) - display_log response_log(res) + log_response res if result.kind_of?(String) or @method == :head Response.new(result, res) @@ -183,12 +183,14 @@ module RestClient 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]) + if RestClient.log + if size == 0 + RestClient.log << "#{@method} #{@url} done (0 length file)" + elsif total == 0 + RestClient.log << "#{@method} #{@url} (zero content length)" + else + RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total] + end end end @tf.close @@ -236,30 +238,20 @@ module RestClient end end - def request_log + def log_request if RestClient.log out = [] out << "RestClient.#{method} #{url.inspect}" - out << "headers: #{processed_headers.inspect}" - out << "payload: #{payload.short_inspect}" if payload - out.join(', ') + out << payload.short_inspect if payload + out << processed_headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') + RestClient.log << out.join(', ') end end - def response_log(res) - 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 - - 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 } + def log_response res + if RestClient.log + size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size) + RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes" end end @@ -274,7 +266,7 @@ module MIME # Return the first found content-type for a value considered as an extension or the value itself def type_for_extension ext - candidates = @extension_index[ext] + candidates = @extension_index[ext] candidates.empty? ? ext : candidates[0].content_type end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index fbfd360..e6df857 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -15,7 +15,7 @@ describe RestClient::Request do @net.stub!(:start).and_yield(@http) @net.stub!(:use_ssl=) @net.stub!(:verify_mode=) - RestClient.log = 'test.log' + RestClient.log = nil end it "accept */* mimetype, preferring xml" do @@ -150,7 +150,7 @@ describe RestClient::Request do 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.should_receive(:log_response) @request.transmit(@uri, 'req', 'payload') end @@ -289,71 +289,63 @@ describe RestClient::Request do end it "logs a get request" do - ['RestClient.get "http://url", headers: {"Accept-encoding"=>"gzip, deflate", "Accept"=>"*/*; q=0.5, application/xml"}', - 'RestClient.get "http://url", headers: {"Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate}'].should include - RestClient::Request.new(:method => :get, :url => 'http://url').request_log + log = RestClient.log = [] + RestClient::Request.new(:method => :get, :url => 'http://url').log_request + ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"*/*; q=0.5, application/xml"', + 'RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) end it "logs a post request with a small payload" do - ['RestClient.post "http://url", headers: {"Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3", "Accept"=>"*/*; q=0.5, application/xml"}, paylod: "foo"', - 'RestClient.post "http://url", headers: {"Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3"}, paylod: "foo"'].should include - RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log + log = RestClient.log = [] + RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').log_request + ['RestClient.post "http://url", "foo", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3", "Accept"=>"*/*; q=0.5, application/xml"', + 'RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3"'].should include(log[0]) end it "logs a post request with a large payload" do - ['RestClient.post "http://url", headers: {"Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"}, paylod: 1000 byte length', - 'RestClient.post "http://url", headers: {"Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"}, paylod: 1000 byte length'].should include - RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log + log = RestClient.log = [] + RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request + ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"', + 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"'].should include(log[0]) end it "logs input headers as a hash" do - ['RestClient.get "http://url", headers: {"Accept-encoding"=>"gzip, deflate", "Accept"=>"text/plain"}', - 'RestClient.get "http://url", headers: {"Accept"=>"text/plain", "Accept-encoding"=>"gzip, deflate"}'].should include - RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }) + log = RestClient.log = [] + RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).log_request + ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"text/plain"', + 'RestClient.get "http://url", "Accept"=>"text/plain", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) end it "logs a response including the status code, content type, and result body size in bytes" do + log = RestClient.log = [] 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" + @request.log_response res + log[0].should == "# => 200 OK | text/html 4 bytes" end it "logs a response with a nil Content-type" do + log = RestClient.log = [] 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" + @request.log_response res + log[0].should == "# => 200 OK | 4 bytes" end it "logs a response with a nil body" do + log = RestClient.log = [] 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" + @request.log_response res + log[0].should == "# => 200 OK | text/html 0 bytes" end it "strips the charset from the response content type" do + log = RestClient.log = [] 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') + @request.log_response res + log[0].should == "# => 200 OK | text/html 4 bytes" end it "set read_timeout" do diff --git a/spec/restclient_spec.rb b/spec/restclient_spec.rb index cf3333c..3107afb 100644 --- a/spec/restclient_spec.rb +++ b/spec/restclient_spec.rb @@ -33,21 +33,31 @@ describe RestClient 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' + it "uses << if the log is not a string" do + log = RestClient.log = [] + log.should_receive(:<<).with('xyz') + RestClient.log << 'xyz' 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' + it "displays the log to stdout" do + RestClient.log = 'stdout' + STDOUT.should_receive(:puts).with('xyz') + RestClient.log << 'xyz' end - it "returns nil (no logging) if neither are set (default)" do - ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil) - RestClient.log.should == nil + it "displays the log to stderr" do + RestClient.log = 'stderr' + STDERR.should_receive(:puts).with('xyz') + RestClient.log << 'xyz' + end + + it "append the log to the requested filename" do + RestClient.log = '/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') + RestClient.log << 'xyz' end end + end From d385dc3f05f4d18ed1b6d41fbacf19c138a17e0c Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Sat, 9 Jan 2010 11:20:49 +0100 Subject: [PATCH 05/40] history about logs change --- history.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/history.md b/history.md index 1991104..d2a63f0 100644 --- a/history.md +++ b/history.md @@ -1,3 +1,7 @@ +# 1.2.1 + +- cleaner log API, add a warning for some cases but should be compatible + # 1.2.0 - formatting changed from tabs to spaces From 3b83aadc353b5420a2c66aeba25d8a8099a00e6b Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 18 Jan 2010 22:15:15 +0100 Subject: [PATCH 06/40] Giving a first try --- lib/restclient.rb | 2 +- lib/restclient/exceptions.rb | 73 +++++++++++++++++--------------- lib/restclient/mixin/response.rb | 3 +- lib/restclient/request.rb | 49 ++++++++++----------- 4 files changed, 63 insertions(+), 64 deletions(-) diff --git a/lib/restclient.rb b/lib/restclient.rb index f0ae125..c7a5c26 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -9,12 +9,12 @@ rescue LoadError => e raise LoadError, "no such file to load -- net/https. Try running apt-get install libopenssl-ruby" end +require File.dirname(__FILE__) + '/restclient/exceptions' 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' require File.dirname(__FILE__) + '/restclient/payload' require File.dirname(__FILE__) + '/restclient/net_http_ext' diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index b2ae9da..5d5ffa9 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -1,32 +1,54 @@ module RestClient + # This is the base RestClient exception class. Rescue it if you want to # catch any exception that your request might raise class Exception < RuntimeError - def message(default=nil) - self.class::ErrorMessage - end - end + attr_accessor :message, :response - # Base RestClient exception when there's a response available - class ExceptionWithResponse < Exception - attr_accessor :response - - def initialize(response=nil) + def initialize response = nil @response = response end def http_code - @response.code.to_i if @response + @response.code if @response end def http_body - RestClient::Request.decode(@response['content-encoding'], @response.body) if @response + @response end + + def inspect + "#{self.class} : #{http_code} #{message}" + end + + end + + # We will a create an exception for each status code, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + module Exceptions + # Map http status codes to the corresponding exception class + EXCEPTIONS_MAP = {} + end + + {304 => 'Not Modified', 305 => 'Use Proxy', + 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Resource Not Found', + 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', + 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| + klass = Class.new(Exception) do + send :define_method, :message, Proc.new{message} + message + end + klass = const_set message.gsub(/ /, '').gsub(/-/, ''), klass + Exceptions::EXCEPTIONS_MAP[code] = klass end # A redirect was encountered; caught by execute to retry with the new url. class Redirect < Exception - ErrorMessage = "Redirect" + + message = 'Redirect' attr_accessor :url @@ -35,42 +57,22 @@ module RestClient end end - class NotModified < ExceptionWithResponse - ErrorMessage = 'NotModified' - end - - # Authorization is required to access the resource specified. - class Unauthorized < ExceptionWithResponse - ErrorMessage = 'Unauthorized' - end - - # No resource was found at the given URL. - class ResourceNotFound < ExceptionWithResponse - ErrorMessage = 'Resource not found' - end - # 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' + message = 'Server broke connection' end - # The server took too long to respond. - class RequestTimeout < Exception - ErrorMessage = 'Request timed out' - end # The request failed, meaning the remote HTTP server returned a code other # than success, unauthorized, or redirect. # - # The exception message attempts to extract the error from the XML, using - # format returned by Rails: some message - # # You can get the status code by e.http_code, or see anything about the # response via e.response. For example, the entire result body (which is # probably an HTML error page) is e.response.body. - class RequestFailed < ExceptionWithResponse + class RequestFailed < Exception + def message "HTTP status code #{http_code}" end @@ -79,6 +81,7 @@ module RestClient message end end + end # backwards compatibility diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb index 3571531..5d356c9 100644 --- a/lib/restclient/mixin/response.rb +++ b/lib/restclient/mixin/response.rb @@ -3,8 +3,7 @@ module RestClient module Response attr_reader :net_http_res - # HTTP status code, always 200 since RestClient throws exceptions for - # other codes. + # HTTP status code def code @code ||= @net_http_res.code.to_i end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index c29a53f..6e99ce7 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -20,11 +20,15 @@ module RestClient # * :timeout and :open_timeout # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file class Request + + include RestClient::Exceptions + attr_reader :method, :url, :payload, :headers, :processed_headers, :cookies, :user, :password, :timeout, :open_timeout, :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :raw_response + def self.execute(args) new(args).execute end @@ -152,16 +156,7 @@ module RestClient net.start do |http| res = http.request(req, payload) { |http_response| fetch_body(http_response) } - result = process_result(res) - log_response res - - if result.kind_of?(String) or @method == :head - Response.new(result, res) - elsif @raw_response - RawResponse.new(@tf, res) - else - Response.new(nil, res) - end + process_result(res) end rescue EOFError raise RestClient::ServerBrokeConnection @@ -201,13 +196,20 @@ module RestClient http_response end - def process_result(res) - if res.code =~ /\A2\d{2}\z/ + def process_result res + if @raw_response # We don't decode raw requests - unless @raw_response - self.class.decode res['content-encoding'], res.body if res.body - end - elsif %w(301 302 303).include? res.code + response = RawResponse.new(@tf, res) + else + response = Response.new(Request.decode(res['content-encoding'], res.body), res) + end + log_response response + + code = res.code.to_i + + if (200...206).include? code + response + elsif (301...303).include? code url = res.header['Location'] if url !~ /^http/ @@ -215,24 +217,19 @@ module RestClient uri.path = "/#{url}".squeeze('/') url = uri.to_s end - raise Redirect, url - elsif res.code == "304" - raise NotModified, res - elsif res.code == "401" - raise Unauthorized, res - elsif res.code == "404" - raise ResourceNotFound, res + elsif EXCEPTIONS_MAP[code] + raise EXCEPTIONS_MAP[code], response else - raise RequestFailed, res + raise RequestFailed, response end end - def self.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' - Zlib::Inflate.new.inflate(body) + Zlib::Inflate.new.inflate body else body end From a38c21d8b8a6e282dbc0ee3b994b1bfb5b91b7c8 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 18 Jan 2010 22:47:16 +0100 Subject: [PATCH 07/40] useless line --- lib/restclient/exceptions.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 5d5ffa9..7257b5b 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -39,7 +39,6 @@ module RestClient 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| klass = Class.new(Exception) do send :define_method, :message, Proc.new{message} - message end klass = const_set message.gsub(/ /, '').gsub(/-/, ''), klass Exceptions::EXCEPTIONS_MAP[code] = klass From db3d8adad87e229b0490292ceef88d20d1c922cb Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 18:38:20 +0100 Subject: [PATCH 08/40] better formatting --- lib/restclient/exceptions.rb | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 7257b5b..73ec250 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -29,14 +29,31 @@ module RestClient EXCEPTIONS_MAP = {} end - {304 => 'Not Modified', 305 => 'Use Proxy', - 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Resource Not Found', - 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', - 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', + {304 => 'Not Modified', + 305 => 'Use Proxy', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Resource Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', - 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| klass = Class.new(Exception) do send :define_method, :message, Proc.new{message} end From eee0b7b840f05c852642205d5e75a9a9603c123e Mon Sep 17 00:00:00 2001 From: Cyril Rohr Date: Wed, 20 Jan 2010 16:54:54 +0100 Subject: [PATCH 09/40] Take into account multiple Set-Cookie headers. See http://www.ietf.org/rfc/rfc2109.txt --- lib/restclient/mixin/response.rb | 10 ++++------ lib/restclient/request.rb | 2 +- spec/request_spec.rb | 4 ++-- spec/response_spec.rb | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb index 3571531..23a8e0e 100644 --- a/lib/restclient/mixin/response.rb +++ b/lib/restclient/mixin/response.rb @@ -22,11 +22,9 @@ module RestClient # 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 + @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie| + key, *val = cookie.split(";").first.split("=") + out[key] = val.join("=") out end end @@ -38,7 +36,7 @@ module RestClient module ClassMethods def beautify_headers(headers) headers.inject({}) do |out, (key, value)| - out[key.gsub(/-/, '_').downcase.to_sym] = value.first + out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first out end end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index c29a53f..69269e8 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -64,7 +64,7 @@ module RestClient def make_headers user_headers unless @cookies.empty? - user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ') + user_headers[:cookie] = @cookies.map {|(key, val)| "#{key.to_s}=#{val}" }.sort.join(",") end headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)| diff --git a/spec/request_spec.rb b/spec/request_spec.rb index e6df857..ad73dec 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -83,9 +83,9 @@ describe RestClient::Request do 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 = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1', :user_id => "someone" }) @request.should_receive(:default_headers).and_return({'foo' => 'bar'}) - headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'} + headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1,user_id=someone'} end it "determines the Net::HTTP class to instantiate by the method name" do diff --git a/spec/response_spec.rb b/spec/response_spec.rb index 1324324..4305ca8 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -18,4 +18,22 @@ describe RestClient::Response do @response.raw_headers["Status"][0].should == "200 OK" @response.headers[:status].should == "200 OK" end + + it "should correctly deal with one cookie" do + net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"]}) + response = RestClient::Response.new('abc', net_http_res) + response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"] + response.cookies.should == { "main_page" => "main_page_no_rewrite" } + end + + it "should correctly deal with multiple cookies" do + net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]}) + response = RestClient::Response.new('abc', net_http_res) + response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"] + response.cookies.should == { + "main_page" => "main_page_no_rewrite", + "remember_me" => "", + "user" => "somebody" + } + end end From 13bf146a5c47b9103d3c31701baf9da0cc578efb Mon Sep 17 00:00:00 2001 From: Cyril Rohr Date: Wed, 20 Jan 2010 17:37:54 +0100 Subject: [PATCH 10/40] correctly supports Set-Cookie header with multiple comma-separated cookies inside --- lib/restclient/mixin/response.rb | 13 ++++++--- spec/response_spec.rb | 45 ++++++++++++++++++++------------ 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb index 23a8e0e..63e41ef 100644 --- a/lib/restclient/mixin/response.rb +++ b/lib/restclient/mixin/response.rb @@ -22,9 +22,16 @@ module RestClient # Hash of cookies extracted from response headers def cookies - @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie| - key, *val = cookie.split(";").first.split("=") - out[key] = val.join("=") + @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content| + # correctly parse comma-separated cookies containing HTTP dates (which also contain a comma) + cookie_content.split(/,\s*/).inject([""]) { |array, blob| + blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob) + array + }.each do |cookie| + next if cookie.empty? + key, *val = cookie.split(";").first.split("=") + out[key] = val.join("=") + end out end end diff --git a/spec/response_spec.rb b/spec/response_spec.rb index 4305ca8..ce38c75 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -19,21 +19,34 @@ describe RestClient::Response do @response.headers[:status].should == "200 OK" end - it "should correctly deal with one cookie" do - net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"]}) - response = RestClient::Response.new('abc', net_http_res) - response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"] - response.cookies.should == { "main_page" => "main_page_no_rewrite" } - end - - it "should correctly deal with multiple cookies" do - net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]}) - response = RestClient::Response.new('abc', net_http_res) - response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"] - response.cookies.should == { - "main_page" => "main_page_no_rewrite", - "remember_me" => "", - "user" => "somebody" - } + describe "cookie processing" do + it "should correctly deal with one Set-Cookie header with one cookie inside" do + net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"]}) + response = RestClient::Response.new('abc', net_http_res) + response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"] + response.cookies.should == { "main_page" => "main_page_no_rewrite" } + end + + it "should correctly deal with multiple cookies [multiple Set-Cookie headers]" do + net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]}) + response = RestClient::Response.new('abc', net_http_res) + response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"] + response.cookies.should == { + "main_page" => "main_page_no_rewrite", + "remember_me" => "", + "user" => "somebody" + } + end + + it "should correctly deal with multiple cookies [one Set-Cookie header with multiple cookies]" do + net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT, remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT, user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]}) + response = RestClient::Response.new('abc', net_http_res) + response.cookies.should == { + "main_page" => "main_page_no_rewrite", + "remember_me" => "", + "user" => "somebody" + } + end end + end From ae5874cbae215721908760b6b7ae8a0916a7bca0 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 19:24:26 +0100 Subject: [PATCH 11/40] Added history --- history.md | 1 + 1 file changed, 1 insertion(+) diff --git a/history.md b/history.md index d2a63f0..a3b97bb 100644 --- a/history.md +++ b/history.md @@ -1,6 +1,7 @@ # 1.2.1 - cleaner log API, add a warning for some cases but should be compatible +- accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) # 1.2.0 From 26aa3ce8d005942fa2e2cf2ca7a0e35d56102bf8 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 19:57:38 +0100 Subject: [PATCH 12/40] Fixed tests --- lib/restclient/exceptions.rb | 14 +++++++------- spec/exceptions_spec.rb | 8 +++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 73ec250..9f167d6 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -2,6 +2,10 @@ module RestClient # This is the base RestClient exception class. Rescue it if you want to # catch any exception that your request might raise + # You can get the status code by e.http_code, or see anything about the + # response via e.response. + # For example, the entire result body (which is + # probably an HTML error page) is e.response. class Exception < RuntimeError attr_accessor :message, :response @@ -10,7 +14,8 @@ module RestClient end def http_code - @response.code if @response + # return integer for compatibility + @response.code.to_i if @response end def http_body @@ -81,12 +86,7 @@ module RestClient end - # The request failed, meaning the remote HTTP server returned a code other - # than success, unauthorized, or redirect. - # - # You can get the status code by e.http_code, or see anything about the - # response via e.response. For example, the entire result body (which is - # probably an HTML error page) is e.response.body. + # The request failed with an error code not managed by the code class RequestFailed < Exception def message diff --git a/spec/exceptions_spec.rb b/spec/exceptions_spec.rb index a05720d..52f4d7c 100644 --- a/spec/exceptions_spec.rb +++ b/spec/exceptions_spec.rb @@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/base' describe RestClient::Exception do it "sets the exception message to ErrorMessage" do - RestClient::ResourceNotFound.new.message.should == 'Resource not found' + RestClient::ResourceNotFound.new.message.should == 'Resource Not Found' end it "contains exceptions in RestClient" do @@ -29,10 +29,8 @@ describe RestClient::RequestFailed do 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' + RestClient::RequestFailed.new(@response).http_code.should == 502 + RestClient::RequestFailed.new(@response).message.should == 'HTTP status code 502' end it "shows the status code in the message" do From e253c07f3f8ae4050c8b7ac1becd16ed1317b238 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 21:10:40 +0100 Subject: [PATCH 13/40] Tests are now ok --- lib/restclient/request.rb | 4 ++-- spec/mixin/response_spec.rb | 2 +- spec/request_spec.rb | 15 +++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 62ea5fd..68d08a4 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -207,9 +207,9 @@ module RestClient code = res.code.to_i - if (200...206).include? code + if (200..206).include? code response - elsif (301...303).include? code + elsif (301..303).include? code url = res.header['Location'] if url !~ /^http/ diff --git a/spec/mixin/response_spec.rb b/spec/mixin/response_spec.rb index a72ee54..41b1c73 100644 --- a/spec/mixin/response_spec.rb +++ b/spec/mixin/response_spec.rb @@ -5,7 +5,7 @@ class MockResponse def initialize(body, res) @net_http_res = res - @body = @body + @body = body end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index ad73dec..adb6d16 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -150,7 +150,6 @@ describe RestClient::Request do it "transmits the request with Net::HTTP" do @http.should_receive(:request).with('req', 'payload') @request.should_receive(:process_result) - @request.should_receive(:log_response) @request.transmit(@uri, 'req', 'payload') end @@ -235,18 +234,18 @@ describe RestClient::Request do 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' }) + res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource'}, :[] => ['content-encoding' => ''], :body => '' ) 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' }) + res = mock('response', :code => '301', :header => { 'Location' => 'index' }, :[] => ['content-encoding' => ''], :body => '' ) 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' }) + res = mock('response', :code => '301', :header => { 'Location' => '/index' }, :[] => ['content-encoding' => ''], :body => '' ) lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } end @@ -265,18 +264,18 @@ describe RestClient::Request do end it "raises Unauthorized when the response is 401" do - res = mock('response', :code => '401') + res = mock('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' ) 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') + res = mock('response', :code => '404', :[] => ['content-encoding' => ''], :body => '' ) 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) + res = mock('response', :code => '500', :[] => ['content-encoding' => ''], :body => '' ) + lambda { @request.process_result(res) }.should raise_error(RestClient::InternalServerError) end it "creates a proxy class if a proxy url is given" do From f9af4a29882faeddaa8e1def62a5197962362849 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 22:51:24 +0100 Subject: [PATCH 14/40] Added some integration specs --- rest-client.gemspec | 2 ++ spec/integration_spec.rb | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 spec/integration_spec.rb diff --git a/rest-client.gemspec b/rest-client.gemspec index 77273bd..8094c53 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |s| "lib/restclient/response.rb", "spec/base.rb", "spec/exceptions_spec.rb", + "spec/integration_spec.rb", "spec/master_shake.jpg", "spec/mixin/response_spec.rb", "spec/payload_spec.rb", @@ -48,6 +49,7 @@ Gem::Specification.new do |s| s.test_files = [ "spec/base.rb", "spec/exceptions_spec.rb", + "spec/integration_spec.rb", "spec/mixin/response_spec.rb", "spec/payload_spec.rb", "spec/raw_response_spec.rb", diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb new file mode 100644 index 0000000..1e1b0b7 --- /dev/null +++ b/spec/integration_spec.rb @@ -0,0 +1,38 @@ +require File.dirname(__FILE__) + '/base' + +require 'webmock/rspec' +include WebMock + +describe RestClient do + + it "a simple request" do + body = 'abc' + stub_request(:get, "www.example.com").to_return(:body => body, :status => 200) + response = RestClient.get "www.example.com" + response.code.should == 200 + response.should == body + end + + it "a simple request with gzipped content" do + stub_request(:get, "www.example.com").with(:headers => { 'Accept-Encoding' => 'gzip, deflate' }).to_return(:body => "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000", :status => 200, :headers => { 'Content-Encoding' => 'gzip' } ) + response = RestClient.get "www.example.com" + response.code.should == 200 + response.should == "i'm gziped\n" + end + + it "a 404" do + body = "Ho hai ! I'm not here !" + stub_request(:get, "www.example.com").to_return(:body => body, :status => 404) + begin + RestClient.get "www.example.com" + raise + rescue RestClient::ResourceNotFound => e + e.http_code.should == 404 + e.response.code.should == 404 + e.response.should == body + e.http_body.should == body + end + end + + +end \ No newline at end of file From 45bce1862c1f021feb554e32d3c56948bad30c81 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 22:53:20 +0100 Subject: [PATCH 15/40] Added history --- history.md | 1 + 1 file changed, 1 insertion(+) diff --git a/history.md b/history.md index a3b97bb..75ca28e 100644 --- a/history.md +++ b/history.md @@ -2,6 +2,7 @@ - cleaner log API, add a warning for some cases but should be compatible - accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) +- all http error codes has now a corresponding exception class and all of them contain the Reponse # 1.2.0 From 2474d49d9c36a123b6fbe0e1de3132b0129b24b4 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 20 Jan 2010 22:55:19 +0100 Subject: [PATCH 16/40] Added history.md to gem --- rest-client.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest-client.gemspec b/rest-client.gemspec index 77273bd..f3d2f86 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| s.email = %q{rest.client@librelist.com} s.executables = ["restclient"] s.extra_rdoc_files = [ - "README.rdoc" + "README.rdoc", "history.md" ] s.files = [ "README.rdoc", From 9e23b9635920d16800b9dd7a6db08cabf25e5998 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Thu, 21 Jan 2010 18:48:10 +0100 Subject: [PATCH 17/40] slightly cleaner code --- lib/restclient/exceptions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 9f167d6..1c24a48 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -60,7 +60,7 @@ module RestClient 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| klass = Class.new(Exception) do - send :define_method, :message, Proc.new{message} + send(:define_method, :message) {message} end klass = const_set message.gsub(/ /, '').gsub(/-/, ''), klass Exceptions::EXCEPTIONS_MAP[code] = klass From b83986cec4686a1c7b445da2e14a6b49e8b84bfa Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Thu, 21 Jan 2010 19:32:01 +0100 Subject: [PATCH 18/40] Fix logging --- 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 68d08a4..1bcb58a 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -156,6 +156,7 @@ module RestClient net.start do |http| res = http.request(req, payload) { |http_response| fetch_body(http_response) } + log_response res process_result(res) end rescue EOFError @@ -203,7 +204,6 @@ module RestClient else response = Response.new(Request.decode(res['content-encoding'], res.body), res) end - log_response response code = res.code.to_i From 80fc030a9ba57172cbbdc248125553b973d3565c Mon Sep 17 00:00:00 2001 From: Harm Aarts Date: Mon, 11 Jan 2010 18:04:32 +0100 Subject: [PATCH 19/40] removed harmful headers --- history.md | 1 + lib/restclient/request.rb | 2 ++ spec/request_spec.rb | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/history.md b/history.md index 75ca28e..dac9ebc 100644 --- a/history.md +++ b/history.md @@ -2,6 +2,7 @@ - cleaner log API, add a warning for some cases but should be compatible - accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) +- remove "Content-Length" and "Content-Type" headers when following a redirection (patch provided by haarts) - all http error codes has now a corresponding exception class and all of them contain the Reponse # 1.2.0 diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 1bcb58a..418caeb 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -55,6 +55,8 @@ module RestClient def execute execute_inner rescue Redirect => e + @processed_headers.delete("Content-Length") + @processed_headers.delete("Content-Type") @url = e.url @method = :get @payload = nil diff --git a/spec/request_spec.rb b/spec/request_spec.rb index adb6d16..dd2706f 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -249,12 +249,13 @@ 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 + it "uses GET and clears payload and removes possible harmful headers 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.processed_headers.should_not have_key("Content-Length") @request.url.should == url @request.method.should == :get @request.payload.should be_nil From 68ff7f840f491efd1516b068711f545a2f27c72a Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Fri, 22 Jan 2010 21:04:49 +0100 Subject: [PATCH 20/40] Block for response done, easier than expected! --- README.rdoc | 48 ++- history.md | 3 +- lib/restclient.rb | 20 +- lib/restclient/exceptions.rb | 6 +- lib/restclient/mixin/response.rb | 12 + lib/restclient/request.rb | 42 +- spec/request_spec.rb | 683 ++++++++++++++++--------------- spec/response_spec.rb | 36 +- 8 files changed, 478 insertions(+), 372 deletions(-) diff --git a/README.rdoc b/README.rdoc index 0faf1aa..596f52d 100644 --- a/README.rdoc +++ b/README.rdoc @@ -49,6 +49,44 @@ See RestClient::Resource module docs for details. See RestClient::Resource docs for details. +== Exceptions + +* for results code between 200 and 206 a RestClient::Response will be returned +* for results code between 301 and 303 the redirection will be automatically followed +* for other result codes a RestClient::Exception holding the Response will be raised, a specific exception class will be thrown for know error codes + + RestClient.get 'http://example.com/resource' + ➔ RestClient::ResourceNotFound: RestClient::ResourceNotFound + + begin + RestClient.get 'http://example.com/resource' + rescue => e + e.response + end + ➔ "\n\n404 Not Found\n\n

Not Found

\n

The requested URL..." + +== Result handling + +A block can be passed to the RestClient method, this block will then be called with the Response. +Response.return! can be called to invoke the default response's behavior (return the Response for 200..206, raise an exception in other cases). + + # Don't raise exceptions but return the response + RestClient.get('http://example.com/resource'){|response| response} + ➔ "\n\n404 Not Found\n\n

Not Found

\n

The requested URL..." + + # Manage a specific error code + RestClient.get('http://my-rest-service.com/resource'){ |response| + case response.code + when 200 + p "It worked !" + response + when 423 + raise SomeCustomExceptionIfYouWant + else + response.return! + end + } + == Lower-level access For cases not covered by the general API, you can use the RestClient::Resource class which provide a lower-level API, see the class' rdoc for more information. @@ -58,17 +96,17 @@ For cases not covered by the general API, you can use the RestClient::Resource c The restclient shell command gives an IRB session with RestClient already loaded: $ restclient - >> RestClient.get 'http://example.com' + ➔ RestClient.get 'http://example.com' Specify a URL argument for get/post/put/delete on that resource: $ restclient http://example.com - >> put '/resource', 'data' + ➔ put '/resource', 'data' Add a user and password for authenticated resources: $ restclient https://example.com user pass - >> delete '/private/resource' + ➔ delete '/private/resource' Create ~/.restclient for named sessions: @@ -87,9 +125,9 @@ Then invoke: == Meta -Written by Adam Wiggins, major modifications by Blake Mizerany, maintained by Archiloque +Written by Adam Wiggins, major modifications by Blake Mizerany, maintained by Julien Kirch -Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, François Beausoleil and Nick Plante. +Patches contributed by many, including Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, François Beausoleil and Nick Plante. Released under the MIT License: http://www.opensource.org/licenses/mit-license.php diff --git a/history.md b/history.md index dac9ebc..1201cee 100644 --- a/history.md +++ b/history.md @@ -1,9 +1,10 @@ # 1.2.1 +- a block can be used to process a request's result, this enable to handle custom error codes or paththrought (design by Cyril Rohr) - cleaner log API, add a warning for some cases but should be compatible - accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) - remove "Content-Length" and "Content-Type" headers when following a redirection (patch provided by haarts) -- all http error codes has now a corresponding exception class and all of them contain the Reponse +- all http error codes have now a corresponding exception class and all of them contain the Reponse # 1.2.0 diff --git a/lib/restclient.rb b/lib/restclient.rb index c7a5c26..60c3377 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -64,24 +64,24 @@ require File.dirname(__FILE__) + '/restclient/net_http_ext' # module RestClient - def self.get(url, headers={}) - Request.execute(:method => :get, :url => url, :headers => headers) + def self.get(url, headers={}, &block) + Request.execute(:method => :get, :url => url, :headers => headers, &block) end - def self.post(url, payload, headers={}) - Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers) + def self.post(url, payload, headers={}, &block) + Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers, &block) end - def self.put(url, payload, headers={}) - Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers) + def self.put(url, payload, headers={}, &block) + Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers, &block) end - def self.delete(url, headers={}) - Request.execute(:method => :delete, :url => url, :headers => headers) + def self.delete(url, headers={}, &block) + Request.execute(:method => :delete, :url => url, :headers => headers, &block) end - def self.head(url, headers={}) - Request.execute(:method => :head, :url => url, :headers => headers) + def self.head(url, headers={}, &block) + Request.execute(:method => :head, :url => url, :headers => headers, &block) end class << self diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 1c24a48..971025e 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -34,7 +34,11 @@ module RestClient EXCEPTIONS_MAP = {} end - {304 => 'Not Modified', + {300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', 305 => 'Use Proxy', 400 => 'Bad Request', 401 => 'Unauthorized', diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb index fe78377..538179d 100644 --- a/lib/restclient/mixin/response.rb +++ b/lib/restclient/mixin/response.rb @@ -35,6 +35,18 @@ module RestClient end end + # Return the default behavior corresponding to the response code: + # the response itself for code in 200..206 and an exception in other cases + def return! + if (200..206).include? code + self + elsif Exceptions::EXCEPTIONS_MAP[code] + raise Exceptions::EXCEPTIONS_MAP[code], self + else + raise RequestFailed, self + end + end + def self.included(receiver) receiver.extend(RestClient::Mixin::Response::ClassMethods) end diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 418caeb..30a8c6d 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -21,19 +21,17 @@ module RestClient # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file class Request - include RestClient::Exceptions - attr_reader :method, :url, :payload, :headers, :processed_headers, :cookies, :user, :password, :timeout, :open_timeout, :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :raw_response - def self.execute(args) - new(args).execute + def self.execute(args, &block) + new(args).execute &block end - def initialize(args) + def initialize args @method = args[:method] or raise ArgumentError, "must pass :method" @url = args[:url] or raise ArgumentError, "must pass :url" @headers = args[:headers] || {} @@ -52,20 +50,20 @@ module RestClient @processed_headers = make_headers headers end - def execute - execute_inner + def execute &block + execute_inner &block rescue Redirect => e - @processed_headers.delete("Content-Length") - @processed_headers.delete("Content-Type") + @processed_headers.delete "Content-Length" + @processed_headers.delete "Content-Type" @url = e.url @method = :get @payload = nil - execute + execute &block end - def execute_inner + def execute_inner &block uri = parse_url_with_auth(url) - transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload + transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, &block end def make_headers user_headers @@ -138,8 +136,8 @@ module RestClient end end - def transmit(uri, req, payload) - setup_credentials(req) + def transmit uri, req, payload, &block + setup_credentials req net = net_http_class.new(uri.host, uri.port) net.use_ssl = uri.is_a?(URI::HTTPS) @@ -159,7 +157,7 @@ module RestClient net.start do |http| res = http.request(req, payload) { |http_response| fetch_body(http_response) } log_response res - process_result(res) + process_result res, &block end rescue EOFError raise RestClient::ServerBrokeConnection @@ -179,7 +177,7 @@ module RestClient @tf = Tempfile.new("rest-client") size, total = 0, http_response.header['Content-Length'].to_i http_response.read_body do |chunk| - @tf.write(chunk) + @tf.write chunk size += chunk.size if RestClient.log if size == 0 @@ -209,9 +207,7 @@ module RestClient code = res.code.to_i - if (200..206).include? code - response - elsif (301..303).include? code + if (301..303).include? code url = res.header['Location'] if url !~ /^http/ @@ -220,10 +216,12 @@ module RestClient url = uri.to_s end raise Redirect, url - elsif EXCEPTIONS_MAP[code] - raise EXCEPTIONS_MAP[code], response else - raise RequestFailed, response + if block_given? + yield response + else + response.return! + end end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index dd2706f..6445e9d 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -22,20 +22,22 @@ describe RestClient::Request do @request.default_headers[:accept].should == '*/*; q=0.5, application/xml' end - it "decodes an uncompressed result body by passing it straight through" do - RestClient::Request.decode(nil, 'xyz').should == 'xyz' - end + describe "compression" do + it "decodes an uncompressed result body by passing it straight through" do + RestClient::Request.decode(nil, 'xyz').should == 'xyz' + end - it "decodes a gzip body" do - 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 "decodes a gzip body" do + 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 - RestClient::Request.decode('gzip', '').should be_empty - end + it "ingores gzip for empty bodies" do + RestClient::Request.decode('gzip', '').should be_empty + end - it "decodes a deflated body" do - RestClient::Request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should == "some deflated text" + it "decodes a deflated body" do + RestClient::Request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should == "some deflated text" + end end it "processes a successful result" do @@ -66,19 +68,21 @@ describe RestClient::Request do @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 + describe "user - password" do + 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' + 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 end it "correctly formats cookies provided to the constructor" do @@ -92,50 +96,55 @@ describe RestClient::Request 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' }) - headers = @request.make_headers('3' => '4') - headers.should have_key('1') - headers['1'].should == '2' - headers.should have_key('3') - headers['3'].should == '4' + describe "user headers" do + it "merges user headers with the default headers" do + @request.should_receive(:default_headers).and_return({ '1' => '2' }) + headers = @request.make_headers('3' => '4') + headers.should have_key('1') + headers['1'].should == '2' + headers.should have_key('3') + headers['3'].should == '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' }) + headers = @request.make_headers('1' => '3') + headers.should have_key('1') + headers['1'].should == '3' + end end - it "prefers the user header when the same header exists in the defaults" do - @request.should_receive(:default_headers).and_return({ '1' => '2' }) - headers = @request.make_headers('1' => '3') - headers.should have_key('1') - headers['1'].should == '3' - end + describe "header symbols" do - it "converts header symbols from :content_type to 'Content-type'" do - @request.should_receive(:default_headers).and_return({}) - headers = @request.make_headers(:content_type => 'abc') - headers.should have_key('Content-type') - headers['Content-type'].should == 'abc' - end + it "converts header symbols from :content_type to 'Content-type'" do + @request.should_receive(:default_headers).and_return({}) + headers = @request.make_headers(:content_type => 'abc') + headers.should have_key('Content-type') + headers['Content-type'].should == 'abc' + end - it "converts content-type from extension to real content-type" do - @request.should_receive(:default_headers).and_return({}) - headers = @request.make_headers(:content_type => 'json') - headers.should have_key('Content-type') - headers['Content-type'].should == 'application/json' - end + it "converts content-type from extension to real content-type" do + @request.should_receive(:default_headers).and_return({}) + headers = @request.make_headers(:content_type => 'json') + headers.should have_key('Content-type') + headers['Content-type'].should == 'application/json' + end - it "converts accept from extension(s) to real content-type(s)" do - @request.should_receive(:default_headers).and_return({}) - headers = @request.make_headers(:accept => 'json, mp3') - headers.should have_key('Accept') - headers['Accept'].should == 'application/json, audio/mpeg' + it "converts accept from extension(s) to real content-type(s)" do + @request.should_receive(:default_headers).and_return({}) + headers = @request.make_headers(:accept => 'json, mp3') + headers.should have_key('Accept') + headers['Accept'].should == 'application/json, audio/mpeg' - @request.should_receive(:default_headers).and_return({}) - headers = @request.make_headers(:accept => :json) - headers.should have_key('Accept') - headers['Accept'].should == 'application/json' - end + @request.should_receive(:default_headers).and_return({}) + headers = @request.make_headers(:accept => :json) + headers.should have_key('Accept') + headers['Accept'].should == 'application/json' + end - it "converts header values to strings" do - @request.make_headers('A' => 1)['A'].should == '1' + it "converts header values to strings" do + @request.make_headers('A' => 1)['A'].should == '1' + end end it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do @@ -153,35 +162,28 @@ describe RestClient::Request do @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 + describe "payload" do + 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 "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 "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 "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') + 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 end it "set urlencoded content_type header on hash payloads" do @@ -189,31 +191,33 @@ describe RestClient::Request do @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) + describe "credentials" do + 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.stub!(:user).and_return('joe') + @request.stub!(:password).and_return('mypass') + @request.should_receive(:setup_credentials).with('req') - @request.transmit(@uri, 'req', nil) - end + @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 "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) + 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 end it "catches EOFError and shows the more informative ServerBrokeConnection" do @@ -233,111 +237,127 @@ describe RestClient::Request do 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'}, :[] => ['content-encoding' => ''], :body => '' ) - 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' }, :[] => ['content-encoding' => ''], :body => '' ) - 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' }, :[] => ['content-encoding' => ''], :body => '' ) - lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } - end - - it "uses GET and clears payload and removes possible harmful headers 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.processed_headers.should_not have_key("Content-Length") - @request.url.should == url - @request.method.should == :get - @request.payload.should be_nil + describe "redirection" do + 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'}, :[] => ['content-encoding' => ''], :body => '' ) + lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'} end - @request.execute + it "handles redirects with relative paths" do + res = mock('response', :code => '301', :header => { 'Location' => 'index' }, :[] => ['content-encoding' => ''], :body => '' ) + 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' }, :[] => ['content-encoding' => ''], :body => '' ) + lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' } + end + + it "uses GET and clears payload and removes possible harmful headers 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.processed_headers.should_not have_key("Content-Length") + @request.url.should == url + @request.method.should == :get + @request.payload.should be_nil + end + + @request.execute + end end - it "raises Unauthorized when the response is 401" do - res = mock('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' ) - lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized) + describe "exception" do + it "raises Unauthorized when the response is 401" do + res = mock('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' ) + 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', :[] => ['content-encoding' => ''], :body => '' ) + lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound) + end + + it "raises RequestFailed otherwise" do + res = mock('response', :code => '500', :[] => ['content-encoding' => ''], :body => '' ) + lambda { @request.process_result(res) }.should raise_error(RestClient::InternalServerError) + end end - it "raises ResourceNotFound when the response is 404" do - res = mock('response', :code => '404', :[] => ['content-encoding' => ''], :body => '' ) - lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound) + describe "block usage" do + it "returns what asked to" do + res = mock('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' ) + @request.process_result(res){|response| "foo"}.should == "foo" + end end - it "raises RequestFailed otherwise" do - res = mock('response', :code => '500', :[] => ['content-encoding' => ''], :body => '' ) - lambda { @request.process_result(res) }.should raise_error(RestClient::InternalServerError) + describe "proxy" do + 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 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 + describe "logging" do + it "logs a get request" do + log = RestClient.log = [] + RestClient::Request.new(:method => :get, :url => 'http://url').log_request + ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"*/*; q=0.5, application/xml"', + 'RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) + end - it "logs a get request" do - log = RestClient.log = [] - RestClient::Request.new(:method => :get, :url => 'http://url').log_request - ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"*/*; q=0.5, application/xml"', - 'RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) - end + it "logs a post request with a small payload" do + log = RestClient.log = [] + RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').log_request + ['RestClient.post "http://url", "foo", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3", "Accept"=>"*/*; q=0.5, application/xml"', + 'RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3"'].should include(log[0]) + end - it "logs a post request with a small payload" do - log = RestClient.log = [] - RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').log_request - ['RestClient.post "http://url", "foo", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3", "Accept"=>"*/*; q=0.5, application/xml"', - 'RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3"'].should include(log[0]) - end + it "logs a post request with a large payload" do + log = RestClient.log = [] + RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request + ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"', + 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"'].should include(log[0]) + end - it "logs a post request with a large payload" do - log = RestClient.log = [] - RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request - ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"', - 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"'].should include(log[0]) - end + it "logs input headers as a hash" do + log = RestClient.log = [] + RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).log_request + ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"text/plain"', + 'RestClient.get "http://url", "Accept"=>"text/plain", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) + end - it "logs input headers as a hash" do - log = RestClient.log = [] - RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).log_request - ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"text/plain"', - 'RestClient.get "http://url", "Accept"=>"text/plain", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) - end + it "logs a response including the status code, content type, and result body size in bytes" do + log = RestClient.log = [] + res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') + res.stub!(:[]).with('Content-type').and_return('text/html') + @request.log_response res + log[0].should == "# => 200 OK | text/html 4 bytes" + end - it "logs a response including the status code, content type, and result body size in bytes" do - log = RestClient.log = [] - res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') - res.stub!(:[]).with('Content-type').and_return('text/html') - @request.log_response res - log[0].should == "# => 200 OK | text/html 4 bytes" - end + it "logs a response with a nil Content-type" do + log = RestClient.log = [] + res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') + res.stub!(:[]).with('Content-type').and_return(nil) + @request.log_response res + log[0].should == "# => 200 OK | 4 bytes" + end - it "logs a response with a nil Content-type" do - log = RestClient.log = [] - res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') - res.stub!(:[]).with('Content-type').and_return(nil) - @request.log_response res - log[0].should == "# => 200 OK | 4 bytes" - end - - it "logs a response with a nil body" do - log = RestClient.log = [] - res = mock('result', :code => '200', :class => Net::HTTPOK, :body => nil) - res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8') - @request.log_response res - log[0].should == "# => 200 OK | text/html 0 bytes" + it "logs a response with a nil body" do + log = RestClient.log = [] + res = mock('result', :code => '200', :class => Net::HTTPOK, :body => nil) + res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8') + @request.log_response res + log[0].should == "# => 200 OK | text/html 0 bytes" + end end it "strips the charset from the response content type" do @@ -348,153 +368,166 @@ describe RestClient::Request do log[0].should == "# => 200 OK | text/html 4 bytes" 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) + describe "timeout" do + 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) + @net.should_receive(:read_timeout=).with(123) - @request.transmit(@uri, 'req', nil) + @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 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) + describe "ssl" do + 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 - @net.should_receive(:open_timeout=).with(123) + it "should default to not verifying ssl certificates" do + @request.verify_ssl.should == false + end - @request.transmit(@uri, 'req', nil) - 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 default to not verifying ssl certificates" do - @request.verify_ssl.should == false - 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 - 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 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 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 + it "should default to not having an ssl_client_cert" do + @request.ssl_client_cert.should be(nil) + 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 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 default to not having an ssl_client_cert" do - @request.ssl_client_cert.should be(nil) - 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 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 default to not having an ssl_client_key" do + @request.ssl_client_key.should be(nil) + 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 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 default to not having an ssl_client_key" do - @request.ssl_client_key.should be(nil) - 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 - 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 default to not having an ssl_ca_file" do + @request.ssl_ca_file.should be(nil) + 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 + 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 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') + 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 it "should still return a response object for 204 No Content responses" do diff --git a/spec/response_spec.rb b/spec/response_spec.rb index ce38c75..07489a8 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -18,13 +18,13 @@ describe RestClient::Response do @response.raw_headers["Status"][0].should == "200 OK" @response.headers[:status].should == "200 OK" end - + describe "cookie processing" do it "should correctly deal with one Set-Cookie header with one cookie inside" do net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"]}) response = RestClient::Response.new('abc', net_http_res) response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT"] - response.cookies.should == { "main_page" => "main_page_no_rewrite" } + response.cookies.should == { "main_page" => "main_page_no_rewrite" } end it "should correctly deal with multiple cookies [multiple Set-Cookie headers]" do @@ -32,9 +32,9 @@ describe RestClient::Response do response = RestClient::Response.new('abc', net_http_res) response.headers[:set_cookie].should == ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT", "remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT", "user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"] response.cookies.should == { - "main_page" => "main_page_no_rewrite", - "remember_me" => "", - "user" => "somebody" + "main_page" => "main_page_no_rewrite", + "remember_me" => "", + "user" => "somebody" } end @@ -42,11 +42,31 @@ describe RestClient::Response do net_http_res = mock('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Tue, 20-Jan-2015 15:03:14 GMT, remember_me=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT, user=somebody; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"]}) response = RestClient::Response.new('abc', net_http_res) response.cookies.should == { - "main_page" => "main_page_no_rewrite", - "remember_me" => "", - "user" => "somebody" + "main_page" => "main_page_no_rewrite", + "remember_me" => "", + "user" => "somebody" } end end + describe "exceptions processing" do + it "should return itself for normal codes" do + (200..206).each do |code| + net_http_res = mock('net http response', :code => '200') + response = RestClient::Response.new('abc', net_http_res) + response.return! + end + end + + it "should throw an exception for other codes" do + RestClient::Exceptions::EXCEPTIONS_MAP.each_key do |code| + net_http_res = mock('net http response', :code => code.to_i) + response = RestClient::Response.new('abc', net_http_res) + lambda { response.return!}.should raise_error + end + end + + end + + end From 9252dd79f30d8f9d710d94571bbb99a33afd7a1c Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Fri, 22 Jan 2010 21:08:07 +0100 Subject: [PATCH 21/40] Shorter html sample in README for github display --- README.rdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rdoc b/README.rdoc index 596f52d..a1db3ef 100644 --- a/README.rdoc +++ b/README.rdoc @@ -63,7 +63,7 @@ See RestClient::Resource docs for details. rescue => e e.response end - ➔ "\n\n404 Not Found\n\n

Not Found

\n

The requested URL..." + ➔ "\n\n404 Not Found..." == Result handling @@ -72,7 +72,7 @@ Response.return! can be called to invoke the default response's behavior (return # Don't raise exceptions but return the response RestClient.get('http://example.com/resource'){|response| response} - ➔ "\n\n404 Not Found\n\n

Not Found

\n

The requested URL..." + ➔ "\n\n404 Not Found..." # Manage a specific error code RestClient.get('http://my-rest-service.com/resource'){ |response| From b7dea43b9ce5ca895b7bd8541a09a781408c9498 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Sun, 24 Jan 2010 16:04:31 +0100 Subject: [PATCH 22/40] Added a part about URI normalization --- README.rdoc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.rdoc b/README.rdoc index a1db3ef..214561d 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,6 +1,6 @@ -= REST Client -- simple DSL for accessing REST resources += REST Client -- simple DSL for accessing HTTP and REST resources -A simple REST client for Ruby, inspired by the Sinatra's microframework style +A simple HTTP and REST client for Ruby, inspired by the Sinatra's microframework style of specifying actions: get, put, post, delete. == Usage: Raw URL @@ -87,6 +87,13 @@ Response.return! can be called to invoke the default response's behavior (return end } +== Non-normalized URIs. + +If you want to use non-normalized URIs, you can normalize them with the adressable gem (http://addressable.rubyforge.org/api/). + + require 'addressable/uri' + RestClient.get(Addressable::URI.parse("http://www.詹姆斯.com/").normalize.to_str) + == Lower-level access For cases not covered by the general API, you can use the RestClient::Resource class which provide a lower-level API, see the class' rdoc for more information. From 1175640d03a1fa5f00cd722741e25b390f4d88ae Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Sun, 24 Jan 2010 16:07:21 +0100 Subject: [PATCH 23/40] Stupid typo --- README.rdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 214561d..b9b9866 100644 --- a/README.rdoc +++ b/README.rdoc @@ -89,7 +89,7 @@ Response.return! can be called to invoke the default response's behavior (return == Non-normalized URIs. -If you want to use non-normalized URIs, you can normalize them with the adressable gem (http://addressable.rubyforge.org/api/). +If you want to use non-normalized URIs, you can normalize them with the addressable gem (http://addressable.rubyforge.org/api/). require 'addressable/uri' RestClient.get(Addressable::URI.parse("http://www.詹姆斯.com/").normalize.to_str) From 688b31351dfe62b2f6bbed373f99bd57b5aed2b5 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Sun, 24 Jan 2010 17:22:48 +0100 Subject: [PATCH 24/40] Added \n after logged lines --- lib/restclient/request.rb | 10 +++++----- spec/request_spec.rb | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb index 30a8c6d..d355eea 100644 --- a/lib/restclient/request.rb +++ b/lib/restclient/request.rb @@ -181,11 +181,11 @@ module RestClient size += chunk.size if RestClient.log if size == 0 - RestClient.log << "#{@method} #{@url} done (0 length file)" + RestClient.log << "#{@method} #{@url} done (0 length file\n)" elsif total == 0 - RestClient.log << "#{@method} #{@url} (zero content length)" + RestClient.log << "#{@method} #{@url} (zero content length)\n" else - RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total] + RestClient.log << "#{@method} #{@url} %d%% done (%d of %d)\n" % [(size * 100) / total, size, total] end end end @@ -241,14 +241,14 @@ module RestClient out << "RestClient.#{method} #{url.inspect}" out << payload.short_inspect if payload out << processed_headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') - RestClient.log << out.join(', ') + RestClient.log << out.join(', ') + "\n" end end def log_response res if RestClient.log size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size) - RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes" + RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n" end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 6445e9d..9ccd0dd 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -310,29 +310,29 @@ describe RestClient::Request do it "logs a get request" do log = RestClient.log = [] RestClient::Request.new(:method => :get, :url => 'http://url').log_request - ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"*/*; q=0.5, application/xml"', - 'RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) + ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"*/*; q=0.5, application/xml"' + "\n", + 'RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate"' + "\n"].should include(log[0]) end it "logs a post request with a small payload" do log = RestClient.log = [] RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').log_request - ['RestClient.post "http://url", "foo", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3", "Accept"=>"*/*; q=0.5, application/xml"', - 'RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3"'].should include(log[0]) + ['RestClient.post "http://url", "foo", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3", "Accept"=>"*/*; q=0.5, application/xml"' + "\n", + 'RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"3"' + "\n"].should include(log[0]) end it "logs a post request with a large payload" do log = RestClient.log = [] RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request - ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"', - 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"'].should include(log[0]) + ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"' + "\n", + 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"' + "\n"].should include(log[0]) end it "logs input headers as a hash" do log = RestClient.log = [] RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).log_request - ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"text/plain"', - 'RestClient.get "http://url", "Accept"=>"text/plain", "Accept-encoding"=>"gzip, deflate"'].should include(log[0]) + ['RestClient.get "http://url", "Accept-encoding"=>"gzip, deflate", "Accept"=>"text/plain"' + "\n", + 'RestClient.get "http://url", "Accept"=>"text/plain", "Accept-encoding"=>"gzip, deflate"' + "\n"].should include(log[0]) end it "logs a response including the status code, content type, and result body size in bytes" do @@ -340,7 +340,7 @@ describe RestClient::Request do res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') res.stub!(:[]).with('Content-type').and_return('text/html') @request.log_response res - log[0].should == "# => 200 OK | text/html 4 bytes" + log[0].should == "# => 200 OK | text/html 4 bytes\n" end it "logs a response with a nil Content-type" do @@ -348,7 +348,7 @@ describe RestClient::Request do res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') res.stub!(:[]).with('Content-type').and_return(nil) @request.log_response res - log[0].should == "# => 200 OK | 4 bytes" + log[0].should == "# => 200 OK | 4 bytes\n" end it "logs a response with a nil body" do @@ -356,7 +356,7 @@ describe RestClient::Request do res = mock('result', :code => '200', :class => Net::HTTPOK, :body => nil) res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8') @request.log_response res - log[0].should == "# => 200 OK | text/html 0 bytes" + log[0].should == "# => 200 OK | text/html 0 bytes\n" end end @@ -365,7 +365,7 @@ describe RestClient::Request do res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd') res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8') @request.log_response res - log[0].should == "# => 200 OK | text/html 4 bytes" + log[0].should == "# => 200 OK | text/html 4 bytes\n" end describe "timeout" do From f33af2871edef507278ef2488228f6c5ebeee41a Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 18:13:45 +0100 Subject: [PATCH 25/40] Updated description to mention http, as everybody is not aware of rest --- rest-client.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest-client.gemspec b/rest-client.gemspec index 85b9861..14b670d 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -4,10 +4,10 @@ Gem::Specification.new do |s| s.name = %q{rest-client} s.version = "1.2.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Adam Wiggins", "Archiloque"] + s.authors = ["Adam Wiggins", "Julien Kirch"] s.date = %q{2010-01-3} 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.description = %q{A simple Simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.} s.email = %q{rest.client@librelist.com} s.executables = ["restclient"] s.extra_rdoc_files = [ From 13bd7c16c77cde150ad96f902d6ad624ed11540c Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 18:14:05 +0100 Subject: [PATCH 26/40] Added back the logging documentation which has been erased by the merge --- README.rdoc | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/README.rdoc b/README.rdoc index b9b9866..fb3f1a9 100644 --- a/README.rdoc +++ b/README.rdoc @@ -130,6 +130,73 @@ Then invoke: $ restclient private_site +Use as a one-off, curl-style: + + $ restclient get http://example.com/resource > output_body + + $ restclient put http://example.com/resource < input_body + +== Logging + +To enable logging you can + +* set RestClient.log with a ruby Logger +* or set an environment variable to avoid modifying the code (in this case you can use a file name, "stdout" or "stderr"): + + $ RESTCLIENT_LOG=stdout path/to/my/program + +Either produces logs like this: + + RestClient.get "http://some/resource" + # => 200 OK | text/html 250 bytes + RestClient.put "http://some/resource", "payload" + # => 401 Unauthorized | application/xml 340 bytes + +Note that these logs are valid Ruby, so you can paste them into the restclient +shell or a script to replay your sequence of rest calls. + +== Proxy + +All calls to RestClient, including Resources, will use the proxy specified by +RestClient.proxy: + + RestClient.proxy = "http://proxy.example.com/" + RestClient.get "http://some/resource" + # => response from some/resource as proxied through proxy.example.com + +Often the proxy url is set in an environment variable, so you can do this to +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 + +== SSL Client Certificates + + 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. + == Meta Written by Adam Wiggins, major modifications by Blake Mizerany, maintained by Julien Kirch From ddc59ccc8223df04e8addab8a99076e66ecaf796 Mon Sep 17 00:00:00 2001 From: Crawford Date: Sat, 23 Jan 2010 00:19:45 -0500 Subject: [PATCH 27/40] Changed Content-Disposition: multipart/form-data to Content-Disposition: form-data per rfc 2388 --- history.md | 3 ++- lib/restclient/payload.rb | 4 ++-- spec/payload_spec.rb | 12 ++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/history.md b/history.md index 1201cee..3370365 100644 --- a/history.md +++ b/history.md @@ -1,10 +1,11 @@ -# 1.2.1 +# 1.3.0 - a block can be used to process a request's result, this enable to handle custom error codes or paththrought (design by Cyril Rohr) - cleaner log API, add a warning for some cases but should be compatible - accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) - remove "Content-Length" and "Content-Type" headers when following a redirection (patch provided by haarts) - all http error codes have now a corresponding exception class and all of them contain the Reponse +- changed "Content-Disposition: multipart/form-data" to "Content-Disposition: form-data" per RFC 2388 (patch provided by Kyle Crawford) # 1.2.0 diff --git a/lib/restclient/payload.rb b/lib/restclient/payload.rb index e6c80d8..2ac6ae7 100644 --- a/lib/restclient/payload.rb +++ b/lib/restclient/payload.rb @@ -138,7 +138,7 @@ module RestClient end def create_regular_field(s, k, v) - s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"") + s.write("Content-Disposition: form-data; name=\"#{k}\"") s.write(EOL) s.write(EOL) s.write(v) @@ -146,7 +146,7 @@ module RestClient def create_file_field(s, k, v) begin - s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}") + s.write("Content-Disposition: form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}") s.write("Content-Type: #{v.respond_to?(:content_type) ? v.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 cb0af09..054c443 100644 --- a/spec/payload_spec.rb +++ b/spec/payload_spec.rb @@ -39,11 +39,11 @@ describe RestClient::Payload do m = RestClient::Payload::Multipart.new([[:bar, "baz"], [:foo, "bar"]]) m.to_s.should == <<-EOS --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="bar"\r +Content-Disposition: form-data; name="bar"\r \r baz\r --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="foo"\r +Content-Disposition: form-data; name="foo"\r \r bar\r --#{m.boundary}--\r @@ -55,7 +55,7 @@ bar\r m = RestClient::Payload::Multipart.new({:foo => f}) m.to_s.should == <<-EOS --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r +Content-Disposition: form-data; name="foo"; filename="master_shake.jpg"\r Content-Type: image/jpeg\r \r #{IO.read(f.path)}\r @@ -70,7 +70,7 @@ Content-Type: image/jpeg\r m = RestClient::Payload::Multipart.new({:foo => f}) m.to_s.should == <<-EOS --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="foo"; filename="foo.txt"\r +Content-Disposition: form-data; name="foo"; filename="foo.txt"\r Content-Type: text/plain\r \r #{IO.read(f.path)}\r @@ -82,7 +82,7 @@ Content-Type: text/plain\r m = RestClient::Payload::Multipart.new({:bar => {:baz => "foo"}}) m.to_s.should == <<-EOS --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="bar[baz]"\r +Content-Disposition: form-data; name="bar[baz]"\r \r foo\r --#{m.boundary}--\r @@ -94,7 +94,7 @@ foo\r m = RestClient::Payload::Multipart.new({:foo => {:bar => f}}) m.to_s.should == <<-EOS --#{m.boundary}\r -Content-Disposition: multipart/form-data; name="foo[bar]"; filename="foo.txt"\r +Content-Disposition: form-data; name="foo[bar]"; filename="foo.txt"\r Content-Type: text/plain\r \r #{IO.read(f.path)}\r From 00c40c526b64e904f55649a9c782bca73bb22724 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 19:35:53 +0100 Subject: [PATCH 28/40] another summary updated --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 78f5e61..95163b8 100644 --- a/Rakefile +++ b/Rakefile @@ -5,7 +5,7 @@ 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.summary = "Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions." s.author = "Adam Wiggins" s.email = "rest.client@librelist.com" s.homepage = "http://github.com/archiloque/rest-client" From 1c0e2ec75e0681b78d5e86e5742e9e696b9870b6 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 19:36:20 +0100 Subject: [PATCH 29/40] Compatibility --- lib/restclient/exceptions.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 971025e..6d5b8c0 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -28,6 +28,10 @@ module RestClient end + # Compatibility + class ExceptionWithResponse < Exception + end + # We will a create an exception for each status code, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html module Exceptions # Map http status codes to the corresponding exception class @@ -63,7 +67,7 @@ module RestClient 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| - klass = Class.new(Exception) do + klass = Class.new(ExceptionWithResponse) do send(:define_method, :message) {message} end klass = const_set message.gsub(/ /, '').gsub(/-/, ''), klass From c57b00654c05d66f566dcd6224505da226f80a0a Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 19:37:20 +0100 Subject: [PATCH 30/40] Indicate a change --- history.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/history.md b/history.md index 3370365..1093d16 100644 --- a/history.md +++ b/history.md @@ -4,7 +4,7 @@ - cleaner log API, add a warning for some cases but should be compatible - accept multiple "Set-Cookie" headers, see http://www.ietf.org/rfc/rfc2109.txt (patch provided by Cyril Rohr) - remove "Content-Length" and "Content-Type" headers when following a redirection (patch provided by haarts) -- all http error codes have now a corresponding exception class and all of them contain the Reponse +- all http error codes have now a corresponding exception class and all of them contain the Reponse -> this means that the raised exception can be different - changed "Content-Disposition: multipart/form-data" to "Content-Disposition: form-data" per RFC 2388 (patch provided by Kyle Crawford) # 1.2.0 From 2abe1ea39b8a4c6f2d0c4347bed19701cb128be4 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 19:38:59 +0100 Subject: [PATCH 31/40] Another description update ... --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 95163b8..ba1b1b6 100644 --- a/Rakefile +++ b/Rakefile @@ -4,7 +4,7 @@ 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.description = "A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete." s.summary = "Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions." s.author = "Adam Wiggins" s.email = "rest.client@librelist.com" From 0308635a2ea468f5a6f53e9fa755db67244627d1 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 19:41:22 +0100 Subject: [PATCH 32/40] Preparing 1.3.0 --- VERSION | 2 +- rest-client.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 26aaba0..f0bb29e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +1.3.0 diff --git a/rest-client.gemspec b/rest-client.gemspec index 14b670d..e8ed46e 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -2,10 +2,10 @@ Gem::Specification.new do |s| s.name = %q{rest-client} - s.version = "1.2.0" + s.version = "1.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Wiggins", "Julien Kirch"] - s.date = %q{2010-01-3} + s.date = %q{2010-01-25} s.default_executable = %q{restclient} s.description = %q{A simple Simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.} s.email = %q{rest.client@librelist.com} From 58ed49a2b53cc03ee9a7451d0d8ed9d0de42c7ce Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 20:10:13 +0100 Subject: [PATCH 33/40] Better compatibility --- lib/restclient/exceptions.rb | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index 6d5b8c0..d830d08 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -32,6 +32,18 @@ module RestClient class ExceptionWithResponse < Exception end + # The request failed with an error code not managed by the code + class RequestFailed < ExceptionWithResponse + + def message + "HTTP status code #{http_code}" + end + + def to_s + message + end + end + # We will a create an exception for each status code, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html module Exceptions # Map http status codes to the corresponding exception class @@ -67,11 +79,14 @@ module RestClient 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| - klass = Class.new(ExceptionWithResponse) do + + # Compatibility + superclass = ([304, 401, 404].include? code) ? ExceptionWithResponse : RequestFailed + klass = Class.new(superclass) do send(:define_method, :message) {message} end - klass = const_set message.gsub(/ /, '').gsub(/-/, ''), klass - Exceptions::EXCEPTIONS_MAP[code] = klass + klass_constant = const_set message.gsub(/ /, '').gsub(/-/, ''), klass + Exceptions::EXCEPTIONS_MAP[code] = klass_constant end # A redirect was encountered; caught by execute to retry with the new url. @@ -94,17 +109,6 @@ module RestClient end - # The request failed with an error code not managed by the code - class RequestFailed < Exception - - def message - "HTTP status code #{http_code}" - end - - def to_s - message - end - end end From 7563fd5a88457d8317f163282d88ddab4031f1d4 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Mon, 25 Jan 2010 22:04:59 +0100 Subject: [PATCH 34/40] Cleaner structure: Response is no more a String, and the mixin is replaced by an abstract_response --- Rakefile | 2 +- lib/restclient.rb | 2 +- lib/restclient/abstract_response.rb | 58 ++++++++++++++++ lib/restclient/exceptions.rb | 2 +- lib/restclient/mixin/response.rb | 64 ------------------ lib/restclient/payload.rb | 2 +- lib/restclient/raw_response.rb | 5 +- lib/restclient/response.rb | 36 ++++++---- rest-client-next-1.3.0.gem | Bin 0 -> 49152 bytes rest-client.gemspec | 6 +- .../response_spec.rb => abstract_response.rb} | 15 +--- spec/integration_spec.rb | 6 +- spec/request_spec.rb | 7 +- spec/response_spec.rb | 4 +- 14 files changed, 102 insertions(+), 107 deletions(-) create mode 100644 lib/restclient/abstract_response.rb delete mode 100644 lib/restclient/mixin/response.rb create mode 100644 rest-client-next-1.3.0.gem rename spec/{mixin/response_spec.rb => abstract_response.rb} (80%) diff --git a/Rakefile b/Rakefile index ba1b1b6..9b0d405 100644 --- a/Rakefile +++ b/Rakefile @@ -3,7 +3,7 @@ require 'rake' require 'jeweler' Jeweler::Tasks.new do |s| - s.name = "rest-client" + s.name = "rest-client-next" s.description = "A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete." s.summary = "Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions." s.author = "Adam Wiggins" diff --git a/lib/restclient.rb b/lib/restclient.rb index 60c3377..53d95aa 100644 --- a/lib/restclient.rb +++ b/lib/restclient.rb @@ -11,7 +11,7 @@ end require File.dirname(__FILE__) + '/restclient/exceptions' require File.dirname(__FILE__) + '/restclient/request' -require File.dirname(__FILE__) + '/restclient/mixin/response' +require File.dirname(__FILE__) + '/restclient/abstract_response' require File.dirname(__FILE__) + '/restclient/response' require File.dirname(__FILE__) + '/restclient/raw_response' require File.dirname(__FILE__) + '/restclient/resource' diff --git a/lib/restclient/abstract_response.rb b/lib/restclient/abstract_response.rb new file mode 100644 index 0000000..16d485d --- /dev/null +++ b/lib/restclient/abstract_response.rb @@ -0,0 +1,58 @@ +module RestClient + + class AbstractResponse + + attr_reader :net_http_res + + # HTTP status code + 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 + + # The raw headers. + def raw_headers + @raw_headers ||= @net_http_res.to_hash + end + + # Hash of cookies extracted from response headers + def cookies + @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content| + # correctly parse comma-separated cookies containing HTTP dates (which also contain a comma) + cookie_content.split(/,\s*/).inject([""]) { |array, blob| + blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob) + array + }.each do |cookie| + next if cookie.empty? + key, *val = cookie.split(";").first.split("=") + out[key] = val.join("=") + end + out + end + end + + # Return the default behavior corresponding to the response code: + # the response itself for code in 200..206 and an exception in other cases + def return! + if (200..206).include? code + self + elsif Exceptions::EXCEPTIONS_MAP[code] + raise Exceptions::EXCEPTIONS_MAP[code], self + else + raise RequestFailed, self + end + end + + def AbstractResponse.beautify_headers(headers) + headers.inject({}) do |out, (key, value)| + out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first + out + end + end + end +end diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index d830d08..c6b118e 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -19,7 +19,7 @@ module RestClient end def http_body - @response + @response.body end def inspect diff --git a/lib/restclient/mixin/response.rb b/lib/restclient/mixin/response.rb deleted file mode 100644 index 538179d..0000000 --- a/lib/restclient/mixin/response.rb +++ /dev/null @@ -1,64 +0,0 @@ -module RestClient - module Mixin - module Response - attr_reader :net_http_res - - # HTTP status code - 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 - - # The raw headers. - def raw_headers - @raw_headers ||= @net_http_res.to_hash - end - - # Hash of cookies extracted from response headers - def cookies - @cookies ||= (self.headers[:set_cookie] || []).inject({}) do |out, cookie_content| - # correctly parse comma-separated cookies containing HTTP dates (which also contain a comma) - cookie_content.split(/,\s*/).inject([""]) { |array, blob| - blob =~ /expires=.+?$/ ? array.push(blob) : array.last.concat(blob) - array - }.each do |cookie| - next if cookie.empty? - key, *val = cookie.split(";").first.split("=") - out[key] = val.join("=") - end - out - end - end - - # Return the default behavior corresponding to the response code: - # the response itself for code in 200..206 and an exception in other cases - def return! - if (200..206).include? code - self - elsif Exceptions::EXCEPTIONS_MAP[code] - raise Exceptions::EXCEPTIONS_MAP[code], self - else - raise RequestFailed, self - 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(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first - out - end - end - end - end - end -end diff --git a/lib/restclient/payload.rb b/lib/restclient/payload.rb index 2ac6ae7..4d0ae2c 100644 --- a/lib/restclient/payload.rb +++ b/lib/restclient/payload.rb @@ -88,7 +88,7 @@ module RestClient end def short_inspect - (size > 100 ? "#{size} byte length" : inspect) + (size > 100 ? "#{size} byte(s) length" : inspect) end end diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb index 730c8b9..d6499eb 100644 --- a/lib/restclient/raw_response.rb +++ b/lib/restclient/raw_response.rb @@ -1,5 +1,3 @@ -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 @@ -11,8 +9,7 @@ module RestClient # 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 + class RawResponse < AbstractResponse attr_reader :file diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index 80c0d1a..bfe6759 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -1,19 +1,31 @@ -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 - # 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 - include RestClient::Mixin::Response + # A Response from RestClient, you can access the response body, the code or the headers. + # + class Response < AbstractResponse - def initialize(string, net_http_res) + attr_reader :body + + def initialize(body, net_http_res) @net_http_res = net_http_res - super(string || "") + @body = body || "" + end + + def method_missing symbol, *args + if body.respond_to? symbol + warn "[warning] The Response is no more a String, please update your code" + body.send symbol, *args + else + super + end + end + + def to_s + body.to_s + end + + def inspect + "Code #{code} #{headers[:content_type] ? "#{headers[:content_type] } ": ''} #{body.size} byte(s)" end end diff --git a/rest-client-next-1.3.0.gem b/rest-client-next-1.3.0.gem new file mode 100644 index 0000000000000000000000000000000000000000..8fd45dfd7bf9bd447d4572c284f80890e804da22 GIT binary patch literal 49152 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⪒i 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

S2L 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 literal 0 HcmV?d00001 diff --git a/rest-client.gemspec b/rest-client.gemspec index e8ed46e..bf38dff 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |s| - s.name = %q{rest-client} + s.name = %q{rest-client-next} s.version = "1.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Wiggins", "Julien Kirch"] @@ -21,7 +21,7 @@ Gem::Specification.new do |s| "lib/rest_client.rb", "lib/restclient.rb", "lib/restclient/exceptions.rb", - "lib/restclient/mixin/response.rb", + "lib/restclient/abstract_response.rb", "lib/restclient/net_http_ext.rb", "lib/restclient/payload.rb", "lib/restclient/raw_response.rb", @@ -32,7 +32,7 @@ Gem::Specification.new do |s| "spec/exceptions_spec.rb", "spec/integration_spec.rb", "spec/master_shake.jpg", - "spec/mixin/response_spec.rb", + "spec/abstract_response_spec.rb", "spec/payload_spec.rb", "spec/raw_response_spec.rb", "spec/request_spec.rb", diff --git a/spec/mixin/response_spec.rb b/spec/abstract_response.rb similarity index 80% rename from spec/mixin/response_spec.rb rename to spec/abstract_response.rb index 41b1c73..9d7ace0 100644 --- a/spec/mixin/response_spec.rb +++ b/spec/abstract_response.rb @@ -1,18 +1,9 @@ -require File.dirname(__FILE__) + '/../base' +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 +describe RestClient::AbstractResponse do before do @net_http_res = mock('net http response') - @response = MockResponse.new('abc', @net_http_res) + @response = AbstractResponse.new('abc', @net_http_res) end it "fetches the numeric response code" do diff --git a/spec/integration_spec.rb b/spec/integration_spec.rb index 1e1b0b7..b4a496a 100644 --- a/spec/integration_spec.rb +++ b/spec/integration_spec.rb @@ -10,14 +10,14 @@ describe RestClient do stub_request(:get, "www.example.com").to_return(:body => body, :status => 200) response = RestClient.get "www.example.com" response.code.should == 200 - response.should == body + response.body.should == body end it "a simple request with gzipped content" do stub_request(:get, "www.example.com").with(:headers => { 'Accept-Encoding' => 'gzip, deflate' }).to_return(:body => "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000", :status => 200, :headers => { 'Content-Encoding' => 'gzip' } ) response = RestClient.get "www.example.com" response.code.should == 200 - response.should == "i'm gziped\n" + response.body.should == "i'm gziped\n" end it "a 404" do @@ -29,7 +29,7 @@ describe RestClient do rescue RestClient::ResourceNotFound => e e.http_code.should == 404 e.response.code.should == 404 - e.response.should == body + e.response.body.should == body e.http_body.should == body end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index 9ccd0dd..8b80d0f 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -45,7 +45,8 @@ describe RestClient::Request do 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' + @request.process_result(res).body.should == 'body' + @request.process_result(res).to_s.should == 'body' end it "doesn't classify successful requests as failed" do @@ -324,8 +325,8 @@ describe RestClient::Request do it "logs a post request with a large payload" do log = RestClient.log = [] RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request - ['RestClient.post "http://url", 1000 byte length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"' + "\n", - 'RestClient.post "http://url", 1000 byte length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"' + "\n"].should include(log[0]) + ['RestClient.post "http://url", 1000 byte(s) length, "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000", "Accept"=>"*/*; q=0.5, application/xml"' + "\n", + 'RestClient.post "http://url", 1000 byte(s) length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-encoding"=>"gzip, deflate", "Content-Length"=>"1000"' + "\n"].should include(log[0]) end it "logs input headers as a hash" do diff --git a/spec/response_spec.rb b/spec/response_spec.rb index 07489a8..269f584 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -7,11 +7,11 @@ describe RestClient::Response do end it "behaves like string" do - @response.should == 'abc' + @response.should.to_s == 'abc' end it "accepts nil strings and sets it to empty for the case of HEAD" do - RestClient::Response.new(nil, @net_http_res).should == "" + RestClient::Response.new(nil, @net_http_res).should.to_s == "" end it "test headers and raw headers" do From e0baffea9a1a0ef1b35e8954c48ef15610b97314 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Tue, 26 Jan 2010 18:57:48 +0100 Subject: [PATCH 35/40] Finished renaming spec --- rest-client.gemspec | 4 ++-- spec/{abstract_response.rb => abstract_response_spec.rb} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename spec/{abstract_response.rb => abstract_response_spec.rb} (100%) diff --git a/rest-client.gemspec b/rest-client.gemspec index bf38dff..5d1c682 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- Gem::Specification.new do |s| - s.name = %q{rest-client-next} + s.name = %q{rest-client} s.version = "1.3.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Adam Wiggins", "Julien Kirch"] @@ -50,7 +50,7 @@ Gem::Specification.new do |s| "spec/base.rb", "spec/exceptions_spec.rb", "spec/integration_spec.rb", - "spec/mixin/response_spec.rb", + "spec/abstract_response_spec.rb", "spec/payload_spec.rb", "spec/raw_response_spec.rb", "spec/request_spec.rb", diff --git a/spec/abstract_response.rb b/spec/abstract_response_spec.rb similarity index 100% rename from spec/abstract_response.rb rename to spec/abstract_response_spec.rb From c8c45272af45dd58feb83f2a182329f0932227a8 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Tue, 26 Jan 2010 19:22:08 +0100 Subject: [PATCH 36/40] Oups, commited the gem --- rest-client-next-1.3.0.gem | Bin 49152 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 rest-client-next-1.3.0.gem diff --git a/rest-client-next-1.3.0.gem b/rest-client-next-1.3.0.gem deleted file mode 100644 index 8fd45dfd7bf9bd447d4572c284f80890e804da22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49152 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⪒i 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

S2L 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 From 3b8586f6c900efc3329324daebdc40c2cbbd615e Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Tue, 26 Jan 2010 19:41:53 +0100 Subject: [PATCH 37/40] Response.inspect use the same format as the logging --- lib/restclient/abstract_response.rb | 5 +++ lib/restclient/exceptions.rb | 70 +++++++++++++++++------------ lib/restclient/raw_response.rb | 4 ++ lib/restclient/response.rb | 4 +- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/lib/restclient/abstract_response.rb b/lib/restclient/abstract_response.rb index 16d485d..dda0477 100644 --- a/lib/restclient/abstract_response.rb +++ b/lib/restclient/abstract_response.rb @@ -48,6 +48,11 @@ module RestClient end end + def inspect + "#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n" + end + + def AbstractResponse.beautify_headers(headers) headers.inject({}) do |out, (key, value)| out[key.gsub(/-/, '_').downcase.to_sym] = %w{set-cookie}.include?(key.downcase) ? value : value.first diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb index c6b118e..8242800 100644 --- a/lib/restclient/exceptions.rb +++ b/lib/restclient/exceptions.rb @@ -1,5 +1,45 @@ module RestClient + + STATUSES = {100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 403 => 'Forbidden', + 404 => 'Resource Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported'} + # This is the base RestClient exception class. Rescue it if you want to # catch any exception that your request might raise # You can get the status code by e.http_code, or see anything about the @@ -50,35 +90,7 @@ module RestClient EXCEPTIONS_MAP = {} end - {300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 400 => 'Bad Request', - 401 => 'Unauthorized', - 403 => 'Forbidden', - 404 => 'Resource Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Timeout', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Long', - 415 => 'Unsupported Media Type', - 416 => 'Requested Range Not Satisfiable', - 417 => 'Expectation Failed', - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Timeout', - 505 => 'HTTP Version Not Supported'}.each_pair do |code, message| + STATUSES.each_pair do |code, message| # Compatibility superclass = ([304, 401, 404].include? code) ? ExceptionWithResponse : RequestFailed diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb index d6499eb..8f4274c 100644 --- a/lib/restclient/raw_response.rb +++ b/lib/restclient/raw_response.rb @@ -23,5 +23,9 @@ module RestClient @file.read end + def size + File.size file + end + end end diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index bfe6759..12cba9b 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -24,8 +24,8 @@ module RestClient body.to_s end - def inspect - "Code #{code} #{headers[:content_type] ? "#{headers[:content_type] } ": ''} #{body.size} byte(s)" + def size + body.size end end From a683de6a1b9b9028f197e41788920fd2f103abe0 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 27 Jan 2010 22:54:18 +0100 Subject: [PATCH 38/40] Added block feature to resource --- lib/restclient/resource.rb | 21 +++++++++++---------- spec/resource_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/lib/restclient/resource.rb b/lib/restclient/resource.rb index 39f71b8..6d551a2 100644 --- a/lib/restclient/resource.rb +++ b/lib/restclient/resource.rb @@ -34,10 +34,11 @@ module RestClient # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain' # class Resource - attr_reader :url, :options + attr_reader :url, :options, :block - def initialize(url, options={}, backwards_compatibility=nil) + def initialize(url, options={}, backwards_compatibility=nil, &block) @url = url + @block = block if options.class == Hash @options = options else # compatibility with previous versions @@ -45,38 +46,38 @@ module RestClient end end - def get(additional_headers={}, &b) + def get(additional_headers={}, &block) headers = (options[:headers] || {}).merge(additional_headers) Request.execute(options.merge( :method => :get, :url => url, - :headers => headers), &b) + :headers => headers), &(block || @block)) end - def post(payload, additional_headers={}, &b) + def post(payload, additional_headers={}, &block) headers = (options[:headers] || {}).merge(additional_headers) Request.execute(options.merge( :method => :post, :url => url, :payload => payload, - :headers => headers), &b) + :headers => headers), &(block || @block)) end - def put(payload, additional_headers={}, &b) + def put(payload, additional_headers={}, &block) headers = (options[:headers] || {}).merge(additional_headers) Request.execute(options.merge( :method => :put, :url => url, :payload => payload, - :headers => headers), &b) + :headers => headers), &(block || @block)) end - def delete(additional_headers={}, &b) + def delete(additional_headers={}, &block) headers = (options[:headers] || {}).merge(additional_headers) Request.execute(options.merge( :method => :delete, :url => url, - :headers => headers), &b) + :headers => headers), &(block || @block)) end def to_s diff --git a/spec/resource_spec.rb b/spec/resource_spec.rb index 36de73c..ec0a652 100644 --- a/spec/resource_spec.rb +++ b/spec/resource_spec.rb @@ -1,5 +1,8 @@ require File.dirname(__FILE__) + '/base' +require 'webmock/rspec' +include WebMock + describe RestClient::Resource do before do @resource = RestClient::Resource.new('http://some/resource', :user => 'jane', :password => 'mypass', :headers => { 'X-Something' => '1'}) @@ -72,4 +75,25 @@ describe RestClient::Resource do it "prints its url with to_s" do RestClient::Resource.new('x').to_s.should == 'x' end + + describe 'block' do + it 'can use block when creating the resource' do + stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404) + resource = RestClient::Resource.new('www.example.com'){|response| 'foo'} + resource.get.should == 'foo' + end + + it 'can use block when executing the resource' do + stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404) + resource = RestClient::Resource.new('www.example.com') + resource.get{|response| 'foo'}.should == 'foo' + end + + it 'execution block override resource block' do + stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404) + resource = RestClient::Resource.new('www.example.com'){|response| 'foo'} + resource.get{|response| 'bar'}.should == 'bar' + end + + end end From f7b16453831725bcbec766d78adec59b031a740c Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Wed, 27 Jan 2010 23:07:03 +0100 Subject: [PATCH 39/40] Fixed tests --- lib/restclient/abstract_response.rb | 4 ++++ lib/restclient/raw_response.rb | 4 ++-- lib/restclient/response.rb | 4 ++-- spec/abstract_response_spec.rb | 2 +- spec/response_spec.rb | 8 +++++--- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/restclient/abstract_response.rb b/lib/restclient/abstract_response.rb index dda0477..3915909 100644 --- a/lib/restclient/abstract_response.rb +++ b/lib/restclient/abstract_response.rb @@ -4,6 +4,10 @@ module RestClient attr_reader :net_http_res + def initialize net_http_res + @net_http_res = net_http_res + end + # HTTP status code def code @code ||= @net_http_res.code.to_i diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb index 8f4274c..7890b36 100644 --- a/lib/restclient/raw_response.rb +++ b/lib/restclient/raw_response.rb @@ -13,8 +13,8 @@ module RestClient attr_reader :file - def initialize(tempfile, net_http_res) - @net_http_res = net_http_res + def initialize tempfile, net_http_res + super net_http_res @file = tempfile end diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb index 12cba9b..0d77127 100644 --- a/lib/restclient/response.rb +++ b/lib/restclient/response.rb @@ -6,8 +6,8 @@ module RestClient attr_reader :body - def initialize(body, net_http_res) - @net_http_res = net_http_res + def initialize body, net_http_res + super net_http_res @body = body || "" end diff --git a/spec/abstract_response_spec.rb b/spec/abstract_response_spec.rb index 9d7ace0..1dddf38 100644 --- a/spec/abstract_response_spec.rb +++ b/spec/abstract_response_spec.rb @@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/base' describe RestClient::AbstractResponse do before do @net_http_res = mock('net http response') - @response = AbstractResponse.new('abc', @net_http_res) + @response = RestClient::AbstractResponse.new(@net_http_res) end it "fetches the numeric response code" do diff --git a/spec/response_spec.rb b/spec/response_spec.rb index 269f584..b5e971a 100644 --- a/spec/response_spec.rb +++ b/spec/response_spec.rb @@ -60,9 +60,11 @@ describe RestClient::Response do it "should throw an exception for other codes" do RestClient::Exceptions::EXCEPTIONS_MAP.each_key do |code| - net_http_res = mock('net http response', :code => code.to_i) - response = RestClient::Response.new('abc', net_http_res) - lambda { response.return!}.should raise_error + unless (200..206).include? code + net_http_res = mock('net http response', :code => code.to_i) + response = RestClient::Response.new('abc', net_http_res) + lambda { response.return!}.should raise_error + end end end From fe0eadb0a8345cbf5db7022912521c1cf5565224 Mon Sep 17 00:00:00 2001 From: Julien Kirch Date: Fri, 29 Jan 2010 21:27:39 +0100 Subject: [PATCH 40/40] history for 1.4.0 --- history.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/history.md b/history.md index 1093d16..8e8548a 100644 --- a/history.md +++ b/history.md @@ -1,3 +1,9 @@ +# 1.4.0 + +- Response is no more a String, and the mixin is replaced by an abstract_response, existing calls are redirected to response body with a warning. + +The response change may be breaking in rare cases. + # 1.3.0 - a block can be used to process a request's result, this enable to handle custom error codes or paththrought (design by Cyril Rohr) @@ -7,6 +13,8 @@ - all http error codes have now a corresponding exception class and all of them contain the Reponse -> this means that the raised exception can be different - changed "Content-Disposition: multipart/form-data" to "Content-Disposition: form-data" per RFC 2388 (patch provided by Kyle Crawford) +The only breaking change should be the exception classes, but as the new classes inherits from the existing ones, the breaking cases should be rare. + # 1.2.0 - formatting changed from tabs to spaces