From 75cb7f218f8be9754c1a40b106dcde86c711670f Mon Sep 17 00:00:00 2001 From: bmizerany Date: Wed, 23 Jul 2008 12:12:29 -0700 Subject: [PATCH 01/10] Minor organizational change to separate the core from helpers --- lib/rest_client.rb | 4 ++-- lib/{ => rest_client}/request_errors.rb | 0 lib/{ => rest_client}/resource.rb | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename lib/{ => rest_client}/request_errors.rb (100%) rename lib/{ => rest_client}/resource.rb (100%) diff --git a/lib/rest_client.rb b/lib/rest_client.rb index a1e2d3b..592221c 100644 --- a/lib/rest_client.rb +++ b/lib/rest_client.rb @@ -1,8 +1,8 @@ require 'uri' require 'net/https' -require File.dirname(__FILE__) + '/resource' -require File.dirname(__FILE__) + '/request_errors' +require File.dirname(__FILE__) + '/rest_client/resource' +require File.dirname(__FILE__) + '/rest_client/request_errors' # This module's static methods are the entry point for using the REST client. # diff --git a/lib/request_errors.rb b/lib/rest_client/request_errors.rb similarity index 100% rename from lib/request_errors.rb rename to lib/rest_client/request_errors.rb diff --git a/lib/resource.rb b/lib/rest_client/resource.rb similarity index 100% rename from lib/resource.rb rename to lib/rest_client/resource.rb From 6815ae2496a59cd75e7b9c1987535bd16930f38b Mon Sep 17 00:00:00 2001 From: bmizerany Date: Wed, 23 Jul 2008 13:16:05 -0700 Subject: [PATCH 02/10] Multipart with Streaming --- README => README.rdoc | 17 ++- Rakefile | 2 +- lib/rest_client.rb | 12 ++- lib/rest_client/net_http_ext.rb | 21 ++++ lib/rest_client/payload.rb | 183 ++++++++++++++++++++++++++++++++ rest-client.gemspec | 2 +- spec/master_shake.jpg | Bin 0 -> 22545 bytes spec/payload_spec.rb | 78 ++++++++++++++ spec/rest_client_spec.rb | 2 +- 9 files changed, 310 insertions(+), 7 deletions(-) rename README => README.rdoc (79%) create mode 100644 lib/rest_client/net_http_ext.rb create mode 100644 lib/rest_client/payload.rb create mode 100644 spec/master_shake.jpg create mode 100644 spec/payload_spec.rb diff --git a/README b/README.rdoc similarity index 79% rename from README rename to README.rdoc index 3b288f0..ae550f4 100644 --- a/README +++ b/README.rdoc @@ -13,8 +13,23 @@ of specifying actions: get, put, post, delete. RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' } RestClient.delete 'http://example.com/resource' + +== Multipart -See RestClient module docs for details. +Yeah, that's right! This does multipart sends for you! + + RestClient.post '/data', :myfile => File.new("/path/to/image.jpg") + +This does two things for you: + +* Auto-detects that you have a File value sends it as multipart +* Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry + +If you are sending params that do not contain a File object but the payload needs to be multipart then: + + RestClient.post '/data', :foo => 'bar', :multipart => true + +See RestClient module docs for more details. == Usage: ActiveResource-Style diff --git a/Rakefile b/Rakefile index e69cfc4..c1b8311 100644 --- a/Rakefile +++ b/Rakefile @@ -76,7 +76,7 @@ Rake::RDocTask.new do |t| t.title = "rest-client, fetch RESTful resources effortlessly" t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' t.options << '--charset' << 'utf-8' - t.rdoc_files.include('README') + t.rdoc_files.include('README.rdoc') t.rdoc_files.include('lib/*.rb') end diff --git a/lib/rest_client.rb b/lib/rest_client.rb index 592221c..0c103ef 100644 --- a/lib/rest_client.rb +++ b/lib/rest_client.rb @@ -3,6 +3,8 @@ require 'net/https' 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. # @@ -61,7 +63,7 @@ module RestClient # Internal class used to build and execute the request. class Request - attr_reader :method, :url, :payload, :headers, :user, :password + attr_reader :method, :url, :headers, :user, :password def self.execute(args) new(args).execute @@ -70,8 +72,8 @@ module RestClient 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] || {} - @payload = process_payload(args[:payload]) @user = args[:user] @password = args[:password] end @@ -170,9 +172,13 @@ module RestClient raise RequestFailed, res end end + + def payload + @payload + end def default_headers - { :accept => 'application/xml' } + @payload.headers.merge({ :accept => 'application/xml' }) end end end diff --git a/lib/rest_client/net_http_ext.rb b/lib/rest_client/net_http_ext.rb new file mode 100644 index 0000000..5499b1b --- /dev/null +++ b/lib/rest_client/net_http_ext.rb @@ -0,0 +1,21 @@ +# +# Replace the request method in Net::HTTP to sniff the body type +# and set the stream if appropriate +# +# Taken from: +# http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/ + +module Net + class HTTP + alias __request__ request + + def request(req, body = nil, &block) + if body != nil && body.respond_to?(:read) + req.body_stream = body + return __request__(req, nil, &block) + else + return __request__(req, body, &block) + end + end + end +end diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb new file mode 100644 index 0000000..d1f0d2d --- /dev/null +++ b/lib/rest_client/payload.rb @@ -0,0 +1,183 @@ +require "tempfile" + +module RestClient + + module Payload + extend self + + class NotImplemented < RuntimeError; end + + 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) } + Multipart.new(params) + else + UrlEncoded.new(params) + end + end + + class Base + + def initialize(params) + build_stream(params) + end + + def build_stream(params) + @stream = StringIO.new(params) + @stream.seek(0) + end + + def read(bytes=nil) + @stream.read(bytes) + end + alias :to_s :read + + def escape(v) + URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) + end + + def headers + {'Content-Length' => size.to_s} + end + + def size + @stream.size + end + alias :length :size + + + end + + class UrlEncoded < Base + + def build_stream(params) + @stream = StringIO.new(params.map do |k,v| + "#{escape(k)}=#{escape(v)}" + end.join("&")) + @stream.seek(0) + end + + def headers + super.merge({'Content-Type' => 'application/x-www-form-urlencoded'}) + end + + end + + class Multipart < Base + + EOL = "\r\n" + + def build_stream(params) + b = "--#{boundary}" + + @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}") + @stream.write(b + EOL) + params.each do |k,v| + if v.respond_to?(:read) && v.respond_to?(:path) + create_file_field(@stream, k,v) + else + create_regular_field(@stream, k,v) + end + @stream.write(b + EOL) + end + @stream.write('--') + @stream.seek(0) + end + + def create_regular_field(s, k, v) + s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"") + s.write(EOL) + s.write(EOL) + s.write(v) + end + + def create_file_field(s, k, v) + s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}") + s.write("Content-Type: #{mime_for(v.path)}#{EOL}") + s.write(EOL) + while data = v.read(8124) + s.write(data) + end + end + + def mime_for(path) + ext = File.extname(path)[1..-1] + MIME_TYPES[ext] || 'text/plain' + end + + def boundary + @boundary ||= rand(1_000_000).to_s + end + + def headers + super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}}) + end + + end + + # :stopdoc: + # From WEBrick. + MIME_TYPES = { + "ai" => "application/postscript", + "asc" => "text/plain", + "avi" => "video/x-msvideo", + "bin" => "application/octet-stream", + "bmp" => "image/bmp", + "class" => "application/octet-stream", + "cer" => "application/pkix-cert", + "crl" => "application/pkix-crl", + "crt" => "application/x-x509-ca-cert", + #"crl" => "application/x-pkcs7-crl", + "css" => "text/css", + "dms" => "application/octet-stream", + "doc" => "application/msword", + "dvi" => "application/x-dvi", + "eps" => "application/postscript", + "etx" => "text/x-setext", + "exe" => "application/octet-stream", + "gif" => "image/gif", + "htm" => "text/html", + "html" => "text/html", + "jpe" => "image/jpeg", + "jpeg" => "image/jpeg", + "jpg" => "image/jpeg", + "js" => "text/javascript", + "lha" => "application/octet-stream", + "lzh" => "application/octet-stream", + "mov" => "video/quicktime", + "mpe" => "video/mpeg", + "mpeg" => "video/mpeg", + "mpg" => "video/mpeg", + "pbm" => "image/x-portable-bitmap", + "pdf" => "application/pdf", + "pgm" => "image/x-portable-graymap", + "png" => "image/png", + "pnm" => "image/x-portable-anymap", + "ppm" => "image/x-portable-pixmap", + "ppt" => "application/vnd.ms-powerpoint", + "ps" => "application/postscript", + "qt" => "video/quicktime", + "ras" => "image/x-cmu-raster", + "rb" => "text/plain", + "rd" => "text/plain", + "rtf" => "application/rtf", + "sgm" => "text/sgml", + "sgml" => "text/sgml", + "tif" => "image/tiff", + "tiff" => "image/tiff", + "txt" => "text/plain", + "xbm" => "image/x-xbitmap", + "xls" => "application/vnd.ms-excel", + "xml" => "text/xml", + "xpm" => "image/x-xpixmap", + "xwd" => "image/x-xwindowdump", + "zip" => "application/zip", + } + # :startdoc: + + + end + +end diff --git a/rest-client.gemspec b/rest-client.gemspec index 1a255bf..e1e8675 100644 --- a/rest-client.gemspec +++ b/rest-client.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.homepage = "http://rest-client.heroku.com/" s.has_rdoc = true s.platform = Gem::Platform::RUBY - s.files = %w(Rakefile README rest-client.gemspec + 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) diff --git a/spec/master_shake.jpg b/spec/master_shake.jpg new file mode 100644 index 0000000000000000000000000000000000000000..48169a3a611e297fb290d5a204f7be7e93335c47 GIT binary patch literal 22545 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 WFYK^=LGVFTcDX)rK|HbOfB)Ic0vIp= literal 0 HcmV?d00001 diff --git a/spec/payload_spec.rb b/spec/payload_spec.rb new file mode 100644 index 0000000..5bf96ea --- /dev/null +++ b/spec/payload_spec.rb @@ -0,0 +1,78 @@ +require File.dirname(__FILE__) + "/base" + +context "A regular Payload" do + + specify "should should default content-type to standard enctype" do + RestClient::Payload::UrlEncoded.new({}).headers['Content-Type']. + should == 'application/x-www-form-urlencoded' + end + + specify "should form properly encoded params" do + RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s. + should == "foo=bar" + end + +end + +context "A multipart Payload" do + specify "should should default content-type to standard enctype" do + m = RestClient::Payload::Multipart.new({}) + m.stub!(:boundary).and_return(123) + m.headers['Content-Type'].should == 'multipart/form-data; boundary="123"' + end + + xspecify "should form properly seperated multipart data" do + m = RestClient::Payload::Multipart.new({:foo => "bar"}) + m.stub!(:boundary).and_return("123") + m.to_s.should == <<-EOS +--123\r +Content-Disposition: multipart/form-data; name="foo"\r +\r +bar\r +--123--\r +EOS + end + + xspecify "should form properly seperated multipart data" do + f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") + # f = mock("master_shake.jpg") + # f.stub!(:path).and_return("master_shake.jpg") + # f.stub!(:read).and_return("datadatadata") + m = RestClient::Payload::Multipart.new({:foo => f}) + m.stub!(:boundary).and_return("123") + m.to_s.should == <<-EOS +--123\r +Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r +Content-Type: image/jpeg\r +\r +datadatadata\r +--123--\r +EOS + end + +end + +context "Payload generation" do + + specify "should recognize standard urlencoded params" do + RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded) + end + + specify "should recognize multipart params" do + # f = mock("master_shake.jpg") + # f.stub!(:path) + # f.stub!(:read) + f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") + + RestClient::Payload.generate({"foo" => f}).should be_kind_of(RestClient::Payload::Multipart) + end + + specify "should be multipart if forced" do + RestClient::Payload.generate({"foo" => "bar", :multipart => true}).should be_kind_of(RestClient::Payload::Multipart) + end + + specify "should return data if no of the above" do + RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base) + end + +end diff --git a/spec/rest_client_spec.rb b/spec/rest_client_spec.rb index 754f5f6..f875c20 100644 --- a/spec/rest_client_spec.rb +++ b/spec/rest_client_spec.rb @@ -99,7 +99,7 @@ describe RestClient do 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.should_receive(:transmit).with(@uri, 'result', be_kind_of(RestClient::Payload::Base)) @request.execute_inner end From 0fc988ae2183175ffdc718b87a2435fe3f51a8a8 Mon Sep 17 00:00:00 2001 From: bmizerany Date: Thu, 24 Jul 2008 19:18:12 -0700 Subject: [PATCH 03/10] Downstream streaming This change allows one to pass a block to a request (i.e. get/post/put/..) and get a hold of the Net::HTTPResponse object. RestClient.get("http://something.really.big.com/") do |res| res.read_body do |chunk| .. whatcha gonna do with all that chunk.. all that chunk .. end end The get/post/head will return nil when a block is passed. This is to keep the data out of memory. Enjoy. --- README.rdoc | 8 ++++++++ lib/rest_client.rb | 41 ++++++++++++++++++++----------------- lib/rest_client/payload.rb | 1 + lib/rest_client/resource.rb | 16 +++++++-------- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/README.rdoc b/README.rdoc index ae550f4..04c3e66 100644 --- a/README.rdoc +++ b/README.rdoc @@ -29,6 +29,14 @@ If you are sending params that do not contain a File object but the payload need RestClient.post '/data', :foo => 'bar', :multipart => true +== Streaming downloads + +RestClient.get('http://some/resource/lotsofdata') do |res| + res.read_body do |chunk| + .. do something with chunk .. + end +end + See RestClient module docs for more details. == Usage: ActiveResource-Style diff --git a/lib/rest_client.rb b/lib/rest_client.rb index 0c103ef..bf4c702 100644 --- a/lib/rest_client.rb +++ b/lib/rest_client.rb @@ -35,38 +35,38 @@ require File.dirname(__FILE__) + '/rest_client/net_http_ext' # => "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={}) + def self.get(url, headers={}, &b) Request.execute(:method => :get, :url => url, - :headers => headers) + :headers => headers, &b) end - def self.post(url, payload, headers={}) + def self.post(url, payload, headers={}, &b) Request.execute(:method => :post, :url => url, :payload => payload, - :headers => headers) + :headers => headers, &b) end - def self.put(url, payload, headers={}) + def self.put(url, payload, headers={}, &b) Request.execute(:method => :put, :url => url, :payload => payload, - :headers => headers) + :headers => headers, &b) end - def self.delete(url, headers={}) + def self.delete(url, headers={}, &b) Request.execute(:method => :delete, :url => url, - :headers => headers) + :headers => headers, &b) end # Internal class used to build and execute the request. class Request attr_reader :method, :url, :headers, :user, :password - def self.execute(args) - new(args).execute + def self.execute(args, &b) + new(args).execute(&b) end def initialize(args) @@ -78,16 +78,16 @@ module RestClient @password = args[:password] end - def execute - execute_inner + def execute(&b) + execute_inner(&b) rescue Redirect => e @url = e.url - execute + execute(&b) end - def execute_inner + def execute_inner(&b) uri = parse_url_with_auth(url) - transmit uri, net_http_class(method).new(uri.request_uri, make_headers(headers)), payload + transmit(uri, net_http_class(method).new(uri.request_uri, make_headers(headers)), payload, &b) end def make_headers(user_headers) @@ -132,14 +132,17 @@ module RestClient end end - def transmit(uri, req, payload) + def transmit(uri, req, payload, &b) setup_credentials(req) net = Net::HTTP.new(uri.host, uri.port) net.use_ssl = uri.is_a?(URI::HTTPS) net.start do |http| - process_result http.request(req, payload || "") + ## 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 + process_result(http.request(req, payload || "", &b), &b) end rescue EOFError raise RestClient::ServerBrokeConnection @@ -151,9 +154,9 @@ module RestClient req.basic_auth(user, password) if user end - def process_result(res) + def process_result(res, &b) if %w(200 201 202).include? res.code - res.body + return res.body unless b elsif %w(301 302 303).include? res.code url = res.header['Location'] diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb index d1f0d2d..69aa6c1 100644 --- a/lib/rest_client/payload.rb +++ b/lib/rest_client/payload.rb @@ -1,4 +1,5 @@ require "tempfile" +require "stringio" module RestClient diff --git a/lib/rest_client/resource.rb b/lib/rest_client/resource.rb index d2ca2e8..db44ce5 100644 --- a/lib/rest_client/resource.rb +++ b/lib/rest_client/resource.rb @@ -26,38 +26,38 @@ module RestClient @password = password end - def get(headers={}) + def get(headers={}, &b) Request.execute(:method => :get, :url => url, :user => user, :password => password, - :headers => headers) + :headers => headers, &b) end - def post(payload, headers={}) + def post(payload, headers={}, &b) Request.execute(:method => :post, :url => url, :payload => payload, :user => user, :password => password, - :headers => headers) + :headers => headers, &b) end - def put(payload, headers={}) + def put(payload, headers={}, &b) Request.execute(:method => :put, :url => url, :payload => payload, :user => user, :password => password, - :headers => headers) + :headers => headers, &b) end - def delete(headers={}) + def delete(headers={}, &b) Request.execute(:method => :delete, :url => url, :user => user, :password => password, - :headers => headers) + :headers => headers, &b) end # Construct a subresource, preserving authentication. From 8b000e1a602d005e20a79e6375493de1cb4683e7 Mon Sep 17 00:00:00 2001 From: bmizerany Date: Fri, 25 Jul 2008 13:10:40 -0700 Subject: [PATCH 04/10] close those streams --- lib/rest_client.rb | 2 ++ lib/rest_client/payload.rb | 25 ++++++++++++++++++------- spec/base.rb | 1 + spec/rest_client_spec.rb | 22 +++++++++++----------- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/rest_client.rb b/lib/rest_client.rb index bf4c702..20b1f07 100644 --- a/lib/rest_client.rb +++ b/lib/rest_client.rb @@ -148,6 +148,8 @@ module RestClient raise RestClient::ServerBrokeConnection rescue Timeout::Error raise RestClient::RequestTimeout + ensure + payload.close end def setup_credentials(req) diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb index 69aa6c1..803d323 100644 --- a/lib/rest_client/payload.rb +++ b/lib/rest_client/payload.rb @@ -48,7 +48,10 @@ module RestClient end alias :length :size - + def close + @stream.close + end + end class UrlEncoded < Base @@ -63,7 +66,7 @@ module RestClient def headers super.merge({'Content-Type' => 'application/x-www-form-urlencoded'}) end - + end class Multipart < Base @@ -95,11 +98,15 @@ module RestClient end def create_file_field(s, k, v) - s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}") - s.write("Content-Type: #{mime_for(v.path)}#{EOL}") - s.write(EOL) - while data = v.read(8124) - s.write(data) + begin + s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}") + s.write("Content-Type: #{mime_for(v.path)}#{EOL}") + s.write(EOL) + while data = v.read(8124) + s.write(data) + end + ensure + v.close end end @@ -115,6 +122,10 @@ module RestClient def headers super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}}) end + + def close + @stream.close + end end diff --git a/spec/base.rb b/spec/base.rb index 192612c..cdef584 100644 --- a/spec/base.rb +++ b/spec/base.rb @@ -2,3 +2,4 @@ require 'rubygems' require 'spec' require File.dirname(__FILE__) + '/../lib/rest_client' + diff --git a/spec/rest_client_spec.rb b/spec/rest_client_spec.rb index f875c20..865ff72 100644 --- a/spec/rest_client_spec.rb +++ b/spec/rest_client_spec.rb @@ -1,6 +1,11 @@ require File.dirname(__FILE__) + '/base' describe RestClient do + + def generate_payload(v) + RestClient::Payload::Base.new(v) + end + context "public API" do it "GET" do RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}) @@ -104,9 +109,9 @@ describe RestClient do end it "transmits the request with Net::HTTP" do - @http.should_receive(:request).with('req', 'payload') + @http.should_receive(:request).with('req', be_kind_of(RestClient::Payload::Base)) @request.should_receive(:process_result) - @request.transmit(@uri, 'req', 'payload') + @request.transmit(@uri, 'req', generate_payload('payload')) end it "uses SSL when the URI refers to a https address" do @@ -114,13 +119,7 @@ describe RestClient do @net.should_receive(:use_ssl=).with(true) @http.stub!(:request) @request.stub!(:process_result) - @request.transmit(@uri, 'req', 'payload') - end - - it "doesn't send nil payloads" do - @http.should_receive(:request).with('req', '') - @request.should_receive(:process_result) - @request.transmit(@uri, 'req', nil) + @request.transmit(@uri, 'req', generate_payload('payload')) end it "passes non-hash payloads straight through" do @@ -151,7 +150,7 @@ describe RestClient do @request.stub!(:password).and_return('mypass') @request.should_receive(:setup_credentials).with('req') - @request.transmit(@uri, 'req', nil) + @request.transmit(@uri, 'req', generate_payload('')) end it "does not attempt to send any credentials if user is nil" do @@ -171,7 +170,8 @@ describe RestClient do 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) + lambda { @request.transmit(@uri, 'req', generate_payload('')) }. + should raise_error(RestClient::ServerBrokeConnection) end it "execute calls execute_inner" do From 7a9f4db3c915c53d6e64d826e6773b3ff5be3d6a Mon Sep 17 00:00:00 2001 From: bmizerany Date: Wed, 30 Jul 2008 17:58:02 -0700 Subject: [PATCH 05/10] support .gz for mulitpart --- lib/rest_client/payload.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb index 803d323..3ae337a 100644 --- a/lib/rest_client/payload.rb +++ b/lib/rest_client/payload.rb @@ -150,6 +150,7 @@ module RestClient "etx" => "text/x-setext", "exe" => "application/octet-stream", "gif" => "image/gif", + "gz" => "application/x-gzip", "htm" => "text/html", "html" => "text/html", "jpe" => "image/jpeg", From 640724f6d4e295b9c54d8465b7e75e3e989e7844 Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sun, 3 Aug 2008 15:46:24 -0700 Subject: [PATCH 06/10] i'm fussy about whitespace --- lib/rest_client/net_http_ext.rb | 24 ++--- lib/rest_client/payload.rb | 157 +++++++++++++++----------------- 2 files changed, 84 insertions(+), 97 deletions(-) diff --git a/lib/rest_client/net_http_ext.rb b/lib/rest_client/net_http_ext.rb index 5499b1b..def1f2b 100644 --- a/lib/rest_client/net_http_ext.rb +++ b/lib/rest_client/net_http_ext.rb @@ -2,20 +2,20 @@ # Replace the request method in Net::HTTP to sniff the body type # and set the stream if appropriate # -# Taken from: +# Taken from: # http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/ module Net - class HTTP - alias __request__ request + class HTTP + alias __request__ request - def request(req, body = nil, &block) - if body != nil && body.respond_to?(:read) - req.body_stream = body - return __request__(req, nil, &block) - else - return __request__(req, body, &block) - end - end - end + def request(req, body=nil, &block) + if body != nil && body.respond_to?(:read) + req.body_stream = body + return __request__(req, nil, &block) + else + return __request__(req, body, &block) + end + end + end end diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb index 3ae337a..dfc2921 100644 --- a/lib/rest_client/payload.rb +++ b/lib/rest_client/payload.rb @@ -2,12 +2,9 @@ require "tempfile" require "stringio" module RestClient - module Payload extend self - class NotImplemented < RuntimeError; end - def generate(params) if params.is_a?(String) Base.new(params) @@ -20,62 +17,57 @@ module RestClient end class Base - def initialize(params) build_stream(params) end - + def build_stream(params) @stream = StringIO.new(params) @stream.seek(0) end - + def read(bytes=nil) @stream.read(bytes) end alias :to_s :read - + def escape(v) URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")) end def headers - {'Content-Length' => size.to_s} + { 'Content-Length' => size.to_s } end - + def size @stream.size end alias :length :size - + def close @stream.close end - end class UrlEncoded < Base - def build_stream(params) @stream = StringIO.new(params.map do |k,v| "#{escape(k)}=#{escape(v)}" end.join("&")) @stream.seek(0) end - + def headers - super.merge({'Content-Type' => 'application/x-www-form-urlencoded'}) + super.merge({ 'Content-Type' => 'application/x-www-form-urlencoded' }) end - end class Multipart < Base - EOL = "\r\n" - + def build_stream(params) b = "--#{boundary}" - + @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}") @stream.write(b + EOL) params.each do |k,v| @@ -89,7 +81,7 @@ module RestClient @stream.write('--') @stream.seek(0) end - + def create_regular_field(s, k, v) s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"") s.write(EOL) @@ -109,7 +101,7 @@ module RestClient v.close end end - + def mime_for(path) ext = File.extname(path)[1..-1] MIME_TYPES[ext] || 'text/plain' @@ -118,79 +110,74 @@ module RestClient def boundary @boundary ||= rand(1_000_000).to_s end - + def headers super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}}) end - + def close @stream.close end - end # :stopdoc: - # From WEBrick. - MIME_TYPES = { - "ai" => "application/postscript", - "asc" => "text/plain", - "avi" => "video/x-msvideo", - "bin" => "application/octet-stream", - "bmp" => "image/bmp", - "class" => "application/octet-stream", - "cer" => "application/pkix-cert", - "crl" => "application/pkix-crl", - "crt" => "application/x-x509-ca-cert", - #"crl" => "application/x-pkcs7-crl", - "css" => "text/css", - "dms" => "application/octet-stream", - "doc" => "application/msword", - "dvi" => "application/x-dvi", - "eps" => "application/postscript", - "etx" => "text/x-setext", - "exe" => "application/octet-stream", - "gif" => "image/gif", - "gz" => "application/x-gzip", - "htm" => "text/html", - "html" => "text/html", - "jpe" => "image/jpeg", - "jpeg" => "image/jpeg", - "jpg" => "image/jpeg", - "js" => "text/javascript", - "lha" => "application/octet-stream", - "lzh" => "application/octet-stream", - "mov" => "video/quicktime", - "mpe" => "video/mpeg", - "mpeg" => "video/mpeg", - "mpg" => "video/mpeg", - "pbm" => "image/x-portable-bitmap", - "pdf" => "application/pdf", - "pgm" => "image/x-portable-graymap", - "png" => "image/png", - "pnm" => "image/x-portable-anymap", - "ppm" => "image/x-portable-pixmap", - "ppt" => "application/vnd.ms-powerpoint", - "ps" => "application/postscript", - "qt" => "video/quicktime", - "ras" => "image/x-cmu-raster", - "rb" => "text/plain", - "rd" => "text/plain", - "rtf" => "application/rtf", - "sgm" => "text/sgml", - "sgml" => "text/sgml", - "tif" => "image/tiff", - "tiff" => "image/tiff", - "txt" => "text/plain", - "xbm" => "image/x-xbitmap", - "xls" => "application/vnd.ms-excel", - "xml" => "text/xml", - "xpm" => "image/x-xpixmap", - "xwd" => "image/x-xwindowdump", - "zip" => "application/zip", - } - # :startdoc: - - + # From WEBrick. + MIME_TYPES = { + "ai" => "application/postscript", + "asc" => "text/plain", + "avi" => "video/x-msvideo", + "bin" => "application/octet-stream", + "bmp" => "image/bmp", + "class" => "application/octet-stream", + "cer" => "application/pkix-cert", + "crl" => "application/pkix-crl", + "crt" => "application/x-x509-ca-cert", + "css" => "text/css", + "dms" => "application/octet-stream", + "doc" => "application/msword", + "dvi" => "application/x-dvi", + "eps" => "application/postscript", + "etx" => "text/x-setext", + "exe" => "application/octet-stream", + "gif" => "image/gif", + "gz" => "application/x-gzip", + "htm" => "text/html", + "html" => "text/html", + "jpe" => "image/jpeg", + "jpeg" => "image/jpeg", + "jpg" => "image/jpeg", + "js" => "text/javascript", + "lha" => "application/octet-stream", + "lzh" => "application/octet-stream", + "mov" => "video/quicktime", + "mpe" => "video/mpeg", + "mpeg" => "video/mpeg", + "mpg" => "video/mpeg", + "pbm" => "image/x-portable-bitmap", + "pdf" => "application/pdf", + "pgm" => "image/x-portable-graymap", + "png" => "image/png", + "pnm" => "image/x-portable-anymap", + "ppm" => "image/x-portable-pixmap", + "ppt" => "application/vnd.ms-powerpoint", + "ps" => "application/postscript", + "qt" => "video/quicktime", + "ras" => "image/x-cmu-raster", + "rb" => "text/plain", + "rd" => "text/plain", + "rtf" => "application/rtf", + "sgm" => "text/sgml", + "sgml" => "text/sgml", + "tif" => "image/tiff", + "tiff" => "image/tiff", + "txt" => "text/plain", + "xbm" => "image/x-xbitmap", + "xls" => "application/vnd.ms-excel", + "xml" => "text/xml", + "xpm" => "image/x-xpixmap", + "xwd" => "image/x-xwindowdump", + "zip" => "application/zip", + } + # :startdoc: end - end From bc0e303e74938ef78b414b18c5b080cb04c4e04d Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sun, 3 Aug 2008 16:09:19 -0700 Subject: [PATCH 07/10] convert payload spec to rspec --- spec/payload_spec.rb | 95 ++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/spec/payload_spec.rb b/spec/payload_spec.rb index 5bf96ea..22b5a5a 100644 --- a/spec/payload_spec.rb +++ b/spec/payload_spec.rb @@ -1,46 +1,42 @@ require File.dirname(__FILE__) + "/base" -context "A regular Payload" do +describe RestClient::Payload do + context "A regular Payload" do + it "should should default content-type to standard enctype" do + RestClient::Payload::UrlEncoded.new({}).headers['Content-Type']. + should == 'application/x-www-form-urlencoded' + end - specify "should should default content-type to standard enctype" do - RestClient::Payload::UrlEncoded.new({}).headers['Content-Type']. - should == 'application/x-www-form-urlencoded' + it "should form properly encoded params" do + RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s. + should == "foo=bar" + end end - specify "should form properly encoded params" do - RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s. - should == "foo=bar" - end + context "A multipart Payload" do + it "should should default content-type to standard enctype" do + m = RestClient::Payload::Multipart.new({}) + m.stub!(:boundary).and_return(123) + m.headers['Content-Type'].should == 'multipart/form-data; boundary="123"' + end -end - -context "A multipart Payload" do - specify "should should default content-type to standard enctype" do - m = RestClient::Payload::Multipart.new({}) - m.stub!(:boundary).and_return(123) - m.headers['Content-Type'].should == 'multipart/form-data; boundary="123"' - end - - xspecify "should form properly seperated multipart data" do - m = RestClient::Payload::Multipart.new({:foo => "bar"}) - m.stub!(:boundary).and_return("123") - m.to_s.should == <<-EOS + xit "should form properly seperated multipart data" do + m = RestClient::Payload::Multipart.new({:foo => "bar"}) + m.stub!(:boundary).and_return("123") + m.to_s.should == <<-EOS --123\r Content-Disposition: multipart/form-data; name="foo"\r \r bar\r --123--\r EOS - end + end - xspecify "should form properly seperated multipart data" do - f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") - # f = mock("master_shake.jpg") - # f.stub!(:path).and_return("master_shake.jpg") - # f.stub!(:read).and_return("datadatadata") - m = RestClient::Payload::Multipart.new({:foo => f}) - m.stub!(:boundary).and_return("123") - m.to_s.should == <<-EOS + xit "should form properly seperated multipart data" do + f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") + m = RestClient::Payload::Multipart.new({:foo => f}) + m.stub!(:boundary).and_return("123") + m.to_s.should == <<-EOS --123\r Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r Content-Type: image/jpeg\r @@ -48,31 +44,26 @@ Content-Type: image/jpeg\r datadatadata\r --123--\r EOS + end end -end + context "Payload generation" do + it "should recognize standard urlencoded params" do + RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded) + end + + it "should recognize multipart params" do + f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") -context "Payload generation" do - - specify "should recognize standard urlencoded params" do - RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded) - end - - specify "should recognize multipart params" do - # f = mock("master_shake.jpg") - # f.stub!(:path) - # f.stub!(:read) - f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") + RestClient::Payload.generate({"foo" => f}).should be_kind_of(RestClient::Payload::Multipart) + end - RestClient::Payload.generate({"foo" => f}).should be_kind_of(RestClient::Payload::Multipart) + it "should be multipart if forced" do + RestClient::Payload.generate({"foo" => "bar", :multipart => true}).should be_kind_of(RestClient::Payload::Multipart) + end + + it "should return data if no of the above" do + RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base) + end end - - specify "should be multipart if forced" do - RestClient::Payload.generate({"foo" => "bar", :multipart => true}).should be_kind_of(RestClient::Payload::Multipart) - end - - specify "should return data if no of the above" do - RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base) - end - end From ea2e901cf41c19c43d00579d406b7575e313bfbb Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sun, 3 Aug 2008 16:09:49 -0700 Subject: [PATCH 08/10] generate_payload must be out the describe --- spec/rest_client_spec.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/spec/rest_client_spec.rb b/spec/rest_client_spec.rb index 865ff72..62696fe 100644 --- a/spec/rest_client_spec.rb +++ b/spec/rest_client_spec.rb @@ -1,11 +1,10 @@ require File.dirname(__FILE__) + '/base' +def generate_payload(v) + RestClient::Payload::Base.new(v) +end + describe RestClient do - - def generate_payload(v) - RestClient::Payload::Base.new(v) - end - context "public API" do it "GET" do RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {}) From bf3d50ab9eb33fe3758aa3f35be3f4d33cd8075c Mon Sep 17 00:00:00 2001 From: Adam Wiggins Date: Sun, 3 Aug 2008 16:12:05 -0700 Subject: [PATCH 09/10] credits update --- README.rdoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rdoc b/README.rdoc index 04c3e66..4d9ddf1 100644 --- a/README.rdoc +++ b/README.rdoc @@ -92,7 +92,9 @@ Then invoke: Written by Adam Wiggins (adam at heroku dot com) -Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, and Blake Mizerany +Major modifications by Blake Mizerany + +Patches contributed by: Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, and Aman Gupta Released under the MIT License: http://www.opensource.org/licenses/mit-license.php From 284b69f169632115201795254345de1116636dda Mon Sep 17 00:00:00 2001 From: rick Date: Wed, 10 Dec 2008 20:06:10 -0800 Subject: [PATCH 10/10] fix spacing issues with multipart bodies: * the final boundary should be on a line below the final param's content. * the ending double dash should be on the same line as the final boundary --- lib/rest_client/payload.rb | 3 ++- spec/payload_spec.rb | 18 ++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/rest_client/payload.rb b/lib/rest_client/payload.rb index dfc2921..e896be0 100644 --- a/lib/rest_client/payload.rb +++ b/lib/rest_client/payload.rb @@ -76,9 +76,10 @@ module RestClient else create_regular_field(@stream, k,v) end - @stream.write(b + EOL) + @stream.write(EOL + b) end @stream.write('--') + @stream.write(EOL) @stream.seek(0) end diff --git a/spec/payload_spec.rb b/spec/payload_spec.rb index 22b5a5a..35564ee 100644 --- a/spec/payload_spec.rb +++ b/spec/payload_spec.rb @@ -20,29 +20,27 @@ describe RestClient::Payload do m.headers['Content-Type'].should == 'multipart/form-data; boundary="123"' end - xit "should form properly seperated multipart data" do + it "should form properly seperated multipart data" do m = RestClient::Payload::Multipart.new({:foo => "bar"}) - m.stub!(:boundary).and_return("123") m.to_s.should == <<-EOS ---123\r +--#{m.boundary}\r Content-Disposition: multipart/form-data; name="foo"\r \r bar\r ---123--\r +--#{m.boundary}--\r EOS end - xit "should form properly seperated multipart data" do + it "should form properly seperated multipart data" do f = File.new(File.dirname(__FILE__) + "/master_shake.jpg") m = RestClient::Payload::Multipart.new({:foo => f}) - m.stub!(:boundary).and_return("123") m.to_s.should == <<-EOS ---123\r -Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r +--#{m.boundary}\r +Content-Disposition: multipart/form-data; name="foo"; filename="./spec/master_shake.jpg"\r Content-Type: image/jpeg\r \r -datadatadata\r ---123--\r +#{IO.read(f.path)}\r +--#{m.boundary}--\r EOS end end