rocking_chair 0.0.2
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/lib/rocking_chair.rb +33 -0
- data/lib/rocking_chair/couch_rest_http_adapter.rb +80 -0
- data/lib/rocking_chair/database.rb +176 -0
- data/lib/rocking_chair/error.rb +44 -0
- data/lib/rocking_chair/helper.rb +15 -0
- data/lib/rocking_chair/server.rb +137 -0
- data/lib/rocking_chair/view.rb +284 -0
- data/test/couch_rest_test.rb +350 -0
- data/test/database_test.rb +447 -0
- data/test/extended_couch_rest_test.rb +49 -0
- data/test/fixtures/extended_couch_rest_fixtures.rb +23 -0
- data/test/fixtures/simply_stored_fixtures.rb +36 -0
- data/test/simply_stored_test.rb +214 -0
- data/test/test_helper.rb +36 -0
- data/test/view_test.rb +372 -0
- metadata +77 -0
@@ -0,0 +1,284 @@
|
|
1
|
+
module RockingChair
|
2
|
+
class View
|
3
|
+
|
4
|
+
attr_accessor :database, :keys, :options, :ruby_store, :design_document, :design_document_name, :view_name, :view_document
|
5
|
+
|
6
|
+
def self.run(database, design_document_name, view_name, options = {})
|
7
|
+
raise ArgumentError, "Need a databae, a design_doc_name and a view_name" unless database.present? && design_document_name.present? && view_name.present?
|
8
|
+
new(database, design_document_name, view_name, options).find.render
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.run_all(database, options = {})
|
12
|
+
new(database, :all, :all, options).find.render_for_all
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(database, design_document_name, view_name, options = {})
|
16
|
+
unless design_document_name == :all && view_name == :all
|
17
|
+
RockingChair::Error.raise_404 unless database.exists?("_design/#{design_document_name}")
|
18
|
+
@design_document = JSON.parse(database.storage["_design/#{design_document_name}"])
|
19
|
+
@view_document = design_document['views'][view_name] || RockingChair::Error.raise_404
|
20
|
+
end
|
21
|
+
|
22
|
+
@database = database
|
23
|
+
@keys = database.storage.keys
|
24
|
+
@design_document_name = design_document_name
|
25
|
+
@view_name = view_name
|
26
|
+
initialize_ruby_store
|
27
|
+
|
28
|
+
@options = {
|
29
|
+
'reduce' => false,
|
30
|
+
'limit' => nil,
|
31
|
+
'key' => nil,
|
32
|
+
'descending' => false,
|
33
|
+
'include_docs' => false,
|
34
|
+
'without_deleted' => false,
|
35
|
+
'endkey' => nil,
|
36
|
+
'startkey' => nil,
|
37
|
+
'endkey_docid' => nil,
|
38
|
+
'startkey_docid' => nil
|
39
|
+
}.update(options)
|
40
|
+
@options.assert_valid_keys('reduce', 'limit', 'key', 'descending', 'include_docs', 'without_deleted', 'endkey', 'startkey', 'endkey_docid', 'startkey_docid')
|
41
|
+
RockingChair::Helper.jsonfy_options(@options, 'key', 'startkey', 'endkey', 'startkey_docid', 'endkey_docid')
|
42
|
+
|
43
|
+
normalize_view_name
|
44
|
+
end
|
45
|
+
|
46
|
+
def find
|
47
|
+
if view_name == :all
|
48
|
+
find_all
|
49
|
+
elsif match = view_name.match(/\Aall_documents\Z/)
|
50
|
+
find_all_by_class
|
51
|
+
elsif match = view_name.match(/\Aby_(\w+)\Z/)
|
52
|
+
find_by_attribute(match[1])
|
53
|
+
elsif match = view_name.match(/\Aassociation_#{design_document_name}_belongs_to_(\w+)\Z/)
|
54
|
+
find_belongs_to(match[1])
|
55
|
+
else
|
56
|
+
raise "Unknown View implementation for view #{view_name.inspect} in design document _design/#{design_document_name}"
|
57
|
+
end
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def render
|
62
|
+
offset = 0
|
63
|
+
total_size = keys.size
|
64
|
+
filter_by_startkey_docid_and_endkey_docid
|
65
|
+
filter_by_limit
|
66
|
+
|
67
|
+
if options['reduce'].to_s == 'true'
|
68
|
+
{ "rows" => [{'key' => options['key'], 'value' => keys.size }]}.to_json
|
69
|
+
else
|
70
|
+
rows = keys.map do |key|
|
71
|
+
document = ruby_store[key]
|
72
|
+
if options['include_docs'].to_s == 'true'
|
73
|
+
{'id' => RockingChair::Helper.access('_id', document), 'value' => nil, 'doc' => (document.respond_to?(:_document) ? document._document : document) }.merge(key_description)
|
74
|
+
else
|
75
|
+
{'id' => RockingChair::Helper.access('_id', document), 'key' => options['key'], 'value' => nil}.merge(key_description)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
{ "total_rows" => total_size, "offset" => offset, "rows" => rows}.to_json
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def render_for_all
|
83
|
+
offset = filter_by_startkey
|
84
|
+
filter_by_endkey
|
85
|
+
filter_by_limit
|
86
|
+
|
87
|
+
rows = keys.map do |key|
|
88
|
+
document = ruby_store[key]
|
89
|
+
if options['include_docs'].to_s == 'true'
|
90
|
+
{'id' => document['_id'], 'key' => document['_id'], 'value' => document.update('rev' => document['_rev'])}
|
91
|
+
else
|
92
|
+
{'id' => document['_id'], 'key' => document['_id'], 'value' => {'rev' => document['_rev']}}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
{ "total_rows" => database.document_count, "offset" => offset, "rows" => rows}.to_json
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def find_all
|
102
|
+
sort_by_attribute('_id')
|
103
|
+
end
|
104
|
+
|
105
|
+
def find_all_by_class
|
106
|
+
filter_items_without_correct_ruby_class
|
107
|
+
filter_deleted_items if options['without_deleted'].to_s == 'true'
|
108
|
+
end
|
109
|
+
|
110
|
+
def find_belongs_to(belongs_to)
|
111
|
+
filter_items_by_key([foreign_key_id(belongs_to)])
|
112
|
+
filter_items_without_correct_ruby_class
|
113
|
+
filter_deleted_items if options['without_deleted'].to_s == 'true'
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_by_attribute(attribute_string)
|
117
|
+
attributes = attribute_string.split("_and_")
|
118
|
+
|
119
|
+
filter_items_by_key(attributes)
|
120
|
+
filter_items_without_correct_ruby_class
|
121
|
+
filter_deleted_items if options['without_deleted'].to_s == 'true'
|
122
|
+
sort_by_attribute(attributes.first)
|
123
|
+
end
|
124
|
+
|
125
|
+
def normalize_view_name
|
126
|
+
return if view_name.is_a?(Symbol)
|
127
|
+
|
128
|
+
if view_name.match(/_withoutdeleted\Z/) || view_name.match(/_without_deleted\Z/)
|
129
|
+
options['without_deleted'] = true
|
130
|
+
elsif view_name.match(/_withdeleted\Z/) || view_name.match(/_with_deleted\Z/)
|
131
|
+
options['without_deleted'] = false
|
132
|
+
else
|
133
|
+
options['without_deleted'] = view_document['map'].match(/\"soft\" deleted/) ? true : nil
|
134
|
+
end
|
135
|
+
@view_name = view_name.gsub(/_withoutdeleted\Z/, '').gsub(/_without_deleted\Z/, '').gsub(/_withdeleted\Z/, '').gsub(/_with_deleted\Z/, '')
|
136
|
+
end
|
137
|
+
|
138
|
+
def initialize_ruby_store
|
139
|
+
@ruby_store = database.storage.dup
|
140
|
+
@ruby_store.each{|k,v| ruby_store[k] = JSON.parse(v) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def filter_items_by_key(attributes)
|
144
|
+
if options['startkey']
|
145
|
+
filter_items_by_range(attributes)
|
146
|
+
else
|
147
|
+
filter_items_by_exact_key(attributes)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def filter_items_by_exact_key(attributes)
|
152
|
+
filter_keys = options['key'].is_a?(Array) ? options['key'] : [options['key']]
|
153
|
+
attributes.each_with_index do |attribute, index|
|
154
|
+
filter_items_without_attribute_value(attribute, filter_keys[index])
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def filter_items_by_range(attributes)
|
159
|
+
start_keys = options['startkey'].is_a?(Array) ? options['startkey'] : [options['startkey']]
|
160
|
+
end_keys = options['endkey'].is_a?(Array) ? options['endkey'] : [options['endkey']]
|
161
|
+
|
162
|
+
attributes.each_with_index do |attribute, index|
|
163
|
+
filter_items_not_in_range(attribute, start_keys[index], end_keys[index])
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def filter_items_not_in_range(attribute, start_key, end_key)
|
168
|
+
@keys = keys.select do |key|
|
169
|
+
document = ruby_store[key]
|
170
|
+
if end_key
|
171
|
+
RockingChair::Helper.access(attribute, document) && (RockingChair::Helper.access(attribute, document) >= start_key) && (RockingChair::Helper.access(attribute, document) <= end_key)
|
172
|
+
else
|
173
|
+
RockingChair::Helper.access(attribute, document) && (RockingChair::Helper.access(attribute, document) >= start_key)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def filter_deleted_items
|
179
|
+
@keys = keys.delete_if do |key|
|
180
|
+
document = ruby_store[key]
|
181
|
+
RockingChair::Helper.access('deleted_at', document).present?
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def sort_by_attribute(attribute)
|
186
|
+
attribute ||= '_id'
|
187
|
+
@keys = (options['descending'].to_s == 'true') ?
|
188
|
+
keys.sort{|x,y| RockingChair::Helper.access(attribute, ruby_store[y]) <=> RockingChair::Helper.access(attribute, ruby_store[x]) } :
|
189
|
+
keys.sort{|x,y| RockingChair::Helper.access(attribute, ruby_store[x]) <=> RockingChair::Helper.access(attribute, ruby_store[y]) }
|
190
|
+
end
|
191
|
+
|
192
|
+
def filter_items_without_attribute_value(attribute, attr_value)
|
193
|
+
if attr_value
|
194
|
+
@keys = keys.select do |key|
|
195
|
+
document = ruby_store[key]
|
196
|
+
if RockingChair::Helper.access(attribute, document).is_a?(Array)
|
197
|
+
RockingChair::Helper.access(attribute, document).include?(attr_value)
|
198
|
+
else
|
199
|
+
RockingChair::Helper.access(attribute, document) == attr_value
|
200
|
+
end
|
201
|
+
end
|
202
|
+
else
|
203
|
+
@keys = keys.select do |key|
|
204
|
+
document = ruby_store[key]
|
205
|
+
RockingChair::Helper.access(attribute, document).present?
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def filter_items_without_correct_ruby_class
|
211
|
+
klass_name = design_document_name.classify
|
212
|
+
@keys = keys.select do |key|
|
213
|
+
document = ruby_store[key]
|
214
|
+
RockingChair::Helper.access('ruby_class', document).to_s.classify == klass_name
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def filter_by_limit
|
219
|
+
if options['limit']
|
220
|
+
@keys = keys[0, options['limit'].to_i]
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def filter_by_startkey_docid_and_endkey_docid
|
225
|
+
if options['startkey_docid'] || options['endkey_docid']
|
226
|
+
@keys = keys.select do |key|
|
227
|
+
if options['startkey_docid'] && options['endkey_docid']
|
228
|
+
( key >= options['startkey_docid']) && (key <= options['endkey_docid'])
|
229
|
+
elsif options['startkey_docid']
|
230
|
+
key >= options['startkey_docid']
|
231
|
+
else
|
232
|
+
key <= options['endkey_docid']
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def filter_by_startkey
|
239
|
+
offset = 0
|
240
|
+
if options['startkey']
|
241
|
+
startkey_found = false
|
242
|
+
@keys = keys.map do |key|
|
243
|
+
if startkey_found || key == options['startkey']
|
244
|
+
startkey_found = true
|
245
|
+
key
|
246
|
+
else
|
247
|
+
offset += 1
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
end.compact
|
251
|
+
end
|
252
|
+
return offset
|
253
|
+
end
|
254
|
+
|
255
|
+
def filter_by_endkey
|
256
|
+
if options['endkey']
|
257
|
+
endkey_found = false
|
258
|
+
@keys = keys.map do |key|
|
259
|
+
if key == options['endkey']
|
260
|
+
endkey_found = true
|
261
|
+
key
|
262
|
+
elsif endkey_found
|
263
|
+
nil
|
264
|
+
else
|
265
|
+
key
|
266
|
+
end
|
267
|
+
end.compact
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def foreign_key_id(name)
|
272
|
+
name.underscore.gsub('/','__').gsub('::','__') + "_id"
|
273
|
+
end
|
274
|
+
|
275
|
+
def key_description
|
276
|
+
description = {'key' => options['key']}
|
277
|
+
description = {'startkey' => options['startkey'], 'endkey' => options['endkey']} if options['startkey']
|
278
|
+
description.update('startkey_docid' => options['startkey_docid']) if options['startkey_docid']
|
279
|
+
description.update('endkey_docid' => options['endkey_docid']) if options['endkey_docid']
|
280
|
+
description
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,350 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/test_helper"
|
2
|
+
|
3
|
+
class CouchRestTest < Test::Unit::TestCase
|
4
|
+
context "The HTTP Apdapter for CouchRest" do
|
5
|
+
setup do
|
6
|
+
RockingChair::Server.reset
|
7
|
+
@couch = CouchRest.new("http://127.0.0.1:5984")
|
8
|
+
end
|
9
|
+
|
10
|
+
context "Database API" do
|
11
|
+
|
12
|
+
should "return the info JSON" do
|
13
|
+
assert_equal({"couchdb" => "Welcome","version" => "0.10.1"}, @couch.info)
|
14
|
+
end
|
15
|
+
|
16
|
+
should "return a list of all databases" do
|
17
|
+
assert_equal [], @couch.databases
|
18
|
+
@couch.create_db('database-name')
|
19
|
+
assert_equal ['database-name'], @couch.databases
|
20
|
+
end
|
21
|
+
|
22
|
+
should "create a database" do
|
23
|
+
db = @couch.create_db('database-name')
|
24
|
+
end
|
25
|
+
|
26
|
+
should "return information on a database" do
|
27
|
+
@couch.create_db('database-name')
|
28
|
+
assert_equal({
|
29
|
+
"db_name" => "database-name",
|
30
|
+
"doc_count" => 0,
|
31
|
+
"doc_del_count" => 0,
|
32
|
+
"update_seq" => 10,
|
33
|
+
"purge_seq" => 0,
|
34
|
+
"compact_running" => false,
|
35
|
+
"disk_size" => 16473,
|
36
|
+
"instance_start_time" => "1265409273572320",
|
37
|
+
"disk_format_version" => 4}, @couch.database('database-name').info)
|
38
|
+
end
|
39
|
+
|
40
|
+
should "delete a database" do
|
41
|
+
db = @couch.database('database-name')
|
42
|
+
db.delete!
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when asking for UUIDs" do
|
48
|
+
should "return a UUID" do
|
49
|
+
RockingChair::Database.expects(:uuids).with('2').returns(['1', '2'])
|
50
|
+
assert_equal '2', @couch.next_uuid(2)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "Bulk Document API" do
|
55
|
+
setup do
|
56
|
+
@db = @couch.create_db('RockingChair')
|
57
|
+
RockingChair::Database.any_instance.stubs(:rev).returns('the-revision')
|
58
|
+
end
|
59
|
+
|
60
|
+
context "loading all documents" do
|
61
|
+
should "load the total documents" do
|
62
|
+
5.times do |i|
|
63
|
+
@db.save_doc({'_id' => "item-#{i}", 'a' => 'b'})
|
64
|
+
end
|
65
|
+
assert_equal({
|
66
|
+
"total_rows" => 5, "offset" => 0, "rows" => [
|
67
|
+
{"id" => "item-0", "key" => "item-0", "value" => {"rev" => "the-revision"}},
|
68
|
+
{"id" => "item-1", "key" => "item-1", "value" => {"rev" => "the-revision"}},
|
69
|
+
{"id" => "item-2", "key" => "item-2", "value" => {"rev" => "the-revision"}},
|
70
|
+
{"id" => "item-3", "key" => "item-3", "value" => {"rev" => "the-revision"}},
|
71
|
+
{"id" => "item-4", "key" => "item-4", "value" => {"rev" => "the-revision"}}
|
72
|
+
]
|
73
|
+
}, @db.documents)
|
74
|
+
end
|
75
|
+
|
76
|
+
should "sort the result by ID ascending" do
|
77
|
+
@db.save_doc({'_id' => "C", 'a' => 'b'})
|
78
|
+
@db.save_doc({'_id' => "B", 'a' => 'b'})
|
79
|
+
@db.save_doc({'_id' => "A", 'a' => 'b'})
|
80
|
+
assert_equal({
|
81
|
+
"total_rows" => 3, "offset" => 0, "rows" => [
|
82
|
+
{"id" => "A", "key" => "A", "value" => {"rev" => "the-revision"}},
|
83
|
+
{"id" => "B", "key" => "B", "value" => {"rev" => "the-revision"}},
|
84
|
+
{"id" => "C", "key" => "C", "value" => {"rev" => "the-revision"}}
|
85
|
+
]
|
86
|
+
}, @db.documents)
|
87
|
+
end
|
88
|
+
|
89
|
+
should "sort the result by ID descending" do
|
90
|
+
@db.save_doc({'_id' => "C", 'a' => 'b'})
|
91
|
+
@db.save_doc({'_id' => "B", 'a' => 'b'})
|
92
|
+
@db.save_doc({'_id' => "A", 'a' => 'b'})
|
93
|
+
assert_equal({
|
94
|
+
"total_rows" => 3, "offset" => 0, "rows" => [
|
95
|
+
{"id" => "C", "key" => "C", "value" => {"rev" => "the-revision"}},
|
96
|
+
{"id" => "B", "key" => "B", "value" => {"rev" => "the-revision"}},
|
97
|
+
{"id" => "A", "key" => "A", "value" => {"rev" => "the-revision"}}
|
98
|
+
]
|
99
|
+
}, @db.documents('descending' => true))
|
100
|
+
end
|
101
|
+
|
102
|
+
should "start by the given key" do
|
103
|
+
@db.save_doc({'_id' => "C", 'a' => 'b'})
|
104
|
+
@db.save_doc({'_id' => "B", 'a' => 'b'})
|
105
|
+
@db.save_doc({'_id' => "A", 'a' => 'b'})
|
106
|
+
|
107
|
+
assert_equal({
|
108
|
+
"total_rows" => 3, "offset" => 1, "rows" => [
|
109
|
+
{"id" => "B", "key" => "B", "value" => {"rev" => "the-revision"}},
|
110
|
+
{"id" => "C", "key" => "C", "value" => {"rev" => "the-revision"}}
|
111
|
+
]
|
112
|
+
}, @db.documents(:startkey => 'B'))
|
113
|
+
end
|
114
|
+
|
115
|
+
should "start by the given key, include docs, descending and limit" do
|
116
|
+
@db.save_doc({'_id' => "C", 'a' => 'b'})
|
117
|
+
@db.save_doc({'_id' => "B", 'a' => 'b'})
|
118
|
+
@db.save_doc({'_id' => "A", 'a' => 'b'})
|
119
|
+
@db.save_doc({'_id' => "D", 'a' => 'b'})
|
120
|
+
|
121
|
+
assert_equal({
|
122
|
+
"total_rows" => 4, "offset" => 1, "rows" => [
|
123
|
+
{"id" => "C", "key" => "C", "value" => {"rev" => "the-revision", '_rev' => 'the-revision', '_id' => 'C', 'a' => 'b'}},
|
124
|
+
{"id" => "B", "key" => "B", "value" => {"rev" => "the-revision", '_rev' => 'the-revision', '_id' => 'B', 'a' => 'b'}}
|
125
|
+
]
|
126
|
+
}, @db.documents(:startkey => 'C', :limit => 2, :include_docs => true, :descending => true))
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "bulk modifications" do
|
131
|
+
|
132
|
+
should "accept bulk inserts/updates" do
|
133
|
+
RockingChair::Database.stubs(:uuid).returns('foo-id')
|
134
|
+
|
135
|
+
docs = [{"_id" => 'a', "value" => 1}, {"_id" => 'b', 'value' => 2}, {'value' => 3}]
|
136
|
+
assert_equal([
|
137
|
+
{'id' => 'a', "rev" => 'the-revision'},
|
138
|
+
{'id' => 'b', "rev" => 'the-revision'},
|
139
|
+
{'id' => 'foo-id', "rev" => 'the-revision'}
|
140
|
+
], @db.bulk_save(docs))
|
141
|
+
end
|
142
|
+
|
143
|
+
should "handle automatic inserts/updates" do
|
144
|
+
@db.bulk_save_cache_limit = 5
|
145
|
+
assert_nothing_raised do
|
146
|
+
10.times do |i|
|
147
|
+
@db.save_doc({'_id' => "new-item-#{i}", 'content' => 'here'}, true)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
assert_equal 10, @db.info['doc_count']
|
151
|
+
end
|
152
|
+
|
153
|
+
should "do updates" do
|
154
|
+
@db.save_doc({'_id' => "A", 'a' => 'b'})
|
155
|
+
@db.save_doc({'_id' => "B", 'a' => 'b'})
|
156
|
+
|
157
|
+
docs = [{"_id" => 'A', "a" => 1, '_rev' => 'the-revision'}, {'value' => 3}]
|
158
|
+
@db.bulk_save(docs)
|
159
|
+
|
160
|
+
assert_equal({'_id' => 'A', 'a' => 1, '_rev' => 'the-revision'}, @db.get('A'))
|
161
|
+
end
|
162
|
+
|
163
|
+
should "do deletes" do
|
164
|
+
@db.save_doc({'_id' => "A", 'a' => 'b'})
|
165
|
+
@db.save_doc({'_id' => "B", 'a' => 'b'})
|
166
|
+
|
167
|
+
docs = [{"_id" => 'A', "a" => 1, '_rev' => 'the-revision', '_deleted' => true}]
|
168
|
+
@db.bulk_save(docs)
|
169
|
+
|
170
|
+
assert_raise(RestClient::ResourceNotFound) do
|
171
|
+
@db.get('A')
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context "Document API" do
|
179
|
+
setup do
|
180
|
+
@db = @couch.create_db('RockingChair')
|
181
|
+
RockingChair::Database.any_instance.stubs(:rev).returns('the-revision')
|
182
|
+
end
|
183
|
+
|
184
|
+
context "when retrieving a document (GET)" do
|
185
|
+
setup do
|
186
|
+
@db.save_doc({'_id' => 'the-doc-id', 'a' => 'b'})
|
187
|
+
end
|
188
|
+
|
189
|
+
should "load the document" do
|
190
|
+
assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision'}, @db.get('the-doc-id'))
|
191
|
+
end
|
192
|
+
|
193
|
+
should "raise a 404 if the document does not exist" do
|
194
|
+
assert_raise(RestClient::ResourceNotFound) do
|
195
|
+
@db.get('no-such-id')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
should "load the document by a specific revision" do
|
200
|
+
assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision'}, @db.get('the-doc-id', :rev => 'the-revision'))
|
201
|
+
end
|
202
|
+
|
203
|
+
should "raise a 404 if the revision is not there" do
|
204
|
+
assert_raise(RestClient::ResourceNotFound) do
|
205
|
+
@db.get('the-doc-id', :rev => 'non-existant-revision')
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
should "load the document witht the revisions history" do
|
210
|
+
assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision', "_revisions" => {"start" => 1,"ids" => ["the-revision"]}}, @db.get('the-doc-id', :revs => 'true'))
|
211
|
+
end
|
212
|
+
|
213
|
+
should "load the document witht the detailed revisions history" do
|
214
|
+
assert_equal({"_id" => "the-doc-id","a" => "b", '_rev' => 'the-revision', "_revs_info" => [{"rev" => "the-revision", "status" => "disk"}]}, @db.get('the-doc-id', :revs_info => 'true'))
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
context "when storing a document (PUT)" do
|
219
|
+
should "store a new the document" do
|
220
|
+
assert_equal 0, @db.info['doc_count']
|
221
|
+
@db.save_doc({'_id' => 'new-item', 'content' => 'here'})
|
222
|
+
assert_equal 1, @db.info['doc_count']
|
223
|
+
assert_equal({"_id" => "new-item","content" => "here", '_rev' => 'the-revision'}, @db.get('new-item'))
|
224
|
+
end
|
225
|
+
|
226
|
+
should "update a document" do
|
227
|
+
seq = sequence('revision')
|
228
|
+
RockingChair::Database.any_instance.expects(:rev).returns('first-rev').in_sequence(seq)
|
229
|
+
RockingChair::Database.any_instance.expects(:rev).returns('second-rev').in_sequence(seq)
|
230
|
+
|
231
|
+
@db.save_doc({'_id' => 'new-item', 'content' => 'here'})
|
232
|
+
@db.save_doc({'_id' => 'new-item', 'content' => 'better', '_rev' => 'first-rev'})
|
233
|
+
|
234
|
+
assert_equal({"_id" => "new-item","content" => "better", '_rev' => 'second-rev'}, @db.get('new-item'))
|
235
|
+
end
|
236
|
+
|
237
|
+
should "raise 409 on a revision conflict" do
|
238
|
+
@db.save_doc({'_id' => 'new-item', 'content' => 'here'})
|
239
|
+
assert_raise(HttpAbstraction::Conflict) do
|
240
|
+
@db.save_doc({'_id' => 'new-item', 'content' => 'better', '_rev' => 'wrong-revision'})
|
241
|
+
end
|
242
|
+
|
243
|
+
assert_equal({"_id" => "new-item","content" => "here", '_rev' => 'the-revision'}, @db.get('new-item'))
|
244
|
+
end
|
245
|
+
|
246
|
+
should "ignore the batch parameter" do
|
247
|
+
assert_nothing_raised do
|
248
|
+
@db.save_doc({'_id' => 'new-item', 'content' => 'here'}, false, true)
|
249
|
+
end
|
250
|
+
assert_equal({"_id" => "new-item","content" => "here", '_rev' => 'the-revision'}, @db.get('new-item'))
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
context "when storing a document (POST)" do
|
256
|
+
should "store a new the document" do
|
257
|
+
RockingChair::Database.expects(:uuid).returns('5')
|
258
|
+
assert_equal 0, @db.info['doc_count']
|
259
|
+
assert_equal({"rev"=>"the-revision", "id"=>"5", "ok"=>true},
|
260
|
+
CouchRest.post('127.0.0.1:5984/RockingChair/', {'content' => 'here'}))
|
261
|
+
assert_equal 1, @db.info['doc_count']
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context "when deleting a document (DELETE)" do
|
266
|
+
should "delete if the rev matches" do
|
267
|
+
RockingChair::Database.any_instance.stubs(:rev).returns('123')
|
268
|
+
|
269
|
+
@db.save_doc({'a' => 'b', '_id' => 'delete_me'})
|
270
|
+
@db.delete_doc({'a' => 'b', '_id' => 'delete_me', '_rev' => '123'})
|
271
|
+
assert_raise(RestClient::ResourceNotFound) do
|
272
|
+
@db.get('delete_me')
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
should "fail with conflich if the rev does not matche" do
|
277
|
+
@db.save_doc({'a' => 'b', '_id' => 'delete_me'})
|
278
|
+
assert_raise(HttpAbstraction::Conflict) do
|
279
|
+
@db.delete_doc({'a' => 'b', '_id' => 'delete_me', '_rev' => 'wrong-revision'})
|
280
|
+
end
|
281
|
+
assert_nothing_raised do
|
282
|
+
@db.get('delete_me')
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
context "when copying a document (COPY)" do
|
288
|
+
setup do
|
289
|
+
RockingChair::Database.any_instance.stubs(:rev).returns('123')
|
290
|
+
@db.save_doc({'a' => 'b', '_id' => 'original'})
|
291
|
+
end
|
292
|
+
|
293
|
+
context "when using an destination ID" do
|
294
|
+
|
295
|
+
should "copy" do
|
296
|
+
@db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, 'the_new_id')
|
297
|
+
assert_equal({'a' => 'b', '_id' => 'the_new_id', '_rev' => '123'}, @db.get('the_new_id'))
|
298
|
+
end
|
299
|
+
|
300
|
+
should "copy with overwrite with revision" do
|
301
|
+
seq = sequence('revision')
|
302
|
+
RockingChair::Database.any_instance.expects(:rev).returns('first-rev').in_sequence(seq)
|
303
|
+
RockingChair::Database.any_instance.expects(:rev).returns('second-rev').in_sequence(seq)
|
304
|
+
|
305
|
+
@db.save_doc({'1' => '2', '_id' => 'destination'})
|
306
|
+
|
307
|
+
@db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, "destination?rev=first-rev")
|
308
|
+
assert_equal({'a' => 'b', '_id' => 'destination', '_rev' => 'second-rev'}, @db.get('destination'))
|
309
|
+
end
|
310
|
+
|
311
|
+
should "not copy with overwrite if the revision does not match" do
|
312
|
+
@db.save_doc({'1' => '2', '_id' => 'destination'})
|
313
|
+
assert_raise(HttpAbstraction::Conflict) do
|
314
|
+
@db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, 'destination?rev=not-here')
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
|
320
|
+
context "when using a destination object" do
|
321
|
+
should "copy" do
|
322
|
+
destination = {"_id" => 'destination', 'c' => 'a', '_rev' => '123'}
|
323
|
+
@db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, destination)
|
324
|
+
assert_equal({'a' => 'b', '_id' => 'destination', '_rev' => '123'}, @db.get('destination'))
|
325
|
+
end
|
326
|
+
|
327
|
+
should "copy with overwrite with revision" do
|
328
|
+
seq = sequence('revision')
|
329
|
+
RockingChair::Database.any_instance.expects(:rev).returns('first-rev').in_sequence(seq)
|
330
|
+
RockingChair::Database.any_instance.expects(:rev).returns('second-rev').in_sequence(seq)
|
331
|
+
|
332
|
+
@db.save_doc({'1' => '2', '_id' => 'destination'})
|
333
|
+
|
334
|
+
@db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, {'1' => '2', '_id' => 'destination', '_rev' => 'first-rev'})
|
335
|
+
assert_equal({'a' => 'b', '_id' => 'destination', '_rev' => 'second-rev'}, @db.get('destination'))
|
336
|
+
end
|
337
|
+
|
338
|
+
should "not copy with overwrite if the revision does not match" do
|
339
|
+
@db.save_doc({'1' => '2', '_id' => 'destination'})
|
340
|
+
assert_raise(HttpAbstraction::Conflict) do
|
341
|
+
@db.copy_doc({'a' => 'b', '_id' => 'original', '_rev' => '123'}, {'1' => '2', '_id' => 'destination', '_rev' => 'missing'})
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
end
|
348
|
+
|
349
|
+
end
|
350
|
+
end
|