1
0
Fork 0
mirror of https://github.com/rest-client/rest-client.git synced 2022-11-09 13:49:40 -05:00

Merge branch 'master' into prereorg

Conflicts:
	lib/rest_client.rb
	lib/restclient/resource.rb
	rest-client.gemspec
	spec/rest_client_spec.rb
This commit is contained in:
François Beausoleil 2009-08-12 12:44:31 -04:00
commit 3425532660
23 changed files with 1182 additions and 754 deletions

View file

@ -1,4 +1,24 @@
require 'rake'
require 'jeweler'
Jeweler::Tasks.new do |s|
s.name = "rest-client"
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
s.author = "Adam Wiggins"
s.email = "adam@heroku.com"
s.homepage = "http://rest-client.heroku.com/"
s.rubyforge_project = "rest-client"
s.has_rdoc = true
s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
s.executables = %w(restclient)
end
Jeweler::RubyforgeTasks.new
############################
require 'spec/rake/spectask'
desc "Run all specs"
@ -22,54 +42,9 @@ end
task :default => :spec
######################################################
############################
require 'rake'
require 'rake/testtask'
require 'rake/clean'
require 'rake/gempackagetask'
require 'rake/rdoctask'
require 'fileutils'
version = "0.8.2"
name = "rest-client"
spec = Gem::Specification.new do |s|
s.name = name
s.version = version
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
s.author = "Adam Wiggins"
s.email = "adam@heroku.com"
s.homepage = "http://rest-client.heroku.com/"
s.rubyforge_project = "rest-client"
s.platform = Gem::Platform::RUBY
s.has_rdoc = true
s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*")
s.executables = ['restclient']
s.require_path = "lib"
end
Rake::GemPackageTask.new(spec) do |p|
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
end
task :install => [ :package ] do
sh %{sudo gem install pkg/#{name}-#{version}.gem}
end
task :uninstall => [ :clean ] do
sh %{sudo gem uninstall #{name}}
end
Rake::TestTask.new do |t|
t.libs << "spec"
t.test_files = FileList['spec/*_spec.rb']
t.verbose = true
end
Rake::RDocTask.new do |t|
t.rdoc_dir = 'rdoc'
@ -80,5 +55,3 @@ Rake::RDocTask.new do |t|
t.rdoc_files.include('lib/*.rb')
end
CLEAN.include [ 'pkg', '*.gem', '.config' ]

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.2.1

View file

@ -1,7 +1,7 @@
#!/usr/bin/env ruby
$:.unshift File.dirname(__FILE__) + "/../lib"
require 'rest_client'
require 'restclient'
require "yaml"

View file

@ -1,293 +1,2 @@
require 'uri'
require 'net/https'
require 'zlib'
require 'stringio'
require File.dirname(__FILE__) + '/rest_client/resource'
require File.dirname(__FILE__) + '/rest_client/request_errors'
require File.dirname(__FILE__) + '/rest_client/payload'
require File.dirname(__FILE__) + '/rest_client/net_http_ext'
# This module's static methods are the entry point for using the REST client.
#
# # GET
# xml = RestClient.get 'http://example.com/resource'
# jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
#
# # authentication and SSL
# RestClient.get 'https://user:password@example.com/private/resource'
#
# # POST or PUT with a hash sends parameters as a urlencoded form body
# RestClient.post 'http://example.com/resource', :param1 => 'one'
#
# # nest hash parameters
# RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
#
# # POST and PUT with raw payloads
# RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
# RestClient.post 'http://example.com/resource.xml', xml_doc
# RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
#
# # DELETE
# RestClient.delete 'http://example.com/resource'
#
# To use with a proxy, just set RestClient.proxy to the proper http proxy:
#
# RestClient.proxy = "http://proxy.example.com/"
#
# Or inherit the proxy from the environment:
#
# RestClient.proxy = ENV['http_proxy']
#
# For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
#
# >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
# => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
#
module RestClient
def self.get(url, headers={}, &b)
Request.execute(:method => :get,
:url => url,
:headers => headers, &b)
end
def self.post(url, payload, headers={}, &b)
Request.execute(:method => :post,
:url => url,
:payload => payload,
:headers => headers, &b)
end
def self.put(url, payload, headers={}, &b)
Request.execute(:method => :put,
:url => url,
:payload => payload,
:headers => headers, &b)
end
def self.delete(url, headers={}, &b)
Request.execute(:method => :delete,
:url => url,
:headers => headers, &b)
end
class <<self
attr_accessor :proxy
end
# Print log of RestClient calls. Value can be stdout, stderr, or a filename.
# You can also configure logging by the environment variable RESTCLIENT_LOG.
def self.log=(log)
@@log = log
end
def self.log # :nodoc:
return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
return @@log if defined? @@log
nil
end
# Internal class used to build and execute the request.
class Request
attr_reader :method, :url, :payload, :headers, :user, :password, :timeout
def self.execute(args, &b)
new(args).execute(&b)
end
def initialize(args)
@method = args[:method] or raise ArgumentError, "must pass :method"
@url = args[:url] or raise ArgumentError, "must pass :url"
@payload = Payload.generate(args[:payload] || '')
@headers = args[:headers] || {}
@user = args[:user]
@password = args[:password]
@timeout = args[:timeout]
end
def execute(&b)
execute_inner(&b)
rescue Redirect => e
@url = e.url
execute(&b)
end
def execute_inner(&b)
uri = parse_url_with_auth(url)
transmit(uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload, &b)
end
def make_headers(user_headers)
default_headers.merge(user_headers).inject({}) do |final, (key, value)|
final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
final
end
end
def net_http_class
if RestClient.proxy
proxy_uri = URI.parse(RestClient.proxy)
Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
else
Net::HTTP
end
end
def net_http_request_class(method)
Net::HTTP.const_get(method.to_s.capitalize)
end
def parse_url(url)
url = "http://#{url}" unless url.match(/^http/)
URI.parse(url)
end
def parse_url_with_auth(url)
uri = parse_url(url)
@user = uri.user if uri.user
@password = uri.password if uri.password
uri
end
def process_payload(p=nil, parent_key=nil)
unless p.is_a?(Hash)
p
else
@headers[:content_type] ||= 'application/x-www-form-urlencoded'
p.keys.map do |k|
key = parent_key ? "#{parent_key}[#{k}]" : k
if p[k].is_a? Hash
process_payload(p[k], key)
else
value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
"#{key}=#{value}"
end
end.join("&")
end
end
def transmit(uri, req, payload, &b)
setup_credentials(req)
net = net_http_class.new(uri.host, uri.port)
net.use_ssl = uri.is_a?(URI::HTTPS)
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
display_log request_log
net.start do |http|
http.read_timeout = @timeout if @timeout
res = http.request(req, payload)
display_log response_log(res)
## Ok. I know this is weird but it's a hack for now
## this lets process_result determine if it should read the body
## into memory or not
string = process_result(http.request(req, payload || "", &b), &b)
if string
Response.new(string, res)
else
nil
end
end
rescue EOFError
raise RestClient::ServerBrokeConnection
rescue Timeout::Error
raise RestClient::RequestTimeout
ensure
payload.close
end
def setup_credentials(req)
req.basic_auth(user, password) if user
end
def process_result(res, &b)
if %w(200 201 202).include? res.code
decode(res['content-encoding'], res.body) unless b
elsif %w(301 302 303).include? res.code
url = res.header['Location']
if url !~ /^http/
uri = URI.parse(@url)
uri.path = "/#{url}".squeeze('/')
url = uri.to_s
end
raise Redirect, url
elsif res.code == "304"
raise NotModified
elsif res.code == "401"
raise Unauthorized, res
elsif res.code == "404"
raise ResourceNotFound, res
else
raise RequestFailed, res
end
end
def payload
@payload
def decode(content_encoding, body)
if content_encoding == 'gzip'
Zlib::GzipReader.new(StringIO.new(body)).read
elsif content_encoding == 'deflate'
Zlib::Inflate.new.inflate(body)
else
body
end
end
def request_log
out = []
out << "RestClient.#{method} #{url.inspect}"
out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
out.join(', ')
end
def response_log(res)
"# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{(res.body) ? res.body.size : nil} bytes"
end
def display_log(msg)
return unless log_to = RestClient.log
if log_to == 'stdout'
STDOUT.puts msg
elsif log_to == 'stderr'
STDERR.puts msg
else
File.open(log_to, 'a') { |f| f.puts msg }
end
end
def default_headers
@payload.headers.merge({ :accept => 'application/xml', :accept_encoding => 'gzip, deflate' })
end
end
class Response < String
attr_reader :net_http_res
def initialize(string, net_http_res)
@net_http_res = net_http_res
super string
end
def code
@code ||= @net_http_res.code.to_i
end
def headers
@headers ||= self.class.beautify_headers(@net_http_res.to_hash)
end
def self.beautify_headers(headers)
headers.inject({}) do |out, (key, value)|
out[key.gsub(/-/, '_').to_sym] = value.first
out
end
end
end
end
# This file exists for backward compatbility with require 'rest_client'
require File.dirname(__FILE__) + '/restclient'

101
lib/restclient.rb Normal file
View file

@ -0,0 +1,101 @@
require 'uri'
require 'zlib'
require 'stringio'
begin
require 'net/https'
rescue LoadError => e
raise e unless RUBY_PLATFORM =~ /linux/
raise LoadError, "no such file to load -- net/https. Try running apt-get install libopenssl-ruby"
end
require File.dirname(__FILE__) + '/restclient/request'
require File.dirname(__FILE__) + '/restclient/mixin/response'
require File.dirname(__FILE__) + '/restclient/response'
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'
# This module's static methods are the entry point for using the REST client.
#
# # GET
# xml = RestClient.get 'http://example.com/resource'
# jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
#
# # authentication and SSL
# RestClient.get 'https://user:password@example.com/private/resource'
#
# # POST or PUT with a hash sends parameters as a urlencoded form body
# RestClient.post 'http://example.com/resource', :param1 => 'one'
#
# # nest hash parameters
# RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
#
# # POST and PUT with raw payloads
# RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
# RestClient.post 'http://example.com/resource.xml', xml_doc
# RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
#
# # DELETE
# RestClient.delete 'http://example.com/resource'
#
# # retreive the response http code and headers
# res = RestClient.get 'http://example.com/some.jpg'
# res.code # => 200
# res.headers[:content_type] # => 'image/jpg'
#
# # HEAD
# RestClient.head('http://example.com').headers
#
# To use with a proxy, just set RestClient.proxy to the proper http proxy:
#
# RestClient.proxy = "http://proxy.example.com/"
#
# Or inherit the proxy from the environment:
#
# RestClient.proxy = ENV['http_proxy']
#
# For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
#
# >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
# => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
#
module RestClient
def self.get(url, headers={})
Request.execute(:method => :get, :url => url, :headers => headers)
end
def self.post(url, payload, headers={})
Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers)
end
def self.put(url, payload, headers={})
Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers)
end
def self.delete(url, headers={})
Request.execute(:method => :delete, :url => url, :headers => headers)
end
def self.head(url, headers={})
Request.execute(:method => :head, :url => url, :headers => headers)
end
class << self
attr_accessor :proxy
end
# Print log of RestClient calls. Value can be stdout, stderr, or a filename.
# You can also configure logging by the environment variable RESTCLIENT_LOG.
def self.log=(log)
@@log = log
end
def self.log # :nodoc:
return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
return @@log if defined? @@log
nil
end
end

View file

@ -18,6 +18,10 @@ module RestClient
def http_code
@response.code.to_i if @response
end
def http_body
RestClient::Request.decode(@response['content-encoding'], @response.body) if @response
end
end
# A redirect was encountered; caught by execute to retry with the new url.
@ -30,7 +34,7 @@ module RestClient
end
end
class NotModified < Exception
class NotModified < ExceptionWithResponse
ErrorMessage = 'NotModified'
end
@ -44,7 +48,9 @@ module RestClient
ErrorMessage = 'Resource not found'
end
# The server broke the connection prior to the request completing.
# The server broke the connection prior to the request completing. Usually
# this means it crashed, or sometimes that your network connection was
# severed before it could complete.
class ServerBrokeConnection < Exception
ErrorMessage = 'Server broke connection'
end

View file

@ -0,0 +1,43 @@
module RestClient
module Mixin
module Response
attr_reader :net_http_res
# HTTP status code, always 200 since RestClient throws exceptions for
# other codes.
def code
@code ||= @net_http_res.code.to_i
end
# A hash of the headers, beautified with symbols and underscores.
# e.g. "Content-type" will become :content_type.
def headers
@headers ||= self.class.beautify_headers(@net_http_res.to_hash)
end
# Hash of cookies extracted from response headers
def cookies
@cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c|
key, val = raw_c.split('=')
unless %w(expires domain path secure).member?(key)
out[key] = val
end
out
end
end
def self.included(receiver)
receiver.extend(RestClient::Mixin::Response::ClassMethods)
end
module ClassMethods
def beautify_headers(headers)
headers.inject({}) do |out, (key, value)|
out[key.gsub(/-/, '_').to_sym] = value.first
out
end
end
end
end
end
end

View file

@ -8,14 +8,24 @@ module RestClient
def generate(params)
if params.is_a?(String)
Base.new(params)
elsif params.delete(:multipart) == true ||
params.any? { |_,v| v.respond_to?(:path) && v.respond_to?(:read) }
elsif params.delete(:multipart) == true || has_file?(params)
Multipart.new(params)
else
UrlEncoded.new(params)
end
end
def has_file?(params)
params.any? do |_, v|
case v
when Hash
has_file?(v)
else
v.respond_to?(:path) && v.respond_to?(:read)
end
end
end
class Base
def initialize(params)
build_stream(params)
@ -92,7 +102,7 @@ module RestClient
def create_file_field(s, k, v)
begin
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}")
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{File.basename(v.path)}\"#{EOL}")
s.write("Content-Type: #{mime_for(v.path)}#{EOL}")
s.write(EOL)
while data = v.read(8124)

View file

@ -0,0 +1,30 @@
require File.dirname(__FILE__) + '/mixin/response'
module RestClient
# The response from RestClient on a raw request looks like a string, but is
# actually one of these. 99% of the time you're making a rest call all you
# care about is the body, but on the occassion you want to fetch the
# headers you can:
#
# RestClient.get('http://example.com').headers[:content_type]
#
# In addition, if you do not use the response as a string, you can access
# a Tempfile object at res.file, which contains the path to the raw
# downloaded request body.
class RawResponse
include RestClient::Mixin::Response
attr_reader :file
def initialize(tempfile, net_http_res)
@net_http_res = net_http_res
@file = tempfile
end
def to_s
@file.open
@file.read
end
end
end

238
lib/restclient/request.rb Normal file
View file

@ -0,0 +1,238 @@
require 'tempfile'
module RestClient
# This class is used internally by RestClient to send the request, but you can also
# access it internally if you'd like to use a method not directly supported by the
# main API. For example:
#
# RestClient::Request.execute(:method => :head, :url => 'http://example.com')
#
class Request
attr_reader :method, :url, :payload, :headers,
:cookies, :user, :password, :timeout, :open_timeout,
:verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
:raw_response
def self.execute(args)
new(args).execute
end
def initialize(args)
@method = args[:method] or raise ArgumentError, "must pass :method"
@url = args[:url] or raise ArgumentError, "must pass :url"
@headers = args[:headers] || {}
@cookies = @headers.delete(:cookies) || args[:cookies] || {}
@payload = process_payload(args[:payload])
@user = args[:user]
@password = args[:password]
@timeout = args[:timeout]
@open_timeout = args[:open_timeout]
@raw_response = args[:raw_response] || false
@verify_ssl = args[:verify_ssl] || false
@ssl_client_cert = args[:ssl_client_cert] || nil
@ssl_client_key = args[:ssl_client_key] || nil
@ssl_ca_file = args[:ssl_ca_file] || nil
@tf = nil # If you are a raw request, this is your tempfile
end
def execute
execute_inner
rescue Redirect => e
@url = e.url
@method = :get
@payload = nil
execute
end
def execute_inner
uri = parse_url_with_auth(url)
transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
end
def make_headers(user_headers)
unless @cookies.empty?
user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
end
default_headers.merge(user_headers).inject({}) do |final, (key, value)|
final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
final
end
end
def net_http_class
if RestClient.proxy
proxy_uri = URI.parse(RestClient.proxy)
Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
else
Net::HTTP
end
end
def net_http_request_class(method)
Net::HTTP.const_get(method.to_s.capitalize)
end
def parse_url(url)
url = "http://#{url}" unless url.match(/^http/)
URI.parse(url)
end
def parse_url_with_auth(url)
uri = parse_url(url)
@user = uri.user if uri.user
@password = uri.password if uri.password
uri
end
def process_payload(p=nil, parent_key=nil)
unless p.is_a?(Hash)
p
else
@headers[:content_type] ||= 'application/x-www-form-urlencoded'
p.keys.map do |k|
key = parent_key ? "#{parent_key}[#{k}]" : k
if p[k].is_a? Hash
process_payload(p[k], key)
else
value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
"#{key}=#{value}"
end
end.join("&")
end
end
def transmit(uri, req, payload)
setup_credentials(req)
net = net_http_class.new(uri.host, uri.port)
net.use_ssl = uri.is_a?(URI::HTTPS)
if @verify_ssl == false
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
elsif @verify_ssl.is_a? Integer
net.verify_mode = @verify_ssl
end
net.cert = @ssl_client_cert if @ssl_client_cert
net.key = @ssl_client_key if @ssl_client_key
net.ca_file = @ssl_ca_file if @ssl_ca_file
net.read_timeout = @timeout if @timeout
net.open_timeout = @open_timeout if @open_timeout
display_log request_log
net.start do |http|
res = http.request(req, payload) { |http_response| fetch_body(http_response) }
result = process_result(res)
display_log response_log(res)
if result.kind_of?(String) or @method == :head
Response.new(result, res)
elsif @raw_response
RawResponse.new(@tf, res)
else
nil
end
end
rescue EOFError
raise RestClient::ServerBrokeConnection
rescue Timeout::Error
raise RestClient::RequestTimeout
end
def setup_credentials(req)
req.basic_auth(user, password) if user
end
def fetch_body(http_response)
if @raw_response
# Taken from Chef, which as in turn...
# Stolen from http://www.ruby-forum.com/topic/166423
# Kudos to _why!
@tf = Tempfile.new("rest-client")
size, total = 0, http_response.header['Content-Length'].to_i
http_response.read_body do |chunk|
@tf.write(chunk)
size += chunk.size
if size == 0
display_log("#{@method} #{@url} done (0 length file)")
elsif total == 0
display_log("#{@method} #{@url} (zero content length)")
else
display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
end
end
@tf.close
@tf
else
http_response.read_body
end
http_response
end
def process_result(res)
if res.code =~ /\A2\d{2}\z/
# 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
url = res.header['Location']
if url !~ /^http/
uri = URI.parse(@url)
uri.path = "/#{url}".squeeze('/')
url = uri.to_s
end
raise Redirect, url
elsif res.code == "304"
raise NotModified, res
elsif res.code == "401"
raise Unauthorized, res
elsif res.code == "404"
raise ResourceNotFound, res
else
raise RequestFailed, res
end
end
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)
else
body
end
end
def request_log
out = []
out << "RestClient.#{method} #{url.inspect}"
out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
out.join(', ')
end
def response_log(res)
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 }
end
end
def default_headers
{ :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
end
end
end

View file

@ -16,6 +16,10 @@ module RestClient
#
# RestClient::Resource.new('http://slow', :timeout => 10)
#
# With an open timeout (seconds):
#
# RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
#
# You can also use resources to share common headers. For headers keys,
# symbols are converted to strings. Example:
#
@ -89,9 +93,10 @@ module RestClient
def timeout
options[:timeout]
:user => user,
:password => password,
:headers => headers, &b)
end
def open_timeout
options[:open_timeout]
end
# Construct a subresource, preserving authentication.

View file

@ -0,0 +1,20 @@
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
def initialize(string, net_http_res)
@net_http_res = net_http_res
super(string || "")
end
end
end

View file

@ -1,18 +1,70 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = "rest-client"
s.version = "0.8.2"
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
s.author = "Adam Wiggins"
s.email = "adam@heroku.com"
s.rubyforge_project = "rest-client"
s.homepage = "http://rest-client.heroku.com/"
s.has_rdoc = true
s.platform = Gem::Platform::RUBY
s.files = %w(Rakefile README.rdoc rest-client.gemspec
lib/request_errors.rb lib/resource.rb lib/rest_client.rb
spec/base.rb spec/request_errors_spec.rb spec/resource_spec.rb spec/rest_client_spec.rb
bin/restclient)
s.executables = ['restclient']
s.require_path = "lib"
s.name = %q{rest-client}
s.version = "1.1.0"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Adam Wiggins"]
s.date = %q{2009-08-12}
s.default_executable = %q{restclient}
s.description = %q{A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.}
s.email = %q{adam@heroku.com}
s.executables = ["restclient"]
s.extra_rdoc_files = [
"README.rdoc"
]
s.files = [
"README.rdoc",
"Rakefile",
"VERSION",
"bin/restclient",
"lib/rest_client.rb",
"lib/restclient.rb",
"lib/restclient/exceptions.rb",
"lib/restclient/mixin/response.rb",
"lib/restclient/net_http_ext.rb",
"lib/restclient/payload.rb",
"lib/restclient/raw_response.rb",
"lib/restclient/request.rb",
"lib/restclient/resource.rb",
"lib/restclient/response.rb",
"spec/base.rb",
"spec/exceptions_spec.rb",
"spec/master_shake.jpg",
"spec/mixin/response_spec.rb",
"spec/payload_spec.rb",
"spec/raw_response_spec.rb",
"spec/request_spec.rb",
"spec/resource_spec.rb",
"spec/response_spec.rb",
"spec/restclient_spec.rb"
]
s.homepage = %q{http://rest-client.heroku.com/}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{rest-client}
s.rubygems_version = %q{1.3.5}
s.summary = %q{Simple REST client for Ruby, inspired by microframework syntax for specifying actions.}
s.test_files = [
"spec/base.rb",
"spec/exceptions_spec.rb",
"spec/mixin/response_spec.rb",
"spec/payload_spec.rb",
"spec/raw_response_spec.rb",
"spec/request_spec.rb",
"spec/resource_spec.rb",
"spec/response_spec.rb",
"spec/restclient_spec.rb"
]
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 3
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
else
end
else
end
end

View file

@ -1,5 +1,4 @@
require 'rubygems'
require 'spec'
require File.dirname(__FILE__) + '/../lib/rest_client'
require File.dirname(__FILE__) + '/../lib/restclient'

View file

@ -12,6 +12,10 @@ describe RestClient::Exception do
end
describe RestClient::RequestFailed do
before do
@response = mock('HTTP Response', :code => '502')
end
it "stores the http response on the exception" do
begin
raise RestClient::RequestFailed, :response
@ -21,11 +25,18 @@ describe RestClient::RequestFailed do
end
it "http_code convenience method for fetching the code as an integer" do
RestClient::RequestFailed.new(mock('res', :code => '502')).http_code.should == 502
RestClient::RequestFailed.new(@response).http_code.should == 502
end
it "http_body convenience method for fetching the body (decoding when necessary)" do
@response.stub!(:[]).with('content-encoding').and_return('gzip')
@response.stub!(:body).and_return('compressed body')
RestClient::Request.should_receive(:decode).with('gzip', 'compressed body').and_return('plain body')
RestClient::RequestFailed.new(@response).http_body.should == 'plain body'
end
it "shows the status code in the message" do
RestClient::RequestFailed.new(mock('res', :code => '502')).to_s.should match(/502/)
RestClient::RequestFailed.new(@response).to_s.should match(/502/)
end
end

View file

@ -0,0 +1,46 @@
require File.dirname(__FILE__) + '/../base'
class MockResponse
include RestClient::Mixin::Response
def initialize(body, res)
@net_http_res = res
@body = @body
end
end
describe RestClient::Mixin::Response do
before do
@net_http_res = mock('net http response')
@response = MockResponse.new('abc', @net_http_res)
end
it "fetches the numeric response code" do
@net_http_res.should_receive(:code).and_return('200')
@response.code.should == 200
end
it "beautifies the headers by turning the keys to symbols" do
h = RestClient::Response.beautify_headers('content-type' => [ 'x' ])
h.keys.first.should == :content_type
end
it "beautifies the headers by turning the values to strings instead of one-element arrays" do
h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] )
h.values.first.should == 'text/html'
end
it "fetches the headers" do
@net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
@response.headers.should == { :content_type => 'text/html' }
end
it "extracts cookies from response headers" do
@net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
@response.cookies.should == { 'session_id' => '1' }
end
it "can access the net http result directly" do
@response.net_http_res.should == @net_http_res
end
end

View file

@ -36,7 +36,7 @@ EOS
m = RestClient::Payload::Multipart.new({:foo => f})
m.to_s.should == <<-EOS
--#{m.boundary}\r
Content-Disposition: multipart/form-data; name="foo"; filename="./spec/master_shake.jpg"\r
Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r
Content-Type: image/jpeg\r
\r
#{IO.read(f.path)}\r
@ -63,5 +63,10 @@ EOS
it "should return data if no of the above" do
RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base)
end
it "should recognize nested multipart payloads" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
RestClient::Payload.generate({"foo" => {"file" => f}}).should be_kind_of(RestClient::Payload::Multipart)
end
end
end

17
spec/raw_response_spec.rb Normal file
View file

@ -0,0 +1,17 @@
require File.dirname(__FILE__) + '/base'
describe RestClient::RawResponse do
before do
@tf = mock("Tempfile", :read => "the answer is 42", :open => true)
@net_http_res = mock('net http response')
@response = RestClient::RawResponse.new(@tf, @net_http_res)
end
it "behaves like string" do
@response.to_s.should == 'the answer is 42'
end
it "exposes a Tempfile" do
@response.file.should == @tf
end
end

476
spec/request_spec.rb Normal file
View file

@ -0,0 +1,476 @@
require File.dirname(__FILE__) + '/base'
describe RestClient::Request do
before do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
@uri = mock("uri")
@uri.stub!(:request_uri).and_return('/resource')
@uri.stub!(:host).and_return('some')
@uri.stub!(:port).and_return(80)
@net = mock("net::http base")
@http = mock("net::http connection")
Net::HTTP.stub!(:new).and_return(@net)
@net.stub!(:start).and_yield(@http)
@net.stub!(:use_ssl=)
@net.stub!(:verify_mode=)
end
it "accept */* mimetype, preferring xml" do
@request.default_headers[:accept].should == '*/*; q=0.5, application/xml'
end
it "decodes an uncompressed result body by passing it straight through" do
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 "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"
end
it "processes a successful result" do
res = mock("result")
res.stub!(:code).and_return("200")
res.stub!(:body).and_return('body')
res.stub!(:[]).with('content-encoding').and_return(nil)
@request.process_result(res).should == 'body'
end
it "doesn't classify successful requests as failed" do
203.upto(206) do |code|
res = mock("result")
res.stub!(:code).and_return(code.to_s)
res.stub!(:body).and_return("")
res.stub!(:[]).with('content-encoding').and_return(nil)
@request.process_result(res).should be_empty
end
end
it "parses a url into a URI object" do
URI.should_receive(:parse).with('http://example.com/resource')
@request.parse_url('http://example.com/resource')
end
it "adds http:// to the front of resources specified in the syntax example.com/resource" do
URI.should_receive(:parse).with('http://example.com/resource')
@request.parse_url('example.com/resource')
end
it "extracts the username and password when parsing http://user:password@example.com/" do
URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1'))
@request.parse_url_with_auth('http://joe:pass1@example.com/resource')
@request.user.should == 'joe'
@request.password.should == 'pass1'
end
it "doesn't overwrite user and password (which may have already been set by the Resource constructor) if there is no user/password in the url" do
URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
@request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2')
@request.parse_url_with_auth('http://example.com/resource')
@request.user.should == 'beth'
@request.password.should == 'pass2'
end
it "correctly formats cookies provided to the constructor" do
URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
@request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' })
@request.should_receive(:default_headers).and_return({'foo' => 'bar'})
headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'}
end
it "determines the Net::HTTP class to instantiate by the method name" do
@request.net_http_request_class(:put).should == Net::HTTP::Put
end
it "merges user headers with the default headers" do
@request.should_receive(:default_headers).and_return({ '1' => '2' })
@request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' }
end
it "prefers the user header when the same header exists in the defaults" do
@request.should_receive(:default_headers).and_return({ '1' => '2' })
@request.make_headers('1' => '3').should == { '1' => '3' }
end
it "converts header symbols from :content_type to 'Content-type'" do
@request.should_receive(:default_headers).and_return({})
@request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' }
end
it "converts header values to strings" do
@request.make_headers('A' => 1)['A'].should == '1'
end
it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
@request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
klass = mock("net:http class")
@request.should_receive(:net_http_request_class).with(:put).and_return(klass)
klass.should_receive(:new).and_return('result')
@request.should_receive(:transmit).with(@uri, 'result', 'payload')
@request.execute_inner
end
it "transmits the request with Net::HTTP" do
@http.should_receive(:request).with('req', 'payload')
@request.should_receive(:process_result)
@request.should_receive(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "uses SSL when the URI refers to a https address" do
@uri.stub!(:is_a?).with(URI::HTTPS).and_return(true)
@net.should_receive(:use_ssl=).with(true)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "sends nil payloads" do
@http.should_receive(:request).with('req', nil)
@request.should_receive(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', nil)
end
it "passes non-hash payloads straight through" do
@request.process_payload("x").should == "x"
end
it "converts a hash payload to urlencoded data" do
@request.process_payload(:a => 'b c+d').should == "a=b%20c%2Bd"
end
it "accepts nested hashes in payload" do
payload = @request.process_payload(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }})
payload.should include('user[name]=joe')
payload.should include('user[location][country]=USA')
payload.should include('user[location][state]=CA')
end
it "set urlencoded content_type header on hash payloads" do
@request.process_payload(:a => 1)
@request.headers[:content_type].should == 'application/x-www-form-urlencoded'
end
it "sets up the credentials prior to the request" do
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.stub!(:user).and_return('joe')
@request.stub!(:password).and_return('mypass')
@request.should_receive(:setup_credentials).with('req')
@request.transmit(@uri, 'req', nil)
end
it "does not attempt to send any credentials if user is nil" do
@request.stub!(:user).and_return(nil)
req = mock("request")
req.should_not_receive(:basic_auth)
@request.setup_credentials(req)
end
it "setup credentials when there's a user" do
@request.stub!(:user).and_return('joe')
@request.stub!(:password).and_return('mypass')
req = mock("request")
req.should_receive(:basic_auth).with('joe', 'mypass')
@request.setup_credentials(req)
end
it "catches EOFError and shows the more informative ServerBrokeConnection" do
@http.stub!(:request).and_raise(EOFError)
lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
end
it "execute calls execute_inner" do
@request.should_receive(:execute_inner)
@request.execute
end
it "class method execute wraps constructor" do
req = mock("rest request")
RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
req.should_receive(:execute)
RestClient::Request.execute(1 => 2)
end
it "raises a Redirect with the new location when the response is in the 30x range" do
res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' })
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'}
end
it "handles redirects with relative paths" do
res = mock('response', :code => '301', :header => { 'Location' => 'index' })
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
end
it "handles redirects with absolute paths" do
@request.instance_variable_set('@url', 'http://some/place/else')
res = mock('response', :code => '301', :header => { 'Location' => '/index' })
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
end
it "uses GET and clears payload when following 30x redirects" do
url = "http://example.com/redirected"
@request.should_receive(:execute_inner).once.ordered.and_raise(RestClient::Redirect.new(url))
@request.should_receive(:execute_inner).once.ordered do
@request.url.should == url
@request.method.should == :get
@request.payload.should be_nil
end
@request.execute
end
it "raises Unauthorized when the response is 401" do
res = mock('response', :code => '401')
lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
end
it "raises ResourceNotFound when the response is 404" do
res = mock('response', :code => '404')
lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
end
it "raises RequestFailed otherwise" do
res = mock('response', :code => '500')
lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
end
it "creates a proxy class if a proxy url is given" do
RestClient.stub!(:proxy).and_return("http://example.com/")
@request.net_http_class.should include(Net::HTTP::ProxyDelta)
end
it "creates a non-proxy class if a proxy url is not given" do
@request.net_http_class.should_not include(Net::HTTP::ProxyDelta)
end
it "logs a get request" do
RestClient::Request.new(:method => :get, :url => 'http://url').request_log.should ==
'RestClient.get "http://url"'
end
it "logs a post request with a small payload" do
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log.should ==
'RestClient.post "http://url", "foo"'
end
it "logs a post request with a large payload" do
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log.should ==
'RestClient.post "http://url", "(1000 byte payload)"'
end
it "logs input headers as a hash" do
RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).request_log.should ==
'RestClient.get "http://url", :accept=>"text/plain"'
end
it "logs a response including the status code, content type, and result body size in bytes" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
res.stub!(:[]).with('Content-type').and_return('text/html')
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
end
it "logs a response with a nil Content-type" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
res.stub!(:[]).with('Content-type').and_return(nil)
@request.response_log(res).should == "# => 200 OK | 4 bytes"
end
it "logs a response with a nil body" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => nil)
res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
@request.response_log(res).should == "# => 200 OK | text/html 0 bytes"
end
it "strips the charset from the response content type" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
end
it "displays the log to stdout" do
RestClient.stub!(:log).and_return('stdout')
STDOUT.should_receive(:puts).with('xyz')
@request.display_log('xyz')
end
it "displays the log to stderr" do
RestClient.stub!(:log).and_return('stderr')
STDERR.should_receive(:puts).with('xyz')
@request.display_log('xyz')
end
it "append the log to the requested filename" do
RestClient.stub!(:log).and_return('/tmp/restclient.log')
f = mock('file handle')
File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
f.should_receive(:puts).with('xyz')
@request.display_log('xyz')
end
it "set read_timeout" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@net.should_receive(:read_timeout=).with(123)
@request.transmit(@uri, 'req', nil)
end
it "set open_timeout" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@net.should_receive(:open_timeout=).with(123)
@request.transmit(@uri, 'req', nil)
end
it "should default to not verifying ssl certificates" do
@request.verify_ssl.should == false
end
it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do
@net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do
@request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true)
@net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do
mode = OpenSSL::SSL::VERIFY_PEER |
OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
@request = RestClient::Request.new( :method => :put,
:url => 'https://some/resource',
:payload => 'payload',
:verify_ssl => mode )
@net.should_receive(:verify_mode=).with(mode)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should default to not having an ssl_client_cert" do
@request.ssl_client_cert.should be(nil)
end
it "should set the ssl_client_cert if provided" do
@request = RestClient::Request.new(
:method => :put,
:url => 'https://some/resource',
:payload => 'payload',
:ssl_client_cert => "whatsupdoc!"
)
@net.should_receive(:cert=).with("whatsupdoc!")
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should not set the ssl_client_cert if it is not provided" do
@request = RestClient::Request.new(
:method => :put,
:url => 'https://some/resource',
:payload => 'payload'
)
@net.should_not_receive(:cert=).with("whatsupdoc!")
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should default to not having an ssl_client_key" do
@request.ssl_client_key.should be(nil)
end
it "should set the ssl_client_key if provided" do
@request = RestClient::Request.new(
:method => :put,
:url => 'https://some/resource',
:payload => 'payload',
:ssl_client_key => "whatsupdoc!"
)
@net.should_receive(:key=).with("whatsupdoc!")
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should not set the ssl_client_key if it is not provided" do
@request = RestClient::Request.new(
:method => :put,
:url => 'https://some/resource',
:payload => 'payload'
)
@net.should_not_receive(:key=).with("whatsupdoc!")
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should default to not having an ssl_ca_file" do
@request.ssl_ca_file.should be(nil)
end
it "should set the ssl_ca_file if provided" do
@request = RestClient::Request.new(
:method => :put,
:url => 'https://some/resource',
:payload => 'payload',
:ssl_ca_file => "Certificate Authority File"
)
@net.should_receive(:ca_file=).with("Certificate Authority File")
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
it "should not set the ssl_ca_file if it is not provided" do
@request = RestClient::Request.new(
:method => :put,
:url => 'https://some/resource',
:payload => 'payload'
)
@net.should_not_receive(:ca_file=).with("Certificate Authority File")
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', 'payload')
end
end

16
spec/response_spec.rb Normal file
View file

@ -0,0 +1,16 @@
require File.dirname(__FILE__) + '/base'
describe RestClient::Response do
before do
@net_http_res = mock('net http response')
@response = RestClient::Response.new('abc', @net_http_res)
end
it "behaves like string" do
@response.should == 'abc'
end
it "accepts nil strings and sets it to empty for the case of HEAD" do
RestClient::Response.new(nil, @net_http_res).should == ""
end
end

View file

@ -1,383 +0,0 @@
require File.dirname(__FILE__) + '/base'
def generate_payload(v)
RestClient::Payload::Base.new(v)
end
describe RestClient do
context "public API" do
it "GET" do
RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
RestClient.get('http://some/resource')
end
it "POST" do
RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.post('http://some/resource', 'payload')
end
it "PUT" do
RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.put('http://some/resource', 'payload')
end
it "DELETE" do
RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
RestClient.delete('http://some/resource')
end
end
context "logging" do
after do
RestClient.log = nil
end
it "gets the log source from the RESTCLIENT_LOG environment variable" do
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return('from env')
RestClient.log = 'from class method'
RestClient.log.should == 'from env'
end
it "sets a destination for log output, used if no environment variable is set" do
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
RestClient.log = 'from class method'
RestClient.log.should == 'from class method'
end
it "returns nil (no logging) if neither are set (default)" do
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
RestClient.log.should == nil
end
end
context RestClient::Request do
before do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
@uri = mock("uri")
@uri.stub!(:request_uri).and_return('/resource')
@uri.stub!(:host).and_return('some')
@uri.stub!(:port).and_return(80)
@net = mock("net::http base")
@http = mock("net::http connection")
Net::HTTP.stub!(:new).and_return(@net)
@net.stub!(:start).and_yield(@http)
@net.stub!(:use_ssl=)
@net.stub!(:verify_mode=)
end
it "requests xml mimetype" do
@request.default_headers[:accept].should == 'application/xml'
end
it "decodes an uncompressed result body by passing it straight through" do
@request.decode(nil, 'xyz').should == 'xyz'
end
it "decodes a gzip body" do
@request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000").should == "i'm gziped\n"
end
it "decodes a deflated body" do
@request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should == "some deflated text"
end
it "processes a successful result" do
res = mock("result")
res.stub!(:code).and_return("200")
res.stub!(:body).and_return('body')
res.stub!(:[]).with('content-encoding').and_return(nil)
@request.process_result(res).should == 'body'
end
it "parses a url into a URI object" do
URI.should_receive(:parse).with('http://example.com/resource')
@request.parse_url('http://example.com/resource')
end
it "adds http:// to the front of resources specified in the syntax example.com/resource" do
URI.should_receive(:parse).with('http://example.com/resource')
@request.parse_url('example.com/resource')
end
it "extracts the username and password when parsing http://user:password@example.com/" do
URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1'))
@request.parse_url_with_auth('http://joe:pass1@example.com/resource')
@request.user.should == 'joe'
@request.password.should == 'pass1'
end
it "doesn't overwrite user and password (which may have already been set by the Resource constructor) if there is no user/password in the url" do
URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
@request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2')
@request.parse_url_with_auth('http://example.com/resource')
@request.user.should == 'beth'
@request.password.should == 'pass2'
end
it "determines the Net::HTTP class to instantiate by the method name" do
@request.net_http_request_class(:put).should == Net::HTTP::Put
end
it "merges user headers with the default headers" do
@request.should_receive(:default_headers).and_return({ '1' => '2' })
@request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' }
end
it "prefers the user header when the same header exists in the defaults" do
@request.should_receive(:default_headers).and_return({ '1' => '2' })
@request.make_headers('1' => '3').should == { '1' => '3' }
end
it "converts header symbols from :content_type to 'Content-type'" do
@request.should_receive(:default_headers).and_return({})
@request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' }
end
it "converts header values to strings" do
@request.make_headers('A' => 1)['A'].should == '1'
end
it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
@request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
klass = mock("net:http class")
@request.should_receive(:net_http_request_class).with(:put).and_return(klass)
klass.should_receive(:new).and_return('result')
@request.should_receive(:transmit).with(@uri, 'result', be_kind_of(RestClient::Payload::Base))
@request.execute_inner
end
it "transmits the request with Net::HTTP" do
@http.should_receive(:request).with('req', be_kind_of(RestClient::Payload::Base))
@request.should_receive(:process_result)
@request.should_receive(:response_log)
@request.transmit(@uri, 'req', generate_payload('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', generate_payload('payload'))
end
it "sends nil payloads" do
@http.should_receive(:request).with('req', nil)
@request.should_receive(:process_result)
@request.stub!(:response_log)
@request.transmit(@uri, 'req', nil)
end
it "passes non-hash payloads straight through" do
@request.process_payload("x").should == "x"
end
it "converts a hash payload to urlencoded data" do
@request.process_payload(:a => 'b c+d').should == "a=b%20c%2Bd"
end
it "accepts nested hashes in payload" do
payload = @request.process_payload(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }})
payload.should include('user[name]=joe')
payload.should include('user[location][country]=USA')
payload.should include('user[location][state]=CA')
end
it "set urlencoded content_type header on hash payloads" do
@request.process_payload(:a => 1)
@request.headers[:content_type].should == 'application/x-www-form-urlencoded'
end
it "sets up the credentials prior to the request" do
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@request.stub!(:user).and_return('joe')
@request.stub!(:password).and_return('mypass')
@request.should_receive(:setup_credentials).with('req')
@request.transmit(@uri, 'req', generate_payload(''))
end
it "does not attempt to send any credentials if user is nil" do
@request.stub!(:user).and_return(nil)
req = mock("request")
req.should_not_receive(:basic_auth)
@request.setup_credentials(req)
end
it "setup credentials when there's a user" do
@request.stub!(:user).and_return('joe')
@request.stub!(:password).and_return('mypass')
req = mock("request")
req.should_receive(:basic_auth).with('joe', 'mypass')
@request.setup_credentials(req)
end
it "catches EOFError and shows the more informative ServerBrokeConnection" do
@http.stub!(:request).and_raise(EOFError)
lambda { @request.transmit(@uri, 'req', generate_payload('')) }.
should raise_error(RestClient::ServerBrokeConnection)
end
it "execute calls execute_inner" do
@request.should_receive(:execute_inner)
@request.execute
end
it "class method execute wraps constructor" do
req = mock("rest request")
RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
req.should_receive(:execute)
RestClient::Request.execute(1 => 2)
end
it "raises a Redirect with the new location when the response is in the 30x range" do
res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' })
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'}
end
it "handles redirects with relative paths" do
res = mock('response', :code => '301', :header => { 'Location' => 'index' })
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
end
it "handles redirects with absolute paths" do
@request.instance_variable_set('@url', 'http://some/place/else')
res = mock('response', :code => '301', :header => { 'Location' => '/index' })
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
end
it "raises Unauthorized when the response is 401" do
res = mock('response', :code => '401')
lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
end
it "raises ResourceNotFound when the response is 404" do
res = mock('response', :code => '404')
lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
end
it "raises RequestFailed otherwise" do
res = mock('response', :code => '500')
lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
end
it "creates a proxy class if a proxy url is given" do
RestClient.stub!(:proxy).and_return("http://example.com/")
@request.net_http_class.should include(Net::HTTP::ProxyDelta)
end
it "creates a non-proxy class if a proxy url is not given" do
@request.net_http_class.should_not include(Net::HTTP::ProxyDelta)
end
it "logs a get request" do
RestClient::Request.new(:method => :get, :url => 'http://url').request_log.should ==
'RestClient.get "http://url"'
end
it "logs a post request with a small payload" do
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log.should ==
'RestClient.post "http://url", "foo"'
end
it "logs a post request with a large payload" do
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log.should ==
'RestClient.post "http://url", "(1000 byte payload)"'
end
it "logs input headers as a hash" do
RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).request_log.should ==
'RestClient.get "http://url", :accept=>"text/plain"'
end
it "logs a response including the status code, content type, and result body size in bytes" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
res.stub!(:[]).with('Content-type').and_return('text/html')
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
end
it "logs a response with a nil Content-type" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
res.stub!(:[]).with('Content-type').and_return(nil)
@request.response_log(res).should == "# => 200 OK | 4 bytes"
end
it "strips the charset from the response content type" do
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
end
it "displays the log to stdout" do
RestClient.stub!(:log).and_return('stdout')
STDOUT.should_receive(:puts).with('xyz')
@request.display_log('xyz')
end
it "displays the log to stderr" do
RestClient.stub!(:log).and_return('stderr')
STDERR.should_receive(:puts).with('xyz')
@request.display_log('xyz')
end
it "append the log to the requested filename" do
RestClient.stub!(:log).and_return('/tmp/restclient.log')
f = mock('file handle')
File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
f.should_receive(:puts).with('xyz')
@request.display_log('xyz')
end
it "set read_timeout" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
@http.stub!(:request)
@request.stub!(:process_result)
@request.stub!(:response_log)
@http.should_receive(:read_timeout=).with(123)
@request.transmit(@uri, 'req', nil)
end
end
context RestClient::Response do
before do
@net_http_res = mock('net http response')
@response = RestClient::Response.new('abc', @net_http_res)
end
it "behaves like string" do
@response.should == 'abc'
end
it "fetches the numeric response code" do
@net_http_res.should_receive(:code).and_return('200')
@response.code.should == 200
end
it "beautifies the headers by turning the keys to symbols" do
h = RestClient::Response.beautify_headers('content-type' => [ 'x' ])
h.keys.first.should == :content_type
end
it "beautifies the headers by turning the values to strings instead of one-element arrays" do
h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] )
h.values.first.should == 'text/html'
end
it "fetches the headers" do
@net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
@response.headers.should == { :content_type => 'text/html' }
end
it "can access the net http result directly" do
@response.net_http_res.should == @net_http_res
end
end
end

53
spec/restclient_spec.rb Normal file
View file

@ -0,0 +1,53 @@
require File.dirname(__FILE__) + '/base'
describe RestClient do
describe "API" do
it "GET" do
RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
RestClient.get('http://some/resource')
end
it "POST" do
RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.post('http://some/resource', 'payload')
end
it "PUT" do
RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.put('http://some/resource', 'payload')
end
it "DELETE" do
RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
RestClient.delete('http://some/resource')
end
it "HEAD" do
RestClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {})
RestClient.head('http://some/resource')
end
end
describe "logging" do
after do
RestClient.log = nil
end
it "gets the log source from the RESTCLIENT_LOG environment variable" do
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return('from env')
RestClient.log = 'from class method'
RestClient.log.should == 'from env'
end
it "sets a destination for log output, used if no environment variable is set" do
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
RestClient.log = 'from class method'
RestClient.log.should == 'from class method'
end
it "returns nil (no logging) if neither are set (default)" do
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
RestClient.log.should == nil
end
end
end