Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Simplifying Rest API code to pass on RestClient and MultiJSON options

  • Loading branch information...
commit 974d2830392d61fe7f71bb178f131c49ef81e9cf 1 parent dba06eb
@samlown samlown authored
View
3  history.txt
@@ -1,6 +1,7 @@
-== 1.1.0.pre4 - 2011-06-13
+== 1.1.0 - 2011-06-25
* Minor changes
+ * Refactored basic CouchRest API (get, post, etc.) to pass-through RestClient and MultiJSON options, including headers.
* CouchRest::Attributes module created to make attribute related methods independent.
== 1.1.0.pre3 - 2011-06-06
View
8 lib/couchrest/database.rb
@@ -245,7 +245,7 @@ def update_doc(doc_id, params = {}, update_limit = 10)
def view(name, params = {}, payload = {}, &block)
payload['keys'] = params.delete(:keys) if params[:keys]
# Try recognising the name, otherwise assume already prepared
- view_path = name =~ /^([^_].+?)\/(.*)$/ ? "_design/#{$1}/_view/#{$2}" : name
+ view_path = name_to_view_path(name)
url = CouchRest.paramify_url "#{@root}/#{view_path}", params
if block_given?
if !payload.empty?
@@ -386,5 +386,11 @@ def encode_attachments(attachments)
def base64(data)
Base64.encode64(data).gsub(/\s/,'')
end
+
+ # Convert a simplified view name into a complete view path. If
+ # the name already starts with a "_" no alterations will be made.
+ def name_to_view_path(name)
+ name =~ /^([^_].+?)\/(.*)$/ ? "_design/#{$1}/_view/#{$2}" : name
+ end
end
end
View
154 lib/couchrest/rest_api.rb
@@ -1,7 +1,75 @@
module CouchRest
+ # CouchRest RestAPI
+ #
+ # The basic low-level interface for all REST requests to the database. Everything must pass
+ # through here before it is sent to the server.
+ #
+ # Five types of REST requests are supported: get, put, post, delete, and copy.
+ #
+ # Requests that do not have a payload, get, delete and copy, accept the URI and options parameters,
+ # where as put and post both expect a document as the second parameter.
+ #
+ # The API will try to intelegently split the options between the JSON parser and RestClient API.
+ #
+ # The following options will be recognised as header options and automatically added
+ # to the header hash:
+ #
+ # * :content_type, type of content to be sent, especially useful when sending files as this will set the file type. The default is :json.
+ # * :accept, the content type to accept in the response. This should pretty much always be :json.
+ #
+ # The following request options are supported:
+ #
+ # * :method override the requested method (should not be used!).
+ # * :url override the URL used in the request.
+ # * :payload override the document or data sent in the message body (only PUT or POST).
+ # * :headers any additional headers (overrides :content_type and :accept)
+ # * :timeout and :open_timeout the time in miliseconds to wait for the request, see RestClient API for more details.
+ # * :verify_ssl, :ssl_client_cert, :ssl_client_key, and :ssl_ca_file, SSL handling methods.
+ #
+ #
+ # Any other remaining options will be sent to the MultiJSON backend except for the :raw option.
+ #
+ # When :raw is true in PUT and POST requests, no attempt will be made to convert the document payload to JSON. This is
+ # not normally necessary as IO and Tempfile objects will not be parsed anyway. The result of the request will
+ # *always* be parsed.
+ #
+ # For all other requests, mainly GET, the :raw option will make no attempt to parse the result. This
+ # is useful for receiving files from the database.
+ #
+
module RestAPI
+ # Send a GET request.
+ def get(uri, options = {})
+ execute(uri, :get, options)
+ end
+
+ # Send a PUT request.
+ def put(uri, doc = nil, options = {})
+ execute(uri, :put, options, doc)
+ end
+
+ # Send a POST request.
+ def post(uri, doc = nil, options = {})
+ execute(uri, :post, options, doc)
+ end
+
+ # Send a DELETE request.
+ def delete(uri, options = {})
+ execute(uri, :delete, options)
+ end
+
+ # Send a COPY request to the URI provided.
+ def copy(uri, destination, options = {})
+ opts = options.nil? ? {} : options.dup
+ # also copy headers!
+ opts[:headers] = options[:headers].nil? ? {} : options[:headers].dup
+ opts[:headers]['Destination'] = destination
+ execute(uri, :copy, opts)
+ end
+
+ # The default RestClient headers used in each request.
def default_headers
{
:content_type => :json,
@@ -9,61 +77,60 @@ def default_headers
}
end
- def get(uri, options = {})
- begin
- parse_response(RestClient.get(uri, default_headers), options.dup)
- rescue => e
- if $DEBUG
- raise "Error while sending a GET request #{uri}\n: #{e}"
- else
- raise e
- end
- end
- end
+ private
- def put(uri, doc = nil, options = {})
- opts = options.dup
- payload = payload_from_doc(doc, opts)
+ # Perform the RestClient request by removing the parse specific options, ensuring the
+ # payload is prepared, and sending the request ready to parse the response.
+ def execute(url, method, options = {}, payload = nil)
+ request, parser = prepare_and_split_options(url, method, options)
+ # Prepare the payload if it is provided
+ request[:payload] = payload_from_doc(payload, parser) if payload
begin
- parse_response(RestClient.put(uri, payload, default_headers.merge(opts)), opts)
+ parse_response(RestClient::Request.execute(request), parser)
rescue Exception => e
if $DEBUG
- raise "Error while sending a PUT request #{uri}\npayload: #{payload.inspect}\n#{e}"
+ raise "Error while sending a #{method.to_s.upcase} request #{uri}\noptions: #{opts.inspect}\n#{e}"
else
raise e
end
end
end
- def post(uri, doc = nil, options = {})
- opts = options.dup
- payload = payload_from_doc(doc, opts)
- begin
- parse_response(RestClient.post(uri, payload, default_headers.merge(opts)), opts)
- rescue Exception => e
- if $DEBUG
- raise "Error while sending a POST request #{uri}\npayload: #{payload.inspect}\n#{e}"
+ # Prepare a two hashes, one for the request to the REST backend and a second
+ # for the JSON parser.
+ #
+ # Returns and array of request options and parser options.
+ #
+ def prepare_and_split_options(url, method, options)
+ request = {
+ :url => url,
+ :method => method,
+ :headers => default_headers.merge(options[:headers] || {})
+ }
+ parser = {
+ :raw => false
+ }
+ # Split the options
+ (options || {}).each do |k,v|
+ k = k.to_sym
+ next if k == :headers # already dealt with
+ if restclient_option_keys.include?(k)
+ request[k] = v
+ elsif header_option_keys.include?(k)
+ request[:headers][k] = v
else
- raise e
+ parser[k] = v
end
end
+ [request, parser]
end
- def delete(uri, options = {})
- parse_response(RestClient.delete(uri, default_headers), options.dup)
- end
-
- def copy(uri, destination, options = {})
- parse_response(RestClient::Request.execute( :method => :copy,
- :url => uri,
- :headers => default_headers.merge('Destination' => destination)
- ).to_s, options)
- end
-
- protected
-
# Check if the provided doc is nil or special IO device or temp file. If not,
# encode it into a string.
+ #
+ # The options supported are:
+ # * :raw TrueClass, if true the payload will not be altered.
+ #
def payload_from_doc(doc, opts = {})
(opts.delete(:raw) || doc.nil? || doc.is_a?(IO) || doc.is_a?(Tempfile)) ? doc : MultiJson.encode(doc)
end
@@ -73,5 +140,16 @@ def parse_response(result, opts = {})
opts.delete(:raw) ? result : MultiJson.decode(result, opts.update(:max_nesting => false))
end
+ # An array of all the options that should be passed through to restclient.
+ # Anything not in this list will be passed to the JSON parser.
+ def restclient_option_keys
+ [:method, :url, :payload, :headers, :timeout, :open_timeout,
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file]
+ end
+
+ def header_option_keys
+ [ :content_type, :accept ]
+ end
+
end
end
View
2  lib/couchrest/version.rb
@@ -1,3 +1,3 @@
module CouchRest
- VERSION = '1.1.0.pre3'
+ VERSION = '1.1.0'
end
View
2  spec/couchrest/couchrest_spec.rb
@@ -25,7 +25,7 @@
@cr.info.class.should == Hash
end
end
-
+
it "should restart" do
@cr.restart!
begin
View
13 spec/couchrest/database_spec.rb
@@ -303,7 +303,7 @@
@db.fetch_attachment(@db.get('mydocwithattachment'), 'test.html').should == @attach
end
end
-
+
describe "PUT attachment from file" do
before(:each) do
filename = FIXTURE_PATH + '/attachments/couchdb.png'
@@ -316,12 +316,11 @@
r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image = @file.read, {:content_type => 'image/png'})
r['ok'].should == true
doc = @db.get("attach-this")
- attachment = @db.fetch_attachment(doc,"couchdb.png")
- if attachment.respond_to?(:net_http_res)
- attachment.net_http_res.body.should == image
- else
- attachment.should == image
- end
+ attachment = @db.fetch_attachment(doc, "couchdb.png")
+ (attachment == image).should be_true
+ #if attachment.respond_to?(:net_http_res)
+ # attachment.net_http_res.body.should == image
+ #end
end
end
View
204 spec/couchrest/rest_api_spec.rb
@@ -0,0 +1,204 @@
+require File.expand_path("../../spec_helper", __FILE__)
+
+describe CouchRest::RestAPI do
+
+ describe "class methods" do
+
+ subject { CouchRest }
+
+ let(:request) { RestClient::Request }
+ let(:simple_response) { "{\"ok\":true}" }
+ let(:parser) { MultiJson }
+ let(:parser_opts) { {:max_nesting => false} }
+
+ it "should exist" do
+ should respond_to :get
+ should respond_to :put
+ should respond_to :post
+ should respond_to :copy
+ should respond_to :delete
+ end
+
+ it "should provide default headers" do
+ should respond_to :default_headers
+ CouchRest.default_headers.should be_a(Hash)
+ end
+
+
+ describe :get do
+ it "should send basic request" do
+ req = {:url => 'foo', :method => :get, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.get('foo')
+ end
+
+ it "should never modify options" do
+ options = {:timeout => 1000}
+ options.freeze
+ request.should_receive(:execute).and_return(simple_response)
+ parser.should_receive(:decode)
+ expect { CouchRest.get('foo', options) }.to_not raise_error
+ end
+
+
+ it "should accept 'content_type' header" do
+ req = {:url => 'foo', :method => :get, :headers => CouchRest.default_headers.merge(:content_type => :foo)}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.get('foo', :content_type => :foo)
+ end
+
+ it "should accept 'accept' header" do
+ req = {:url => 'foo', :method => :get, :headers => CouchRest.default_headers.merge(:accept => :foo)}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.get('foo', :accept => :foo)
+ end
+
+ it "should forward RestClient options" do
+ req = {:url => 'foo', :method => :get, :timeout => 1000, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.get('foo', :timeout => 1000)
+ end
+
+ it "should forward parser options" do
+ req = {:url => 'foo', :method => :get, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts.merge(:random => 'foo'))
+ CouchRest.get('foo', :random => 'foo')
+ end
+
+ it "should accept raw option" do
+ req = {:url => 'foo', :method => :get, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_not_receive(:decode)
+ CouchRest.get('foo', :raw => true).should eql(simple_response)
+ end
+
+ it "should allow override of method (not that you'd want to!)" do
+ req = {:url => 'foo', :method => :fubar, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.get('foo', :method => :fubar)
+ end
+
+ it "should allow override of url (not that you'd want to!)" do
+ req = {:url => 'foobardom', :method => :get, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.get('foo', :url => 'foobardom')
+ end
+
+
+ it "should forward an exception if raised" do
+ request.should_receive(:execute).and_raise(RestClient::Exception)
+ expect { CouchRest.get('foo') }.to raise_error(RestClient::Exception)
+ end
+
+ end
+
+ describe :post do
+ it "should send basic request" do
+ req = {:url => 'foo', :method => :post, :headers => CouchRest.default_headers, :payload => 'data'}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:encode).with('data').and_return('data')
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.post('foo', 'data')
+ end
+
+ it "should send basic request" do
+ req = {:url => 'foo', :method => :post, :headers => CouchRest.default_headers, :payload => 'data'}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:encode).with('data').and_return('data')
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.post('foo', 'data')
+ end
+
+ it "should send raw request" do
+ req = {:url => 'foo', :method => :post, :headers => CouchRest.default_headers, :payload => 'data'}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_not_receive(:encode)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.post('foo', 'data', :raw => true)
+ end
+
+ it "should not encode nil request" do
+ req = {:url => 'foo', :method => :post, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_not_receive(:encode)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.post('foo', nil)
+ end
+
+ it "should send raw request automatically if file provided" do
+ f = File.open(FIXTURE_PATH + '/attachments/couchdb.png')
+ req = {:url => 'foo', :method => :post, :headers => CouchRest.default_headers, :payload => f}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_not_receive(:encode)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.post('foo', f)
+ f.close
+ end
+
+ it "should send raw request automatically if Tempfile provided" do
+ f = Tempfile.new('couchrest')
+ req = {:url => 'foo', :method => :post, :headers => CouchRest.default_headers, :payload => f}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_not_receive(:encode)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.post('foo', f)
+ f.close
+ end
+
+
+ end
+
+
+ describe :put do
+ # Only test basic as practically same as post
+ it "should send basic request" do
+ req = {:url => 'foo', :method => :put, :headers => CouchRest.default_headers, :payload => 'data'}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:encode).with('data').and_return('data')
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.put('foo', 'data')
+ end
+
+ end
+
+ describe :delete do
+ it "should send basic request" do
+ req = {:url => 'foo', :method => :delete, :headers => CouchRest.default_headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.delete('foo')
+ end
+ end
+
+ describe :copy do
+ it "should send basic request" do
+ headers = CouchRest.default_headers.merge(
+ 'Destination' => 'fooobar'
+ )
+ req = {:url => 'foo', :method => :copy, :headers => headers}
+ request.should_receive(:execute).with(req).and_return(simple_response)
+ parser.should_receive(:decode).with(simple_response, parser_opts)
+ CouchRest.copy('foo', 'fooobar')
+ end
+
+ it "should never modify header options" do
+ options = {:headers => {:content_type => :foo}}
+ options.freeze
+ request.should_receive(:execute).and_return(simple_response)
+ parser.should_receive(:decode)
+ expect { CouchRest.copy('foo', 'foobar', options) }.to_not raise_error
+ end
+
+ end
+
+
+ end
+
+end
Please sign in to comment.
Something went wrong with that request. Please try again.