couch-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +1 -0
- data/LICENSE +22 -0
- data/Manifest +33 -0
- data/README.markdown +176 -0
- data/Rakefile +17 -0
- data/TODO +11 -0
- data/couch-client.gemspec +33 -0
- data/lib/couch-client.rb +50 -0
- data/lib/couch-client/attachment.rb +32 -0
- data/lib/couch-client/attachment_list.rb +10 -0
- data/lib/couch-client/collection.rb +20 -0
- data/lib/couch-client/connection.rb +104 -0
- data/lib/couch-client/connection_handler.rb +70 -0
- data/lib/couch-client/consistent_hash.rb +98 -0
- data/lib/couch-client/database.rb +39 -0
- data/lib/couch-client/design.rb +79 -0
- data/lib/couch-client/document.rb +151 -0
- data/lib/couch-client/hookup.rb +107 -0
- data/lib/couch-client/row.rb +10 -0
- data/spec/attachment_list_spec.rb +7 -0
- data/spec/attachment_spec.rb +57 -0
- data/spec/collection_spec.rb +43 -0
- data/spec/conection_handler_spec.rb +66 -0
- data/spec/connection_spec.rb +93 -0
- data/spec/consistent_hash_spec.rb +171 -0
- data/spec/couch-client_spec.rb +11 -0
- data/spec/database_spec.rb +44 -0
- data/spec/design_spec.rb +100 -0
- data/spec/document_spec.rb +196 -0
- data/spec/files/image.png +0 -0
- data/spec/files/plain.txt +1 -0
- data/spec/hookup_spec.rb +122 -0
- data/spec/row_spec.rb +35 -0
- data/spec/spec_helper.rb +11 -0
- metadata +130 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module CouchClient
|
4
|
+
class InvalidPathObject < Exception; end
|
5
|
+
class InvalidQueryObject < Exception; end
|
6
|
+
class InvalidDatabaseName < Exception; end
|
7
|
+
|
8
|
+
# The ConnectionHandler creates properly formed URIs and paths, while also
|
9
|
+
# specifying sensible defaults for CouchDB. Once initialized, parameters
|
10
|
+
# can be wrote and read using getter and setter syntax.
|
11
|
+
class ConnectionHandler
|
12
|
+
attr_accessor :scheme, :username, :password, :host, :port
|
13
|
+
attr_reader :database
|
14
|
+
|
15
|
+
# ConnectionHandler is constructed without any parameters, and with defaults
|
16
|
+
# for scheme, host and port. Other settings are set via the accessors above.
|
17
|
+
def initialize
|
18
|
+
@scheme = "http"
|
19
|
+
@host = "localhost"
|
20
|
+
@port = 5984
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets the database and ensures that it follows proper naming conventions.
|
24
|
+
def database=(database)
|
25
|
+
if database.match(/^[a-z0-9_$()+-\/]+$/)
|
26
|
+
@database = database
|
27
|
+
else
|
28
|
+
raise InvalidDatabaseName.new("only lowercase characters (a-z), digits (0-9), or _, $, (, ), +, - and / are allowed.")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a properly formed URI that can be used by a HTTP library.
|
33
|
+
def uri(path_obj = nil, query_obj = nil)
|
34
|
+
str = "#{@scheme}://#{@host}:#{@port}"
|
35
|
+
str += path(path_obj, query_obj)
|
36
|
+
str
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates a properly formed path that can be used by a HTTP library.
|
40
|
+
# `path_obj` can be an Array or NilClass, `query_obj` can be Hash or NilClass.
|
41
|
+
def path(path_obj = nil, query_obj = nil)
|
42
|
+
path_obj ||= []
|
43
|
+
query_obj ||= {}
|
44
|
+
|
45
|
+
path_str = if path_obj.is_a?(Array)
|
46
|
+
# If an Array, stringify and escape (unless it is a design document) each object and join with a "/"
|
47
|
+
([@database] + path_obj).map{|p| p.to_s.match(/_design\//) ? p.to_s : CGI.escape(p.to_s)}.join("/")
|
48
|
+
else
|
49
|
+
# Else, raise an error
|
50
|
+
raise InvalidPathObject.new("path must be of type 'Array' not of type '#{path_obj.class}'")
|
51
|
+
end
|
52
|
+
|
53
|
+
query_str = if query_obj.is_a?(Hash)
|
54
|
+
# If a Hash, stringify and escape each object, join each key/value with a "=" and each pair with a "&"
|
55
|
+
query_obj.to_a.map{|q| q.map{|r| CGI.escape(r.to_s)}.join("=")}.join("&")
|
56
|
+
else
|
57
|
+
# Else, raise an error
|
58
|
+
raise InvalidQueryObject.new("path must be of type 'Hash' or 'NilClass' not of type '#{query_obj.class}'")
|
59
|
+
end
|
60
|
+
|
61
|
+
str = "/" + path_str
|
62
|
+
str += "?" + query_str unless query_str.empty?
|
63
|
+
str
|
64
|
+
end
|
65
|
+
|
66
|
+
def inspect
|
67
|
+
"#<#{self.class}>"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module CouchClient
|
2
|
+
# ConsistentHash allows indifferent access with either with symbols or strings
|
3
|
+
# while also converting symbol values in Arrays or Hashes into strings values.
|
4
|
+
#
|
5
|
+
# ConsistentHash only overides methods that already exist in the Hash class;
|
6
|
+
# see the documentation in the Ruby core API for clarification and examples.
|
7
|
+
#
|
8
|
+
# This code was is heavily influenced by ActiveSupport::HashWithIndifferentAccess.
|
9
|
+
class ConsistentHash < Hash
|
10
|
+
# ConsistentHash is constructed with either a Hash or a default value. When
|
11
|
+
# a Hash is given, a new ConsistentHash is constructed with the same default
|
12
|
+
# value. When any other value is given it will be set as the default value.
|
13
|
+
def initialize(hash = {})
|
14
|
+
if hash.is_a?(Hash)
|
15
|
+
super()
|
16
|
+
update(hash).tap do |new_hash|
|
17
|
+
new_hash.default = hash.default
|
18
|
+
end
|
19
|
+
else
|
20
|
+
super(hash)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def default(key = nil)
|
25
|
+
if key.is_a?(Symbol) && include?(key = key.to_s)
|
26
|
+
self[key]
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
33
|
+
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
34
|
+
|
35
|
+
def []=(key, value)
|
36
|
+
regular_writer(convert_key(key), convert_value(value))
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(other_hash)
|
40
|
+
other_hash.each_pair do |key, value|
|
41
|
+
regular_writer(convert_key(key), convert_value(value))
|
42
|
+
end
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :merge!, :update
|
47
|
+
|
48
|
+
def key?(key)
|
49
|
+
super(convert_key(key))
|
50
|
+
end
|
51
|
+
|
52
|
+
alias_method :include?, :key?
|
53
|
+
alias_method :has_key?, :key?
|
54
|
+
alias_method :member?, :key?
|
55
|
+
|
56
|
+
def fetch(key, *extras)
|
57
|
+
super(convert_key(key), *extras)
|
58
|
+
end
|
59
|
+
|
60
|
+
def values_at(*indices)
|
61
|
+
indices.collect {|key| self[convert_key(key)]}
|
62
|
+
end
|
63
|
+
|
64
|
+
def dup
|
65
|
+
ConsistentHash.new(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def merge(hash)
|
69
|
+
dup.update(hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete(key)
|
73
|
+
super(convert_key(key))
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_hash
|
77
|
+
Hash.new(default).merge!(self)
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def convert_key(key)
|
83
|
+
key.is_a?(Symbol) ? key.to_s : key
|
84
|
+
end
|
85
|
+
|
86
|
+
def convert_value(value)
|
87
|
+
if value.instance_of?(Hash)
|
88
|
+
ConsistentHash.new(value)
|
89
|
+
elsif value.instance_of?(Symbol)
|
90
|
+
value.to_s
|
91
|
+
elsif value.instance_of?(Array)
|
92
|
+
value.collect{|e| convert_value(e)}
|
93
|
+
else
|
94
|
+
value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CouchClient
|
2
|
+
# The Database is just a organized collection of functions that interact with the
|
3
|
+
# CouchDB database such as stats, creation, compaction, replication and deletion.
|
4
|
+
class Database
|
5
|
+
# Database is constructed with a connection that is used to make HTTP requests to the server.
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def stats
|
11
|
+
@connection.hookup.get.last
|
12
|
+
end
|
13
|
+
|
14
|
+
def exists?
|
15
|
+
@connection.hookup.get.first == 200
|
16
|
+
end
|
17
|
+
|
18
|
+
def create
|
19
|
+
@connection.hookup.put.last
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete!
|
23
|
+
@connection.hookup.delete.last
|
24
|
+
end
|
25
|
+
|
26
|
+
def compact!
|
27
|
+
@connection.hookup.post(["_compact"]).last
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: add replicate method
|
31
|
+
def replicate
|
32
|
+
raise "pending"
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"#<#{self.class}>"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module CouchClient
|
2
|
+
class ViewNotFound < Exception; end
|
3
|
+
class ShowNotFound < Exception; end
|
4
|
+
class ListNotFound < Exception; end
|
5
|
+
class FullTextNotFound < Exception; end
|
6
|
+
|
7
|
+
# The Design is the interface used to interact with design documents
|
8
|
+
# in order make view, show, list and fulltext requests.
|
9
|
+
class Design
|
10
|
+
attr_accessor :id
|
11
|
+
|
12
|
+
# Design is constructed with an id of the design documemnt and
|
13
|
+
# a connection that is used to make HTTP requests to the server.
|
14
|
+
def initialize(id, connection)
|
15
|
+
@id = id
|
16
|
+
@connection = connection
|
17
|
+
end
|
18
|
+
|
19
|
+
# Makes requests to the server that return mappped/reduced view collections.
|
20
|
+
def view(name, options = {})
|
21
|
+
# key, startkey and endkey must be JSON encoded
|
22
|
+
["key", "startkey", "endkey"].each do |key|
|
23
|
+
options[key] &&= options[key].to_json
|
24
|
+
end
|
25
|
+
|
26
|
+
code, body = @connection.hookup.get(["_design", id, "_view", name], options)
|
27
|
+
|
28
|
+
case code
|
29
|
+
when 200
|
30
|
+
# Return a Collection if results were found
|
31
|
+
Collection.new(code, body, @connection)
|
32
|
+
when 404
|
33
|
+
# Raise an error if nothing was found
|
34
|
+
raise ViewNotFound.new("could not find view field '#{name}' for design '#{id}'")
|
35
|
+
else
|
36
|
+
# Also raise an error if something else happens
|
37
|
+
raise Error.new("code: #{code}, error: #{body["error"]}, reason: #{body["reason"]}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO: add show method
|
42
|
+
def show(name, options = {})
|
43
|
+
raise "pending"
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: add list method
|
47
|
+
def list(name, options = {})
|
48
|
+
raise "pending"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Makes requests to the server that return lucene search results.
|
52
|
+
def fulltext(name, options = {})
|
53
|
+
code, body = @connection.hookup.get(["_fti", "_design", id, name], options)
|
54
|
+
|
55
|
+
case code
|
56
|
+
when 200
|
57
|
+
if body["rows"]
|
58
|
+
# Return a serch result if a query was provided
|
59
|
+
Collection.new(code, body, self)
|
60
|
+
else
|
61
|
+
# Return a status hash if a query was not provided
|
62
|
+
body
|
63
|
+
end
|
64
|
+
else
|
65
|
+
if body["reason"] == "no_such_view"
|
66
|
+
# Raise an error if a fulltext function was not found
|
67
|
+
raise FullTextNotFound.new("could not find fulltext field '#{name}' for design '#{id}'")
|
68
|
+
else
|
69
|
+
# Also raise an error if something else happens
|
70
|
+
raise Error.new("code: #{code}, error: #{body["error"]}, reason: #{body["reason"]}")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def inspect
|
76
|
+
"#<#{self.class}: id: #{@id}>"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module CouchClient
|
2
|
+
class InvalidId < Exception; end
|
3
|
+
class AttachmentError < Exception; end
|
4
|
+
class DocumentNotAvailable < Exception; end
|
5
|
+
|
6
|
+
# The Document is an extended Hash that provides additional methods to
|
7
|
+
# save, update (with attachments), and delete documents on the CouchDB.
|
8
|
+
class Document < ConsistentHash
|
9
|
+
attr_reader :code, :error
|
10
|
+
|
11
|
+
# Document is constructed with a status code, response body,
|
12
|
+
# connection object and a flag stating if the document has been deleted.
|
13
|
+
def initialize(code, body, connection, deleted = false)
|
14
|
+
self.merge!(body)
|
15
|
+
|
16
|
+
@code = code
|
17
|
+
@error = {}
|
18
|
+
@connection = connection
|
19
|
+
@deleted = deleted
|
20
|
+
|
21
|
+
if self.attachments
|
22
|
+
self.attachments = AttachmentList.new(attachments)
|
23
|
+
|
24
|
+
self.attachments.keys.each do |key|
|
25
|
+
self.attachments[key] = Attachment.new(id, key, attachments[key], @connection)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Hookup#(id|id=|rev|rev=|attachments|attachments=) are convenience methods
|
31
|
+
# that correspond to ["_id"], ["_rev"] and ["_attachments"] document fields.
|
32
|
+
["id", "rev", "attachments"].each do |method|
|
33
|
+
define_method(method) do
|
34
|
+
self["_#{method}"]
|
35
|
+
end
|
36
|
+
|
37
|
+
define_method("#{method}=") do |value|
|
38
|
+
self["_#{method}"] = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a copy of the same document that is currently saved on the server.
|
43
|
+
def saved_doc(query = {})
|
44
|
+
if new?
|
45
|
+
raise DocumentNotAvailable.new('this document is new and therefore has not been saved yet')
|
46
|
+
else
|
47
|
+
@connection[self.id, query]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Tries to save the document to the server. If it us unable to,
|
52
|
+
# it will save the error and make it available with via #error.
|
53
|
+
def save
|
54
|
+
# Ensure that "_id" is a String if it is defined.
|
55
|
+
if self.key?("_id") && !self["_id"].is_a?(String)
|
56
|
+
raise InvalidId.new("document _id must be a String")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Documents without an id must use post, with an id must use put
|
60
|
+
@code, body = if self.id
|
61
|
+
@connection.hookup.put([self.id], nil, self)
|
62
|
+
else
|
63
|
+
@connection.hookup.post(nil, nil, self)
|
64
|
+
end
|
65
|
+
|
66
|
+
# If the document was saved
|
67
|
+
if body["ok"]
|
68
|
+
# Update id and rev, ensure the deleted flag is set to `false` and return `true`
|
69
|
+
self.id ||= body["id"]
|
70
|
+
self.rev = body["rev"]
|
71
|
+
@deleted = false
|
72
|
+
true
|
73
|
+
else
|
74
|
+
# Save error message and return `false`.
|
75
|
+
@error = {body["error"] => body["reason"]}
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Tries to attach a file to the document. If it us unable to,
|
81
|
+
# it will save the error and make it available with via #error.
|
82
|
+
def attach(name, content, content_type)
|
83
|
+
# The document must already be saved to the server before a file can be attached.
|
84
|
+
if self.rev
|
85
|
+
@code, body = @connection.hookup.put([self.id, name], {"rev" => self.rev}, content, content_type)
|
86
|
+
|
87
|
+
# If the document was saved
|
88
|
+
if body["ok"]
|
89
|
+
# Update rev and return `true`
|
90
|
+
self.rev = body["rev"]
|
91
|
+
true
|
92
|
+
else
|
93
|
+
# Save error message and return `false`.
|
94
|
+
@error = {body["error"] => body["reason"]}
|
95
|
+
false
|
96
|
+
end
|
97
|
+
else
|
98
|
+
# Raise an error if the document is new before trying to attach a file.
|
99
|
+
raise AttachmentError.new("a document must exist before an attachment can be uploaded to it")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Tries to delete a file from the server. If it us unable to,
|
104
|
+
# it will save the error and make it available with via #error.
|
105
|
+
def delete!
|
106
|
+
@code, body = @connection.hookup.delete([id], {"rev" => rev})
|
107
|
+
|
108
|
+
# If the document was deleted
|
109
|
+
if body["ok"]
|
110
|
+
# Update the rev, set the deleted flag to `true` and return `true`
|
111
|
+
self.rev = body["rev"]
|
112
|
+
@deleted = true
|
113
|
+
true
|
114
|
+
else
|
115
|
+
# Save error message and return `false`.
|
116
|
+
@error = {body["error"] => body["reason"]}
|
117
|
+
false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Identifies the document as a design document
|
122
|
+
def design?
|
123
|
+
!!self.id.match(/_design\//) # Design documents start with "_design/"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Identifies the document as not yet being saved to the server
|
127
|
+
def new?
|
128
|
+
!rev
|
129
|
+
end
|
130
|
+
|
131
|
+
# Identifies that there are currently errors that must be resolved
|
132
|
+
def error?
|
133
|
+
!@error.empty?
|
134
|
+
end
|
135
|
+
|
136
|
+
# Identifies that the document does not yet pass a `validate_on_update` function
|
137
|
+
def invalid?
|
138
|
+
@code == 403 && @error["forbidden"]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Identifies that the document cannot be saved due to conflicts
|
142
|
+
def conflict?
|
143
|
+
!!@error["conflict"]
|
144
|
+
end
|
145
|
+
|
146
|
+
# Identifies that the document has been deleted
|
147
|
+
def deleted?
|
148
|
+
@deleted
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'curb'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module CouchClient
|
5
|
+
class InvalidHTTPVerb < Exception; end
|
6
|
+
class InvalidJSONData < Exception; end
|
7
|
+
class SymbolUsedInField < Exception; end
|
8
|
+
|
9
|
+
# The Hookup is the basic HTTP interface that connects CouchClient to CouchDB.
|
10
|
+
# Hookup can use any HTTP library if the conventions listed below are followed.
|
11
|
+
#
|
12
|
+
# If modified, Hookup must have head, get, post, put and delete instance methods.
|
13
|
+
class Hookup
|
14
|
+
attr_reader :handler
|
15
|
+
|
16
|
+
# Hookup is constructed with a connection handler, which formats the
|
17
|
+
# proper URIs with a domain, authentication, path and query string.
|
18
|
+
def initialize(handler)
|
19
|
+
@handler = handler
|
20
|
+
end
|
21
|
+
|
22
|
+
# Hookup#(head|get|delete) has the following method signature
|
23
|
+
# hookup.verb(["path", "name"], {"query_key" => "query_value"}, "content/type")
|
24
|
+
#
|
25
|
+
# And has the following response
|
26
|
+
# [code, {"data_key" => "data_value"}]
|
27
|
+
#
|
28
|
+
# Except, if the verb is `head`, which has the following response
|
29
|
+
# [code, nil]
|
30
|
+
#
|
31
|
+
# Or, if the verb is `get` and content_type is not "application/json", which has the following response
|
32
|
+
# [code, "string containing file data"]
|
33
|
+
#
|
34
|
+
# By default path is nil, query is nil, and content_type is "application/json"
|
35
|
+
[:head, :get, :delete].each do |verb|
|
36
|
+
define_method(verb) do |*args|
|
37
|
+
# These methods do not have a body parameter. As such, the following
|
38
|
+
# prevents setting content_type to nil instead not setting it at all.
|
39
|
+
params = [verb, args.shift, args.shift, nil]
|
40
|
+
params << args.shift unless args.empty?
|
41
|
+
curl(*params)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Hookup#(post|put) has the following method signature
|
46
|
+
# hookup.verb(["path", "name"], {"query_key" => "query_value"}, {"data_key" => "data_value"}, "content/type")
|
47
|
+
#
|
48
|
+
# And has the following response
|
49
|
+
# [code, {"data_key" => "data_value"}]
|
50
|
+
#
|
51
|
+
# By default path is nil, query is nil, data is {} and content_type is "application/json"
|
52
|
+
[:post, :put].each do |verb|
|
53
|
+
define_method(verb) do |*args|
|
54
|
+
curl(verb, *args)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def inspect
|
59
|
+
"#<#{self.class}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# This is the method that actually makes curl request for each verb method listed above.
|
65
|
+
def curl(verb, path = nil, query = nil, data = {}, content_type = "application/json")
|
66
|
+
# Setup curb options block
|
67
|
+
options = lambda do |easy|
|
68
|
+
easy.headers["User-Agent"] = "couch-client v#{VERSION}"
|
69
|
+
easy.headers["Content-Type"] = content_type if content_type
|
70
|
+
easy.headers["Accepts"] = content_type if content_type
|
71
|
+
easy.username = handler.username
|
72
|
+
easy.userpwd = handler.password
|
73
|
+
end
|
74
|
+
|
75
|
+
easy = case verb
|
76
|
+
when :head, :get, :delete
|
77
|
+
# head, get and delete http methods only take a uri string and options block
|
78
|
+
Curl::Easy.send("http_#{verb}", handler.uri(path, query), &options)
|
79
|
+
when :post, :put
|
80
|
+
# post and put http methods take a uri string, data string and options block
|
81
|
+
# also convert the hash into json if the content_type of the request is json
|
82
|
+
data = data.to_json if content_type == "application/json"
|
83
|
+
Curl::Easy.send("http_#{verb}", handler.uri(path, query), data, &options)
|
84
|
+
else
|
85
|
+
raise InvalidHTTPVerb.new("only `head`, `get`, `post`, `put` and `delete` are supported")
|
86
|
+
end
|
87
|
+
|
88
|
+
# code is the http code (e.g. 200 or 404)
|
89
|
+
code = easy.response_code
|
90
|
+
|
91
|
+
# body is either a nil, a hash or a string containing attachment data
|
92
|
+
body = if easy.body_str == "" || easy.body_str.nil?
|
93
|
+
nil
|
94
|
+
elsif content_type == "application/json" || [:post, :put, :delete].include?(verb)
|
95
|
+
begin
|
96
|
+
JSON.parse(easy.body_str)
|
97
|
+
rescue
|
98
|
+
raise InvalidJSONData.new("document received is not valid JSON")
|
99
|
+
end
|
100
|
+
else
|
101
|
+
easy.body_str
|
102
|
+
end
|
103
|
+
|
104
|
+
[code, body]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|