akdubya-cushion 0.5.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.
@@ -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