Skip to content

Commit

Permalink
Simplifying Rest API code to pass on RestClient and MultiJSON options
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Jun 25, 2011
1 parent dba06eb commit 974d283
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 49 deletions.
3 changes: 2 additions & 1 deletion 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
Expand Down
8 changes: 7 additions & 1 deletion lib/couchrest/database.rb
Expand Up @@ -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?
Expand Down Expand Up @@ -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
154 changes: 116 additions & 38 deletions lib/couchrest/rest_api.rb
@@ -1,69 +1,136 @@
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,
:accept => :json
}
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
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/couchrest/version.rb
@@ -1,3 +1,3 @@
module CouchRest
VERSION = '1.1.0.pre3'
VERSION = '1.1.0'
end
2 changes: 1 addition & 1 deletion spec/couchrest/couchrest_spec.rb
Expand Up @@ -25,7 +25,7 @@
@cr.info.class.should == Hash
end
end

it "should restart" do
@cr.restart!
begin
Expand Down
13 changes: 6 additions & 7 deletions spec/couchrest/database_spec.rb
Expand Up @@ -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'
Expand All @@ -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

Expand Down

0 comments on commit 974d283

Please sign in to comment.