akdubya-cushion 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ module Cushion
2
+ class Response < Hash
3
+ def initialize(keys = {})
4
+ keys.each do |k,v|
5
+ self[k.to_s] = v
6
+ end
7
+ end
8
+
9
+ def []=(key, value)
10
+ super(key.to_s, value)
11
+ end
12
+
13
+ def [](key)
14
+ super(key.to_s)
15
+ end
16
+ end
17
+
18
+ class Document < Response
19
+ attr_accessor :database
20
+
21
+ class << self
22
+ def use_database(db)
23
+ @database = db
24
+ end
25
+
26
+ def database
27
+ @database
28
+ end
29
+ end
30
+
31
+ def id
32
+ self[:_id]
33
+ end
34
+
35
+ def rev
36
+ self[:_rev]
37
+ end
38
+
39
+ # Returns true if this document has never been saved
40
+ def new_document?
41
+ !rev
42
+ end
43
+
44
+ # Saves this document to the database.
45
+ def save
46
+ result = database.save_doc(self)
47
+ if result['ok']
48
+ self[:_rev] = result['rev']
49
+ self[:_id] = result['id']
50
+ end
51
+ result['ok']
52
+ end
53
+
54
+ # Deletes this document from the database.
55
+ def destroy
56
+ result = database.delete_doc(id, rev)
57
+ if result['ok']
58
+ self[:_rev] = nil
59
+ self[:_id] = nil
60
+ end
61
+ result['ok']
62
+ end
63
+
64
+ # Reloads this document from the database.
65
+ def reload
66
+ result = database.open_doc(id)
67
+ initialize(result)
68
+ self
69
+ end
70
+
71
+ # Copies the document to a new id. If +dest_id+ currently exists then
72
+ # +dest_rev+ must be provided.
73
+ def copy(dest_id, dest_rev = nil)
74
+ result = database.copy_doc(id, dest_id, :dest_rev => dest_rev)
75
+ result['ok']
76
+ end
77
+
78
+ # Moves the document to a new id. If +dest_id+ currently exists then
79
+ # +dest_rev+ must be provided.
80
+ def move(dest_id, dest_rev = nil)
81
+ result = database.move_doc(id, dest_id, rev, :dest_rev => dest_rev)
82
+ result['ok']
83
+ end
84
+
85
+ # Returns this document's database.
86
+ def database
87
+ @database || self.class.database
88
+ end
89
+
90
+ # Opens the attachment identified by +filename+. Returns an IO object.
91
+ def open_attachment(filename)
92
+ database.open_attachment(id, filename)
93
+ end
94
+
95
+ # Attaches a file to this document under +filename+.
96
+ def save_attachment(filename, data, options={})
97
+ result = database.save_attachment(id, rev, filename, data, options)
98
+ self[:_rev] = result['rev']
99
+ result['ok']
100
+ end
101
+
102
+ # Deletes the attachment identified by +filename+.
103
+ def delete_attachment(filename)
104
+ result = database.delete_attachment(id, rev, filename)
105
+ self[:_rev] = result['rev']
106
+ result['ok']
107
+ end
108
+
109
+ # Renames an attachment. +oldname+ is the name of the existing attachment
110
+ # and +newname+ is the desired name.
111
+ def rename_attachment(oldname, newname)
112
+ result = database.rename_attachment(id, rev, oldname, newname)
113
+ self[:_rev] = result['rev']
114
+ result['ok']
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,208 @@
1
+ require 'open-uri'
2
+
3
+ module Cushion
4
+ class Server
5
+ attr_accessor :uri, :uuid_batch_count
6
+
7
+ # Initialize a Cushion::Server to the uri at +server+.
8
+ def initialize(server = DEFAULT_COUCH_HOST, uuid_batch_count = 1000)
9
+ @uri = server
10
+ @uuid_batch_count = uuid_batch_count
11
+ end
12
+
13
+ # Retrieves the server's welcome message.
14
+ def info(headers = {})
15
+ get('', headers)
16
+ end
17
+
18
+ # Retrieves a list of running tasks on the server.
19
+ def active_tasks(headers = {})
20
+ get("_active_tasks", headers)
21
+ end
22
+
23
+ # Retrieves a list of all databases on the server.
24
+ def all_dbs(headers = {})
25
+ get("_all_dbs", headers)
26
+ end
27
+
28
+ # Fetches the configuration information for this server.
29
+ def config(headers = {})
30
+ get("_config", headers)
31
+ end
32
+
33
+ # Sets a configuration option identified by +section+ and +option+.
34
+ def set_config(section, option, value)
35
+ put("_config/#{section}/#{option}", value, :accept => "text/plain")
36
+ end
37
+
38
+ # Replicates +source+ database to +target+ database. +source+ and +target+
39
+ # may be either local database names (for local replication) or fully
40
+ # qualified urls (for remote replication).
41
+ def replicate(source, target, headers = {})
42
+ post("_replicate", {:source => source.to_s, :target => target.to_s}, headers)
43
+ end
44
+
45
+ # Restarts the server.
46
+ def restart(headers = {})
47
+ post("_restart", nil, headers)
48
+ end
49
+
50
+ # Fetches stats information for this server.
51
+ def stats(headers = {})
52
+ get("_stats", headers)
53
+ end
54
+
55
+ # Creates a database on this server with the provided +name+.
56
+ def create(name, headers = {})
57
+ db(name).create(headers)
58
+ end
59
+
60
+ # Deletes the database identified by +name+ from this server.
61
+ def delete(name, headers = {})
62
+ db(name).delete(headers)
63
+ end
64
+
65
+ # Deletes and re-creates the database identified by +name+.
66
+ # Returns a Cushion::Database.
67
+ def recreate(name)
68
+ rdb = db(name)
69
+ begin
70
+ rdb.delete
71
+ rescue RestClient::ResourceNotFound
72
+ nil
73
+ end
74
+ rdb.create
75
+ rdb
76
+ end
77
+
78
+ # Returns a Cushion::Database identified by +name+. This method does
79
+ # not attempt to create the database if it does not already exist on the
80
+ # server.
81
+ def [](name)
82
+ db(name)
83
+ end
84
+
85
+ # Returns a Cushion::Database identified by +name+. This method does
86
+ # not attempt to create the database if it does not already exist on the
87
+ # server.
88
+ def db(name)
89
+ Cushion::Database.new(self, name)
90
+ end
91
+ alias_method :database, :db
92
+
93
+ # Creates a database on this server with the provided +name+ if it doesn't
94
+ # already exist. Returns a Cushion::Database.
95
+ def db!(name)
96
+ ndb = db(name)
97
+ ndb.create rescue nil
98
+ ndb
99
+ end
100
+ alias_method :database!, :db!
101
+
102
+ # Issues a HEAD request to the CouchDB server.
103
+ def head(path, headers = {})
104
+ RestClient.head("#{@uri}/#{path}", headers).headers
105
+ end
106
+
107
+ # Issues a GET request to the CouchDB server. Returns a parsed response body
108
+ # if the +accept+ options is set to 'application/json' (the default), otherwise
109
+ # returns the raw RestClient response body.
110
+ def get(path, headers = {})
111
+ defaults = { :accept => "application/json" }
112
+ parse_response(RestClient.get("#{@uri}/#{path}", defaults.merge(headers)))
113
+ end
114
+
115
+ # Issues a POST request to the CouchDB server. Parses the request body if
116
+ # the +content_type+ option is set to 'application/json' (the default).
117
+ # Returns a parsed response body if the +accept+ options is set to
118
+ # 'application/json' (the default), otherwise returns the raw
119
+ # RestClient response body.
120
+ def post(path, body, headers = {})
121
+ defaults = { :accept => "application/json", :content_type => "application/json" }
122
+ opts = defaults.merge(headers)
123
+ parse_response(RestClient.post("#{@uri}/#{path}", construct_payload(body, opts), opts))
124
+ end
125
+
126
+ # Issues a PUT request to the CouchDB server. Parses the request body if
127
+ # the +content_type+ option is set to 'application/json' (the default).
128
+ # Returns a parsed response body if the +accept+ option is set to
129
+ # 'application/json' (the default), otherwise returns the raw
130
+ # RestClient response body.
131
+ def put(path, body, headers = {})
132
+ defaults = { :accept => "application/json", :content_type => "application/json" }
133
+ opts = defaults.merge(headers)
134
+ parse_response(RestClient.put("#{@uri}/#{path}", construct_payload(body, opts), opts))
135
+ end
136
+
137
+ # Issues a DELETE request to the CouchDB server. Returns a parsed response body
138
+ # if the +accept+ options is set to 'application/json' (the default), otherwise
139
+ # returns the raw RestClient response body.
140
+ def delete(path, headers = {})
141
+ defaults = { :accept => "application/json" }
142
+ parse_response(RestClient.delete("#{@uri}/#{path}", defaults.merge(headers)))
143
+ end
144
+
145
+ # Issues a COPY request to the CouchDB server, copying a document from +source+
146
+ # to +destination+. Returns a parsed response body if the +accept+ options
147
+ # is set to 'application/json' (the default), otherwise returns the raw
148
+ # RestClient response body.
149
+ def copy(source, destination, headers = {})
150
+ defaults = { :accept => "application/json", 'Destination' => destination }
151
+ parse_response(RestClient.copy("#{@uri}/#{source}", defaults.merge(headers)))
152
+ end
153
+
154
+ # Issues a MOVE request to the CouchDB server, moving a document from +source+
155
+ # to +destination+. Returns a parsed response body if the +accept+ options
156
+ # is set to 'application/json' (the default), otherwise returns the raw
157
+ # RestClient response body.
158
+ def move(source, destination, headers = {})
159
+ defaults = { :accept => "application/json", 'Destination' => destination }
160
+ parse_response(RestClient.move("#{@uri}/#{source}", defaults.merge(headers)))
161
+ end
162
+
163
+ def request(verb, path, params)
164
+ RestClient::Request.execute(
165
+ :method => verb,
166
+ :url => "#{@uri}/#{path}",
167
+ :payload => params[:body],
168
+ :headers => params[:headers]
169
+ )
170
+ end
171
+
172
+ # Opens the attachment located at +path+. Returns a Tempfile.
173
+ def open_attachment(path)
174
+ open("#{@uri}/#{path}")
175
+ rescue OpenURI::HTTPError
176
+ raise RestClient::ResourceNotFound
177
+ end
178
+
179
+ # Retrives the next unused UUID from CouchDB. Provide a +count+ to set the number
180
+ # of UUIDs cached by this server instance.
181
+ def next_uuid(count = @uuid_batch_count)
182
+ @uuids ||= []
183
+ if @uuids.empty?
184
+ @uuids = get("_uuids?count=#{count}")["uuids"]
185
+ end
186
+ @uuids.pop
187
+ end
188
+
189
+ private
190
+
191
+ def parse_response(result)
192
+ if result.headers[:content_type] == "application/json"
193
+ JSON.parse(result)
194
+ else
195
+ result
196
+ end
197
+ end
198
+
199
+ def construct_payload(body, opts)
200
+ return nil unless body
201
+ if opts[:content_type] == "application/json"
202
+ body.to_json
203
+ else
204
+ body
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,9 @@
1
+ require File.dirname(__FILE__) + '/helpers'
2
+
3
+ describe Cushion do
4
+
5
+ it "should instantiate a Cushion::Server" do
6
+ Cushion.new.should.be.a.kind_of Cushion::Server
7
+ end
8
+
9
+ end
@@ -0,0 +1,223 @@
1
+ require File.dirname(__FILE__) + '/helpers'
2
+
3
+ describe "Cushion::Database" do
4
+
5
+ before do
6
+ @db = Cushion.db!('http://127.0.0.1:5984/cushion_test')
7
+ end
8
+
9
+ after do
10
+ @db.delete rescue nil
11
+ end
12
+
13
+ describe "Operations" do
14
+
15
+ it "should properly initialize a database instance" do
16
+ badname = Cushion::Database.new(Cushion.new, "my/database")
17
+ badname.name.should == "my%2Fdatabase"
18
+ lambda { Cushion::Database.new( :foo, "mydb" ) }.should.raise ArgumentError
19
+ end
20
+
21
+ it "should create and delete a database" do
22
+ newdb = Cushion.db('http://127.0.0.1:5984/foo_test')
23
+ newdb.create["ok"].should.be.true
24
+ newdb.delete["ok"].should.be.true
25
+ end
26
+
27
+ it "should retrieve database info" do
28
+ @db.info["db_name"].should == "cushion_test"
29
+ end
30
+
31
+ it "should compact a database" do
32
+ @db.save_doc("foo" => "bar")
33
+ @db.compact["ok"].should.be.true
34
+ # Finish compaction before moving on
35
+ while @db.server.active_tasks != [] do
36
+ sleep(0.5)
37
+ end
38
+ end
39
+
40
+ it "should pass requests to external handlers" do
41
+ lambda { @db.external(:get, "stats") }.should.not.raise
42
+ end
43
+
44
+ end
45
+
46
+ describe "Saving" do
47
+
48
+ it "should save a document" do
49
+ @db.save_doc("_id" => "123")["ok"].should.be.true
50
+ end
51
+
52
+ it "should automatically generate an id if none provided on save" do
53
+ saved = @db.save_doc("foo" => "bar")
54
+ saved["ok"].should.be.true
55
+ saved["rev"].should.not.be.nil
56
+ end
57
+
58
+ it "should bulk save documents" do
59
+ count = @db.info["doc_count"]
60
+ @db.bulk_docs([{"_id" => "456"},{"_id" => "789"}])["ok"].should.be.true
61
+ @db.info["doc_count"].should == count + 2
62
+ end
63
+
64
+ end
65
+
66
+ describe "Deleting" do
67
+
68
+ before do
69
+ @db.save_doc("_id" => "123")
70
+ @db.save_doc("_id" => "456")
71
+ end
72
+
73
+ it "should delete a document" do
74
+ doc = @db.open_doc("123")
75
+ @db.delete_doc("123", doc['_rev'])["ok"].should.be.true
76
+ end
77
+
78
+ it "should bulk delete documents" do
79
+ count = @db.info["doc_count"]
80
+ docs = @db.all_docs(:keys => ["123", "456"], :include_docs => true)
81
+ @db.bulk_delete(docs["rows"].map{ |row| row["doc"] })["ok"].should.be.true
82
+ @db.info["doc_count"].should == count - 2
83
+ end
84
+
85
+ end
86
+
87
+ describe "Copying" do
88
+
89
+ before do
90
+ @abc = @db.save_doc("_id" => "abc")
91
+ @ghi = @db.save_doc("_id" => "ghi")
92
+ end
93
+
94
+ it "should copy an existing doc to a new/existing doc" do
95
+ res = @db.copy_doc("abc", "def")
96
+ res["id"].should == "def"
97
+ @db.copy_doc("abc", "def", :dest_rev => res["rev"])["ok"].should.be.true
98
+ end
99
+
100
+ it "should move an existing doc to a new/existing doc" do
101
+ @db.move_doc("abc", "def", @abc["rev"])["ok"].should.be.true
102
+ lambda { @db.open_doc("abc") }.should.raise RestClient::ResourceNotFound
103
+ doc = @db.open_doc("def")
104
+ @db.move_doc("def", "ghi", doc["_rev"], :dest_rev => @ghi["rev"])["ok"].should.be.true
105
+ end
106
+
107
+ end
108
+
109
+ describe "Reading" do
110
+
111
+ before do
112
+ @db.save_doc("_id" => "123")
113
+ @db.save_doc("_id" => "456")
114
+ end
115
+
116
+ it "should open a document" do
117
+ @db.open_doc("123")["_id"].should == "123"
118
+ end
119
+
120
+ it "should load a Cushion::Document" do
121
+ @db.doc("123").should.be.kind_of Cushion::Document
122
+ end
123
+
124
+ it "should retrieve all documents" do
125
+ @db.all_docs["total_rows"].should == 2
126
+ end
127
+
128
+ it "should retrieve all documents by keys" do
129
+ @db.all_docs(:keys => ["123"])["rows"].length.should == 1
130
+ end
131
+
132
+ end
133
+
134
+ describe "Views" do
135
+
136
+ before do
137
+ @db.save_doc("_id" => "gomer", "name" => "gomer pyle")
138
+ @db.save_doc("_id" => "vince", "name" => "vince carter")
139
+ @view = {:map => "function(doc){emit(doc.name,null)}"}
140
+ @db.save_doc("_id" => "_design/foo", "views" => { "bar" => @view })
141
+ end
142
+
143
+ it "should retrieve docs from temp views" do
144
+ @db.temp_view(@view, :key => "gomer pyle")["rows"].length.should == 1
145
+ end
146
+
147
+ it "should retrieve docs from temp views by keys" do
148
+ @db.temp_view(@view, :keys => ["vince carter"])["rows"].length.should == 1
149
+ end
150
+
151
+ it "should retrieve docs from regular views" do
152
+ @db.view("foo/bar", :key => "gomer pyle")["rows"].length.should == 1
153
+ end
154
+
155
+ it "should retrieve docs from regular views by keys" do
156
+ @db.view("foo/bar", :keys => ["vince carter"])["rows"].length.should == 1
157
+ end
158
+
159
+ it "should retrieve a doc from a show function" do
160
+ design = @db.open_doc("_design/foo")
161
+ design["shows"] = { "people" => "function(doc, req) { var response = {'code':200, 'headers':{}, 'body':'Hello World'}; return response; }" }
162
+ @db.save_doc(design)
163
+ @db.show("foo", "people", "gomer").should == "Hello World"
164
+ end
165
+
166
+ it "should retrieve docs from a list function" do
167
+ design = @db.open_doc("_design/foo")
168
+ design["lists"] = { "people" => "function(head, row, req, row_info) { var response = {'code':200, 'headers':{}, 'body':'x'}; return response; }" }
169
+ @db.save_doc(design)
170
+ @db.list("foo", "people", "bar").should == "xxxx"
171
+ end
172
+
173
+ end
174
+
175
+ describe "Attachments" do
176
+
177
+ it "should save and open inline attachments" do
178
+ @db.save_doc("_id" => "inline", "_attachments" => {
179
+ "foo.txt" => {
180
+ "content_type" => "text/plain",
181
+ "data" => "Hello World!"
182
+ }
183
+ })["ok"].should.be.true
184
+
185
+ @db.open_attachment("inline", "foo.txt").read.should == "Hello World!"
186
+ end
187
+
188
+ it "should save and open standalone attachments" do
189
+ res = @db.save_doc("_id" => "attachable")
190
+ @db.save_attachment(res['id'], res['rev'],
191
+ "foo.txt", "Hello World!",
192
+ :content_type => "text/plain"
193
+ )["ok"].should.be.true
194
+
195
+ @db.open_attachment("attachable", "foo.txt").read.should == "Hello World!"
196
+ end
197
+
198
+ it "should delete attachments" do
199
+ res = @db.save_doc("_id" => "deleteme", "_attachments" => {
200
+ "foo.txt" => {
201
+ "content_type" => "text/plain",
202
+ "data" => "Hello World!"
203
+ }
204
+ })
205
+
206
+ @db.delete_attachment("deleteme", res["rev"], "foo.txt")["ok"].should.be.true
207
+ end
208
+
209
+ it "should rename attachments" do
210
+ res = @db.save_doc("_id" => "rename", "_attachments" => {
211
+ "foo.txt" => {
212
+ "content_type" => "text/plain",
213
+ "data" => "Hello World!"
214
+ }
215
+ })
216
+
217
+ @db.rename_attachment("rename", res["rev"], "foo.txt", "bar.txt")["ok"].should.be.true
218
+ @db.open_doc("rename")["_attachments"]["bar.txt"].should.not.be.nil
219
+ end
220
+
221
+ end
222
+
223
+ end