couch-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,11 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
|
2
|
+
|
3
|
+
describe CouchClient do
|
4
|
+
it 'should exist' do
|
5
|
+
CouchClient.should be_a(Module)
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should have a .connect method that constructs a new CouchClient::Connection object' do
|
9
|
+
CouchClient.connect(:database => "sandbox").should be_a(CouchClient::Connection)
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
|
2
|
+
|
3
|
+
describe CouchClient::Database do
|
4
|
+
before(:all) do
|
5
|
+
@couch = CouchClient.connect(COUCHDB_TEST_SETTINGS)
|
6
|
+
@couch.database.create
|
7
|
+
end
|
8
|
+
|
9
|
+
after(:all) do
|
10
|
+
@couch.database.delete!
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#stats' do
|
14
|
+
it 'should exist' do
|
15
|
+
@couch.database.stats.should be_a(Hash)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#exists?' do
|
20
|
+
it 'should exist' do
|
21
|
+
@couch.database.exists?.should be_a(TrueClass)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#create' do
|
26
|
+
# Is already tested as it is used in the before(:all) setup
|
27
|
+
# to make the database that is currently being tested.
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#delete' do
|
31
|
+
# Is already tested as it is used in the after(:all) teardown
|
32
|
+
# to delete the database that is currently being tested.
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#compact!' do
|
36
|
+
it 'should exist' do
|
37
|
+
@couch.database.compact!.should be_a(Hash)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#replicate' do
|
42
|
+
pending 'will be built in another release'
|
43
|
+
end
|
44
|
+
end
|
data/spec/design_spec.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
|
2
|
+
|
3
|
+
describe CouchClient::Connection do
|
4
|
+
before(:all) do
|
5
|
+
@couch = CouchClient.connect(COUCHDB_TEST_SETTINGS)
|
6
|
+
@couch.database.create
|
7
|
+
|
8
|
+
factory = lambda do |hash|
|
9
|
+
doc = @couch.build(hash)
|
10
|
+
doc.save
|
11
|
+
doc
|
12
|
+
end
|
13
|
+
|
14
|
+
@alice = factory.call({"_id" => "123", "name" => "alice", "city" => "nyc"})
|
15
|
+
@bob = factory.call({"_id" => "456", "name" => "bob", "city" => "chicago"})
|
16
|
+
@charlie = factory.call({"_id" => "789", "name" => "charlie", "city" => "san fran"})
|
17
|
+
@design = factory.call({"_id" => "_design/people",
|
18
|
+
"views" => {
|
19
|
+
"all" => {"map" => "function(doc){emit(doc._id, doc)}"},
|
20
|
+
"sum" => {"map" => "function(doc){emit(null, 1)}", "reduce" => "function(id, values, rereduce){return sum(values)}"},
|
21
|
+
},
|
22
|
+
"fulltext" => {
|
23
|
+
"by_name" => {
|
24
|
+
"index" => "function(doc){var ret = new Document();ret.add(doc.name);return ret;}"
|
25
|
+
}
|
26
|
+
}
|
27
|
+
})
|
28
|
+
|
29
|
+
@people = @couch.design("people")
|
30
|
+
end
|
31
|
+
|
32
|
+
after(:all) do
|
33
|
+
@couch.database.delete!
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should have an id' do
|
37
|
+
@people.id.should eql("people")
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#view' do
|
41
|
+
it 'should return a mapped collection if the view exists' do
|
42
|
+
view = @people.view("all", "include_docs" => true)
|
43
|
+
view.should be_a(CouchClient::Collection)
|
44
|
+
view.info.should be_a(Hash)
|
45
|
+
view.size.should eql(3)
|
46
|
+
view.first.keys.should eql(["id", "key", "value", "doc"])
|
47
|
+
view.first["id"].should eql(@alice.id)
|
48
|
+
view.last["id"].should eql(@charlie.id)
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should return mapped results based on "key"' do
|
52
|
+
view = @people.view("all", "key" => @bob.id)
|
53
|
+
view.size.should eql(1)
|
54
|
+
view.first["id"].should eql(@bob.id)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should return mapped results based on "startkey"' do
|
58
|
+
view = @people.view("all", "startkey" => @bob.id)
|
59
|
+
view.size.should eql(2)
|
60
|
+
view.first["id"].should eql(@bob.id)
|
61
|
+
view.last["id"].should eql(@charlie.id)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should return mapped results based on "endkey"' do
|
65
|
+
view = @people.view("all", "endkey" => @bob.id)
|
66
|
+
view.size.should eql(2)
|
67
|
+
view.first["id"].should eql(@alice.id)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should return a mapped and reduced collection if the view exists' do
|
71
|
+
view = @people.view("sum", "group" => true)
|
72
|
+
view.should be_a(CouchClient::Collection)
|
73
|
+
view.info.should be_a(Hash)
|
74
|
+
view.size.should eql(1)
|
75
|
+
view.first.keys.should eql(["key", "value"])
|
76
|
+
view.first["value"].should eql(3)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#show' do
|
81
|
+
pending 'will be built in another release'
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#list' do
|
85
|
+
pending 'will be built in another release'
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#fulltext' do
|
89
|
+
it 'should return a lucine status hash if the fulltext exists' do
|
90
|
+
@people.fulltext("by_name").should be_a(Hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return a search results collection if the fulltext exists and a query is given' do
|
94
|
+
fulltext = @people.fulltext("by_name", "q" => "al*")
|
95
|
+
fulltext.should be_a(CouchClient::Collection)
|
96
|
+
fulltext.info.should be_a(Hash)
|
97
|
+
fulltext.first["id"].should eql(@alice.id)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
|
2
|
+
require 'digest/sha1'
|
3
|
+
|
4
|
+
describe CouchClient::Document do
|
5
|
+
before(:each) do
|
6
|
+
@couch = CouchClient.connect(COUCHDB_TEST_SETTINGS)
|
7
|
+
@couch.database.create
|
8
|
+
|
9
|
+
factory = lambda do |hash|
|
10
|
+
doc = @couch.build(hash)
|
11
|
+
doc.save
|
12
|
+
doc
|
13
|
+
end
|
14
|
+
|
15
|
+
@alice = factory.call({"name" => "alice", "city" => "nyc"})
|
16
|
+
@bob = factory.call({"name" => "bob", "city" => "chicago"})
|
17
|
+
@new = @couch.build
|
18
|
+
@design = factory.call({"_id" => "_design/people", "views" => {"all" => {"map" => "function(doc){emit(doc._id, doc)}"}}})
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:each) do
|
22
|
+
@couch.database.delete!
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should have #id, #rev and #attachments methods' do
|
26
|
+
@alice.id.should eql(@alice["_id"])
|
27
|
+
@alice.rev.should eql(@alice["_rev"])
|
28
|
+
@alice.attachments.should eql(@alice["_attachments"])
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should have #id=, #rev= and #attachments= methods' do
|
32
|
+
@alice.should respond_to(:id=)
|
33
|
+
@alice.should respond_to(:rev=)
|
34
|
+
@alice.should respond_to(:attachments=)
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#saved_doc' do
|
38
|
+
it 'should get the doc from the database' do
|
39
|
+
@alice["key"] = "value"
|
40
|
+
@alice.saved_doc.should_not eql(@alice)
|
41
|
+
|
42
|
+
@alice["key"].should eql("value")
|
43
|
+
@alice.saved_doc["key"].should be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should return nothing if the doc is new' do
|
47
|
+
lambda{@new.saved_doc}.should raise_error(CouchClient::DocumentNotAvailable)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#save' do
|
52
|
+
it 'should save a new document' do
|
53
|
+
@new.new?.should be_true
|
54
|
+
@new.rev.should be_nil
|
55
|
+
|
56
|
+
@new.save
|
57
|
+
|
58
|
+
@new.new?.should be_false
|
59
|
+
@new.rev.should_not be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should update an existing document' do
|
63
|
+
@alice["key"].should be_nil
|
64
|
+
@alice["key"] = "value"
|
65
|
+
|
66
|
+
@alice.save
|
67
|
+
|
68
|
+
@alice["key"].should eql("value")
|
69
|
+
@alice.saved_doc["key"].should eql("value")
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should not save a document in conflict' do
|
73
|
+
@alice_old = @alice.saved_doc
|
74
|
+
@alice_old["old"] = true
|
75
|
+
@alice["key"] = "value"
|
76
|
+
|
77
|
+
@alice.save
|
78
|
+
|
79
|
+
@alice_old.save
|
80
|
+
@alice_old.error?.should be_true
|
81
|
+
@alice_old.conflict?.should be_true
|
82
|
+
@alice_old.error.should eql({"conflict"=>"Document update conflict."})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#attach' do
|
87
|
+
before(:all) do
|
88
|
+
@read = lambda do |file|
|
89
|
+
File.read(File.join(File.dirname(__FILE__), "files", file))
|
90
|
+
end
|
91
|
+
|
92
|
+
@digest = lambda do |file|
|
93
|
+
Digest::SHA1.hexdigest(file)
|
94
|
+
end
|
95
|
+
|
96
|
+
@plain = @read.call("plain.txt")
|
97
|
+
@image = @read.call("image.png")
|
98
|
+
|
99
|
+
@plain_digest = @digest.call(@plain)
|
100
|
+
@image_digest = @digest.call(@image)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should attach a file' do
|
104
|
+
@alice.attach("plain.txt", @plain, "text/plain")
|
105
|
+
@alice.attach("image.png", @image, "image/png")
|
106
|
+
@alice = @alice.saved_doc
|
107
|
+
|
108
|
+
@alice.attachments.should eql({"image.png"=>{"content_type"=>"image/png", "revpos"=>3, "length"=>104744, "stub"=>true}, "plain.txt"=>{"content_type"=>"text/plain", "revpos"=>2, "length"=>406, "stub"=>true}})
|
109
|
+
@digest.call(@alice.attachments["plain.txt"].data).should eql(@plain_digest)
|
110
|
+
@digest.call(@alice.attachments["image.png"].data).should eql(@image_digest)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'should not attach a file to a new record' do
|
114
|
+
lambda{@new.attach("plain.txt", @plain, "text/plain")}.should raise_error(CouchClient::AttachmentError)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#delete!' do
|
119
|
+
it 'should delete a docment' do
|
120
|
+
@bob.deleted?.should be_false
|
121
|
+
@bob.delete!
|
122
|
+
@bob.deleted?.should be_true
|
123
|
+
lambda{@bob.saved_doc}.should raise_error(CouchClient::DocumentNotFound)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should not delete a document in conflict' do
|
127
|
+
@alice_old = @alice.saved_doc
|
128
|
+
@alice_old["old"] = true
|
129
|
+
@alice["key"] = "value"
|
130
|
+
|
131
|
+
@alice.save
|
132
|
+
|
133
|
+
@alice_old.delete!
|
134
|
+
@alice_old.error?.should be_true
|
135
|
+
@alice_old.conflict?.should be_true
|
136
|
+
@alice_old.error.should eql({"conflict"=>"Document update conflict."})
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe '#design?' do
|
141
|
+
it 'should identify a design document' do
|
142
|
+
@design.design?.should be_true
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'should not identify a normal document' do
|
146
|
+
@alice.design?.should be_false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe '#new?' do
|
151
|
+
it 'should identify a new document' do
|
152
|
+
@alice.new?.should be_false
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'should not identify an existing document' do
|
156
|
+
@new.new?.should be_true
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe '#error #error? and conflict?' do
|
161
|
+
it 'should yield errors for a document that has errors' do
|
162
|
+
@alice_old = @alice.saved_doc
|
163
|
+
@alice["key"] = "value"
|
164
|
+
|
165
|
+
@alice_old.error?.should be_false
|
166
|
+
@alice_old.error.should eql({})
|
167
|
+
@alice_old.conflict?.should be_false
|
168
|
+
|
169
|
+
@alice.save
|
170
|
+
@alice_old.save
|
171
|
+
|
172
|
+
@alice_old.error?.should be_true
|
173
|
+
@alice_old.error.should eql({"conflict"=>"Document update conflict."})
|
174
|
+
@alice_old.conflict?.should be_true
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe '#invalid?' do
|
179
|
+
before(:each) do
|
180
|
+
@alice.instance_variable_set(:@code, 403)
|
181
|
+
@alice.instance_variable_set(:@error, {"forbidden" => "Document must have a name field."})
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'should identify an invalid document' do
|
185
|
+
@alice.invalid?.should be_true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe '#deleted?' do
|
190
|
+
it 'should identify a deleted document' do
|
191
|
+
@bob.deleted?.should be_false
|
192
|
+
@bob.delete!
|
193
|
+
@bob.deleted?.should be_true
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.
|
data/spec/hookup_spec.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
|
2
|
+
require 'digest/sha1'
|
3
|
+
|
4
|
+
describe CouchClient::Hookup do
|
5
|
+
before(:all) do
|
6
|
+
handler = CouchClient::ConnectionHandler.new
|
7
|
+
handler.database = COUCHDB_TEST_DATABASE
|
8
|
+
|
9
|
+
@hookup = CouchClient::Hookup.new(handler)
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:all) do
|
13
|
+
@hookup.delete
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'rest methods' do
|
17
|
+
describe 'put' do
|
18
|
+
it 'should create a database if one does not exist' do
|
19
|
+
@hookup.put.should eql([201, {"ok" => true}])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should not create a database if one already exists' do
|
23
|
+
@hookup.put.should eql([412, {"error" => "file_exists", "reason" => "The database could not be created, the file already exists."}])
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should create a document if a path is provided' do
|
27
|
+
@hookup.put(["alice"]).should eql([201, {"ok" => true, "id" => "alice", "rev" => "1-967a00dff5e02add41819138abb3284d"}])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should create a document with fields if a path with data is provided' do
|
31
|
+
@hookup.put(["bob"], nil, {"name" => "bob", "city" => "nyc"}).should eql([201, {"ok" => true, "id" => "bob", "rev" => "1-fb4ceea745e7c1cd487886f06eba6536"}])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'post' do
|
36
|
+
before(:all) do
|
37
|
+
@hookup.put
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should create a document' do
|
41
|
+
code, body = @hookup.post(nil, nil, {"name" => "charlie"})
|
42
|
+
code.should eql(201)
|
43
|
+
body.should be_a(Hash)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'get and head' do
|
48
|
+
before(:all) do
|
49
|
+
@hookup.put
|
50
|
+
@hookup.put(["dave"], nil, {"name" => "dave", "city" => "chicago"})
|
51
|
+
@id = @hookup.post(nil, nil, {"name" => "edgar", "city" => "miami"}).last["id"]
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should get database information when not given a path' do
|
55
|
+
code, body = @hookup.get
|
56
|
+
code.should eql(200)
|
57
|
+
body["db_name"].should eql("couch-client_test")
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should get a document when given a path' do
|
61
|
+
@hookup.get(["dave"]).should eql([200, {"_id" => "dave", "_rev" => "1-17a22c4b658fd637577a4626344be252", "name" => "dave", "city" => "chicago"}])
|
62
|
+
code, body = @hookup.get([@id])
|
63
|
+
code.should eql(200)
|
64
|
+
body["name"].should eql("edgar")
|
65
|
+
body["city"].should eql("miami")
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'should head a document when given a path' do
|
69
|
+
@hookup.head(["dave"]).should eql([200, nil])
|
70
|
+
code, body = @hookup.head([@id])
|
71
|
+
code.should eql(200)
|
72
|
+
body.should be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'delete' do
|
77
|
+
before(:all) do
|
78
|
+
@hookup.put
|
79
|
+
@rev = @hookup.put(["fred"], nil, {"name" => "fred", "city" => "san fran"}).last["rev"]
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should delete a document when given a path' do
|
83
|
+
@hookup.delete(["fred"], {"rev" => @rev}).should eql([200, {"ok" => true, "id" => "fred", "rev" => "2-9bee1aef2fee82160ae8549079645933"}])
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should delete the database when not given a path' do
|
87
|
+
@hookup.delete.should eql([200, {"ok" => true}])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'attachments' do
|
93
|
+
before(:all) do
|
94
|
+
@read = lambda do |file|
|
95
|
+
File.read(File.join(File.dirname(__FILE__), "files", file))
|
96
|
+
end
|
97
|
+
|
98
|
+
@digest = lambda do |file|
|
99
|
+
Digest::SHA1.hexdigest(file)
|
100
|
+
end
|
101
|
+
|
102
|
+
@plain = @read.call("plain.txt")
|
103
|
+
@image = @read.call("image.png")
|
104
|
+
|
105
|
+
@plain_digest = @digest.call(@plain)
|
106
|
+
@image_digest = @digest.call(@image)
|
107
|
+
|
108
|
+
@hookup.put
|
109
|
+
@rev = @hookup.put(["greg"], nil, {"name" => "greg", "city" => "austin"}).last["rev"]
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'can be uploaded' do
|
113
|
+
@rev = @hookup.put(["greg", "plain.txt"], {"rev" => @rev}, @plain, "text/plain").last["rev"]
|
114
|
+
@rev = @hookup.put(["greg", "image.png"], {"rev" => @rev}, @image, "image/png").last["rev"]
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'can be downloaded' do
|
118
|
+
@digest.call(@hookup.get(["greg", "plain.txt"], nil, "text/plain").last).should eql(@plain_digest)
|
119
|
+
@digest.call(@hookup.get(["greg", "image.png"], nil, "image/png").last).should eql(@image_digest)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|