From 873943ed6f7de457ab2ec196b1f98dfccc05f75b Mon Sep 17 00:00:00 2001 From: Chris Anderson Date: Wed, 19 Mar 2008 09:09:19 -0700 Subject: [PATCH] the rest client dependency --- deps/rest-client | 1 - deps/rest-client/README | 47 ++++++++ deps/rest-client/Rakefile | 82 +++++++++++++ deps/rest-client/lib/resource.rb | 58 +++++++++ deps/rest-client/lib/rest_client.rb | 122 +++++++++++++++++++ deps/rest-client/spec/base.rb | 4 + deps/rest-client/spec/resource_spec.rb | 31 +++++ deps/rest-client/spec/rest_client_spec.rb | 141 ++++++++++++++++++++++ 8 files changed, 485 insertions(+), 1 deletion(-) delete mode 160000 deps/rest-client create mode 100644 deps/rest-client/README create mode 100644 deps/rest-client/Rakefile create mode 100644 deps/rest-client/lib/resource.rb create mode 100644 deps/rest-client/lib/rest_client.rb create mode 100644 deps/rest-client/spec/base.rb create mode 100644 deps/rest-client/spec/resource_spec.rb create mode 100644 deps/rest-client/spec/rest_client_spec.rb diff --git a/deps/rest-client b/deps/rest-client deleted file mode 160000 index d484781..0000000 --- a/deps/rest-client +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d484781d75ff5f986412103f9f68b6be6fc223fc diff --git a/deps/rest-client/README b/deps/rest-client/README new file mode 100644 index 0000000..3942c12 --- /dev/null +++ b/deps/rest-client/README @@ -0,0 +1,47 @@ += REST Client -- simple DSL for accessing REST resources + +A simple REST client for Ruby, inspired by the microframework (Camping, +Sinatra...) style of specifying actions: get, put, post, delete. + +== Usage: Raw URL + + require 'rest_client' + + xml = RestClient.get 'http://some/resource' + jpg = RestClient.get 'http://some/resource', :accept => 'image/jpg' + + RestClient.put 'http://some/resource', File.read('my.pdf'), :content_type => 'application/pdf' + + RestClient.post 'http://some/resource', xml, :content_type => 'application/xml' + + RestClient.delete 'http://some/resource' + +See RestClient module docs for details. + +== Usage: ActiveResource-Style + + resource = RestClient::Resource.new 'http://some/resource' + resource.get + + protected_resource = RestClient::Resource.new 'http://protected/resource', 'user', 'pass' + protected_resource.put File.read('pic.jpg'), :content_type => 'image/jpg' + +See RestClient::Resource module docs for details. + +== Shell + +Require rest_client from within irb to access RestClient interactively, like +using curl at the command line. Better yet, require gem from within your +~/.rush/env.rb and have instant access to it from within your rush +(http://rush.heroku.com) sessions. + +== Meta + +Written by Adam Wiggins (adam at heroku dot com) + +Released under the MIT License: http://www.opensource.org/licenses/mit-license.php + +http://rest-client.heroku.com + +http://github.com/adamwiggins/rest-client + diff --git a/deps/rest-client/Rakefile b/deps/rest-client/Rakefile new file mode 100644 index 0000000..e79818d --- /dev/null +++ b/deps/rest-client/Rakefile @@ -0,0 +1,82 @@ +require 'rake' +require 'spec/rake/spectask' + +desc "Run all specs" +Spec::Rake::SpecTask.new('spec') do |t| + t.spec_files = FileList['spec/*_spec.rb'] +end + +desc "Print specdocs" +Spec::Rake::SpecTask.new(:doc) do |t| + t.spec_opts = ["--format", "specdoc", "--dry-run"] + t.spec_files = FileList['spec/*_spec.rb'] +end + +desc "Run all examples with RCov" +Spec::Rake::SpecTask.new('rcov') do |t| + t.spec_files = FileList['spec/*_spec.rb'] + t.rcov = true + t.rcov_opts = ['--exclude', 'examples'] +end + +task :default => :spec + +###################################################### + +require 'rake' +require 'rake/testtask' +require 'rake/clean' +require 'rake/gempackagetask' +require 'rake/rdoctask' +require 'fileutils' + +version = "0.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 microframework (Camping, Sinatra...) 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.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' + t.title = "rest-client, fetch RESTful resources effortlessly" + t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + t.options << '--charset' << 'utf-8' + t.rdoc_files.include('README') + t.rdoc_files.include('lib/*.rb') +end + +CLEAN.include [ 'pkg', '*.gem', '.config' ] + diff --git a/deps/rest-client/lib/resource.rb b/deps/rest-client/lib/resource.rb new file mode 100644 index 0000000..1991dbe --- /dev/null +++ b/deps/rest-client/lib/resource.rb @@ -0,0 +1,58 @@ +module RestClient + # A class that can be instantiated for access to a RESTful resource, + # including authentication. + # + # Example: + # + # resource = RestClient::Resource.new('http://some/resource') + # jpg = resource.get(:accept => 'image/jpg') + # + # With HTTP basic authentication: + # + # resource = RestClient::Resource.new('http://protected/resource', 'user', 'pass') + # resource.delete + # + class Resource + attr_reader :url, :user, :password + + def initialize(url, user=nil, password=nil) + @url = url + @user = user + @password = password + end + + def get(headers={}) + Request.execute(:method => :get, + :url => url, + :user => user, + :password => password, + :headers => headers) + end + + def post(payload, headers={}) + Request.execute(:method => :post, + :url => url, + :payload => payload, + :user => user, + :password => password, + :headers => headers) + end + + def put(payload, headers={}) + Request.execute(:method => :put, + :url => url, + :payload => payload, + :user => user, + :password => password, + :headers => headers) + end + + def delete(headers={}) + Request.execute(:method => :delete, + :url => url, + :user => user, + :password => password, + :headers => headers) + end + end +end diff --git a/deps/rest-client/lib/rest_client.rb b/deps/rest-client/lib/rest_client.rb new file mode 100644 index 0000000..6e8ff8f --- /dev/null +++ b/deps/rest-client/lib/rest_client.rb @@ -0,0 +1,122 @@ +require 'uri' +require 'net/http' + +require File.dirname(__FILE__) + '/resource' + +# This module's static methods are the entry point for using the REST client. +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 + + # Internal class used to build and execute the request. + class Request + attr_reader :method, :url, :payload, :headers, :user, :password + + 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" + @payload = args[:payload] + @headers = args[:headers] || {} + @user = args[:user] + @password = args[:password] + end + + def execute + execute_inner + rescue Redirect => e + @url = e.message + execute + end + + def execute_inner + uri = parse_url(url) + transmit uri, net_http_class(method).new(uri.path, make_headers(headers)), payload + end + + def make_headers(user_headers) + final = {} + merged = default_headers.merge(user_headers) + merged.keys.each do |key| + final[key.to_s.gsub(/_/, '-').capitalize] = merged[key] + end + final + end + + def net_http_class(method) + Object.module_eval "Net::HTTP::#{method.to_s.capitalize}" + end + + def parse_url(url) + url = "http://#{url}" unless url.match(/^http/) + URI.parse(url) + end + + # A redirect was encountered; caught by execute to retry with the new url. + class Redirect < Exception; end + + # Request failed with an unhandled http error code. + class RequestFailed < Exception; end + + # Authorization is required to access the resource specified. + class Unauthorized < Exception; end + + def transmit(uri, req, payload) + setup_credentials(req) + + Net::HTTP.start(uri.host, uri.port) do |http| + process_result http.request(req, payload || "") + end + end + + def setup_credentials(req) + req.basic_auth(user, password) if user + end + + def process_result(res) + if %w(200 201 202).include? res.code + res.body + elsif %w(301 302 303).include? res.code + raise Redirect, res.header['Location'] + elsif res.code == "401" + raise Unauthorized + else + raise RequestFailed, error_message(res) + end + end + + def error_message(res) + "HTTP code #{res.code}: #{res.body}" + end + + def default_headers + { :accept => 'application/xml' } + end + end +end diff --git a/deps/rest-client/spec/base.rb b/deps/rest-client/spec/base.rb new file mode 100644 index 0000000..192612c --- /dev/null +++ b/deps/rest-client/spec/base.rb @@ -0,0 +1,4 @@ +require 'rubygems' +require 'spec' + +require File.dirname(__FILE__) + '/../lib/rest_client' diff --git a/deps/rest-client/spec/resource_spec.rb b/deps/rest-client/spec/resource_spec.rb new file mode 100644 index 0000000..4c5c795 --- /dev/null +++ b/deps/rest-client/spec/resource_spec.rb @@ -0,0 +1,31 @@ +require File.dirname(__FILE__) + '/base' + +describe RestClient::Resource do + before do + @resource = RestClient::Resource.new('http://some/resource', 'jane', 'mypass') + end + + it "GET" do + RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}, :user => 'jane', :password => 'mypass') + @resource.get + end + + it "POST" do + RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => { :content_type => 'image/jpg' }, :user => 'jane', :password => 'mypass') + @resource.post 'abc', :content_type => 'image/jpg' + end + + it "PUT" do + RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => { :content_type => 'image/jpg' }, :user => 'jane', :password => 'mypass') + @resource.put 'abc', :content_type => 'image/jpg' + end + + it "DELETE" do + RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}, :user => 'jane', :password => 'mypass') + @resource.delete + end + + it "can instantiate with no user/password" do + @resource = RestClient::Resource.new('http://some/resource') + end +end diff --git a/deps/rest-client/spec/rest_client_spec.rb b/deps/rest-client/spec/rest_client_spec.rb new file mode 100644 index 0000000..7980beb --- /dev/null +++ b/deps/rest-client/spec/rest_client_spec.rb @@ -0,0 +1,141 @@ +require File.dirname(__FILE__) + '/base' + +describe RestClient do + context "public API" do + it "GET" do + RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}) + RestClient.get('http://some/resource') + end + + it "POST" do + RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {}) + RestClient.post('http://some/resource', 'payload') + end + + it "PUT" do + RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {}) + RestClient.put('http://some/resource', 'payload') + end + + it "DELETE" do + RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {}) + RestClient.delete('http://some/resource') + end + end + + context RestClient::Request do + before do + @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload') + + @uri = mock("uri") + @uri.stub!(:path).and_return('/resource') + @uri.stub!(:host).and_return('some') + @uri.stub!(:port).and_return(80) + end + + it "requests xml mimetype" do + @request.default_headers[:accept].should == 'application/xml' + end + + it "processes a successful result" do + res = mock("result") + res.stub!(:code).and_return("200") + res.stub!(:body).and_return('body') + @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 "determines the Net::HTTP class to instantiate by the method name" do + @request.net_http_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 "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do + @request.should_receive(:parse_url).with('http://some/resource').and_return(@uri) + klass = mock("net:http class") + @request.should_receive(:net_http_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 = mock("net::http connection") + Net::HTTP.should_receive(:start).and_yield(http) + http.should_receive(:request).with('req', 'payload') + @request.should_receive(:process_result) + @request.transmit(@uri, 'req', 'payload') + end + + it "doesn't send nil payloads" do + http = mock("net::http connection") + Net::HTTP.should_receive(:start).and_yield(http) + http.should_receive(:request).with('req', '') + @request.should_receive(:process_result) + @request.transmit(@uri, 'req', nil) + end + + it "sets up the credentials prior to the request" do + http = mock("net::http connection") + Net::HTTP.should_receive(:start).and_yield(http) + http.stub!(:request) + @request.stub!(:process_result) + + @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 "does not attempt to send any credentials if user is nil" 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 "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 + end +end