ShyCouch 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -3
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/lib/ShyCouch.rb +274 -198
- data/lib/ShyCouch/data.rb +445 -243
- data/readme.md +117 -0
- data/test/test_ShyCouch.rb +17 -25
- data/test/test_couch_document.rb +171 -7
- data/test/test_couchdb_api.rb +45 -6
- data/test/test_couchdb_factory.rb +3 -3
- data/test/test_design_documents.rb +150 -129
- data/test/test_document_validation.rb +85 -0
- data/test/test_fields.rb +12 -12
- data/test/test_views.rb +129 -33
- metadata +49 -42
- data/.document +0 -5
- data/Gemfile.lock +0 -36
- data/README +0 -117
- data/ShyCouch.gemspec +0 -83
- data/design_notes.rb +0 -39
data/readme.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
ShyCouch
|
2
|
+
--------
|
3
|
+
|
4
|
+
|
5
|
+
Ruby library for CouchDB providing a native objects layer, abstracting the HTTP interface.
|
6
|
+
|
7
|
+
It's for those who think that ActiveRecord and even Django's lovely ORMs don't get you far enough away from SQL. It's for people who want to make funky websites and self-managed / self-hosted web services, not like, you know, Define Business Requirements And Cardinalities or whatever.
|
8
|
+
|
9
|
+
Soon it'll have support for native Ruby views on the view server. In the meantime, views are written inline in Ruby and then parsed in JavaScript using ShyRubyJS. ShyRubyJS is not very mature, so for anything complex you should write them as inline JavaScript.
|
10
|
+
|
11
|
+
If anyone can think of a good semantics to prevent confusion between Couch views and the views in an MVC framework that might use this library, I'd love to hear it.
|
12
|
+
|
13
|
+
Usage
|
14
|
+
-----
|
15
|
+
|
16
|
+
Create a database object, automatically initializing the database on the CouchDB instance if it doesn't exist
|
17
|
+
|
18
|
+
````ruby
|
19
|
+
couch_settings = {
|
20
|
+
"db"=> {
|
21
|
+
"host" => "localhost",
|
22
|
+
"port" => 5984,
|
23
|
+
"name" => "food-app",
|
24
|
+
"user" => "myUsername",
|
25
|
+
"password" => "myPassword"
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
@couchdb = ShyCouch::getDB(couch_settings)
|
30
|
+
````
|
31
|
+
Create a design document and give it some views.
|
32
|
+
|
33
|
+
Note: soon there'll be a syntax for having default HTTP query options (e.g. `?include_docs=true`) on particular views.
|
34
|
+
|
35
|
+
````ruby
|
36
|
+
design = ShyCouch::Data::Design.new :food, {:push_to => @couchdb}
|
37
|
+
|
38
|
+
view1 = ShyCouch::Data::View :all_recipes do
|
39
|
+
map do
|
40
|
+
emit(doc._id, null) if doc.kind == "Recipe"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
view2 = ShyCouch::Data::View :recipe_count do
|
44
|
+
map do
|
45
|
+
emit(doc._id, null) if doc.kind == "Recipe"
|
46
|
+
end
|
47
|
+
reduce do
|
48
|
+
return sum(values)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
design.add_views [view1, view2]
|
53
|
+
design.push!
|
54
|
+
````
|
55
|
+
Subclass the CouchDocument class to represent your data types:
|
56
|
+
|
57
|
+
````ruby
|
58
|
+
class Recipe < ShyCouch::Data::CouchDocument
|
59
|
+
needs :name
|
60
|
+
needs :ingredients
|
61
|
+
needs :difficulty
|
62
|
+
suggests :cost
|
63
|
+
end
|
64
|
+
|
65
|
+
````
|
66
|
+
Do whatever application logic, and then push your documents (soon you'll be able to define the default `:push_to` for the whole class):
|
67
|
+
|
68
|
+
````ruby
|
69
|
+
recipe_data = {
|
70
|
+
:name = "tuesday snack"
|
71
|
+
:ingredients => "sawdust, apples",
|
72
|
+
:difficulty => "not hard enough needs more boss fights",
|
73
|
+
:cost => "cheap eh"
|
74
|
+
}
|
75
|
+
recipe = Recipe.new(:push_to => @couchdb, :data => recipe_data)
|
76
|
+
recipe.push!
|
77
|
+
````
|
78
|
+
Note the `:needs` and `:suggests` syntax when you define a class. Your documents will always raise an error if you try to push without something that's in `:needs` but you can override `:suggests`:
|
79
|
+
|
80
|
+
````ruby
|
81
|
+
recipe_data = {
|
82
|
+
:name => "soup for guests"
|
83
|
+
:ingredients => "king rat, chicken stock"
|
84
|
+
:difficult => "extr3m3"
|
85
|
+
}
|
86
|
+
recipe = Recipe.new(:push_to => @couchdb, :data => recipe_data)
|
87
|
+
recipe.push! :ignore_suggests => :cost
|
88
|
+
````
|
89
|
+
|
90
|
+
Class inheritence maintains document validation:
|
91
|
+
|
92
|
+
````ruby
|
93
|
+
class FascistRecipe < CouchDocument
|
94
|
+
needs :who_is_allowed_to_cook_it
|
95
|
+
end
|
96
|
+
recipe_data = {
|
97
|
+
:who_is_allowed_to_cook_it => "alan"
|
98
|
+
}
|
99
|
+
recipe = Recipe.new :push_to => @couchdb
|
100
|
+
recipe.push!
|
101
|
+
>>>ShyCouch::DocumentValidationError: Document Missing required fields: [:name, :ingredients, :difficulty]
|
102
|
+
````
|
103
|
+
|
104
|
+
You can query your views:
|
105
|
+
|
106
|
+
````ruby
|
107
|
+
recipes = @couchdb.design(:food).query_view(:all_recipes)
|
108
|
+
````
|
109
|
+
|
110
|
+
Querying your views will return the raw view results as a hash keyed by whatever the view was keyed by.
|
111
|
+
|
112
|
+
If you call it like this, though:
|
113
|
+
|
114
|
+
````ruby
|
115
|
+
recipes = @couchdb.design(:food).query_view(:all_recipes, :include_docs => true)
|
116
|
+
````
|
117
|
+
Then you'll get your view results as a `ShyCouch::Data::DocumentCollection` object, where each object is an instance of `ShyCouch::Data::CouchDocument`.
|
data/test/test_ShyCouch.rb
CHANGED
@@ -7,49 +7,41 @@ require_relative '../lib/ShyCouch'
|
|
7
7
|
# Settings for a database that is set up and working, with an admin user
|
8
8
|
$settings = {
|
9
9
|
"db"=> {
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
# "host" => "ramponeau.local",
|
11
|
+
"host" => "localhost",
|
12
|
+
"port" => 5984,
|
13
|
+
"name" => "test",
|
14
|
+
"user" => "cerales",
|
15
|
+
"password" => "password"
|
16
|
+
},
|
17
17
|
}
|
18
18
|
|
19
19
|
class ShyCouchTestHelper < Test::Unit::TestCase
|
20
20
|
# The majority of the tests in this suite need the setup provided in here
|
21
21
|
# And it means I don't forget to delete the database in their teardown functions
|
22
22
|
def setup
|
23
|
-
|
24
|
-
|
23
|
+
valid_settings = $settings
|
24
|
+
@couchdb = ShyCouch.getDB(valid_settings)
|
25
25
|
end
|
26
26
|
def teardown
|
27
|
-
|
28
|
-
|
27
|
+
@couchdb.delete!
|
28
|
+
@couchdb = nil
|
29
29
|
end
|
30
30
|
|
31
31
|
def add_some_documents
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
4.times do
|
33
|
+
recipe = Recipe.new(:push_to => @couchdb, :data => {:length => 10})
|
34
|
+
recipe.push!
|
35
|
+
end
|
36
36
|
end
|
37
37
|
|
38
38
|
class Recipe < ShyCouch::Data::CouchDocument; end
|
39
39
|
end
|
40
40
|
|
41
|
-
# test ShyCouch::CouchDBAPI
|
42
41
|
require_relative 'test_couchdb_api'
|
43
|
-
|
44
|
-
# test ShyCouch::Fields
|
45
|
-
# some of the tests in here are disabled cos they involve attempting to resolve a bad domain name
|
46
42
|
require_relative 'test_fields'
|
47
|
-
#
|
48
|
-
# # test ShyCouch::Data::CouchDocument
|
49
43
|
require_relative 'test_couch_document'
|
50
|
-
#
|
51
44
|
require_relative 'test_couchdb_factory'
|
52
|
-
#
|
53
45
|
require_relative 'test_design_documents'
|
54
|
-
|
55
|
-
require_relative 'test_views'
|
46
|
+
require_relative 'test_document_validation'
|
47
|
+
require_relative 'test_views'
|
data/test/test_couch_document.rb
CHANGED
@@ -3,6 +3,17 @@ require_relative '../lib/ShyCouch.rb'
|
|
3
3
|
|
4
4
|
class CouchDocumentTests
|
5
5
|
|
6
|
+
class ShyCouchDocumentTestHelper < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
valid_settings = $settings
|
9
|
+
@couchdb = ShyCouch.getDB(valid_settings)
|
10
|
+
end
|
11
|
+
def teardown
|
12
|
+
@couchdb.delete!
|
13
|
+
@couchdb = nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
6
17
|
class TestDocumentCreation < Test::Unit::TestCase
|
7
18
|
def setup
|
8
19
|
valid_settings = $settings
|
@@ -45,8 +56,39 @@ class CouchDocumentTests
|
|
45
56
|
|
46
57
|
end
|
47
58
|
|
48
|
-
|
49
|
-
|
59
|
+
|
60
|
+
class TestDocumentDeletion < ShyCouchDocumentTestHelper
|
61
|
+
def setup
|
62
|
+
super
|
63
|
+
# make some documents, store the ids
|
64
|
+
@documents = []
|
65
|
+
@collection = ShyCouch::Data::CouchDocumentCollection.new :push_to => @couchdb
|
66
|
+
@collection << ShyCouch::Data::CouchDocument.new
|
67
|
+
@collection << ShyCouch::Data::CouchDocument.new
|
68
|
+
@collection << ShyCouch::Data::CouchDocument.new
|
69
|
+
@collection.push_all!
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_delete
|
73
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
74
|
+
@collection.each do |document|
|
75
|
+
document.delete! :from => @couchdb
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_try_to_pull_after_delete
|
81
|
+
@collection.each do |document|
|
82
|
+
document.delete! :from => @couchdb
|
83
|
+
end
|
84
|
+
@collection.each do |document|
|
85
|
+
assert_raises ShyCouch::ResourceNotFound do
|
86
|
+
@couchdb.pull_document document
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def teardown; super; end
|
50
92
|
end
|
51
93
|
|
52
94
|
class TestDocumentPushing < Test::Unit::TestCase
|
@@ -69,7 +111,7 @@ class CouchDocumentTests
|
|
69
111
|
ShyCouch::Data::CouchDocument.new(:data => {"whatever"=>"yep"}),
|
70
112
|
ShyCouch::Data::CouchDocument.new(:data => {"is_a_document"=>true, "number_of_docs_this_is"=>1})
|
71
113
|
].each { |doc|
|
72
|
-
doc.push!($couchdb)
|
114
|
+
doc.push!(:push_to => $couchdb)
|
73
115
|
}
|
74
116
|
@invalid_documents = nil # make sure user can't set rev maybe? or is that legal?
|
75
117
|
end
|
@@ -90,7 +132,7 @@ class CouchDocumentTests
|
|
90
132
|
def test_push_new_documents
|
91
133
|
@valid_documents.each { |doc|
|
92
134
|
# put the document on the server, grab the server's response
|
93
|
-
res = doc.push!($couchdb)
|
135
|
+
res = doc.push!(:push_to => $couchdb)
|
94
136
|
# check that the server included "ok"=>true in its response
|
95
137
|
assert(res["ok"])
|
96
138
|
# check that the doc now has an id and a rev
|
@@ -115,7 +157,7 @@ class CouchDocumentTests
|
|
115
157
|
doc.buttonCount = 5
|
116
158
|
doc.friends = ["alan", "alex", "all me other mates"]
|
117
159
|
|
118
|
-
res = doc.push!($couchdb)
|
160
|
+
res = doc.push!(:push_to => $couchdb)
|
119
161
|
assert(res["ok"])
|
120
162
|
|
121
163
|
# pull it from the database again
|
@@ -132,7 +174,7 @@ class CouchDocumentTests
|
|
132
174
|
@existing_valid_documents.each { |doc|
|
133
175
|
doc._rev = "hurr"
|
134
176
|
assert_raise RestClient::BadRequest do
|
135
|
-
res = doc.push!($couchdb)
|
177
|
+
res = doc.push!(:push_to => $couchdb)
|
136
178
|
end
|
137
179
|
}
|
138
180
|
end
|
@@ -140,6 +182,117 @@ class CouchDocumentTests
|
|
140
182
|
end
|
141
183
|
end
|
142
184
|
|
185
|
+
# Define a couple of classes with different push_to values for the next test
|
186
|
+
class Hand < ShyCouch::Data::CouchDocument
|
187
|
+
push_to ShyCouch::getDB(
|
188
|
+
"db"=> {
|
189
|
+
"host" => "localhost",
|
190
|
+
"port" => 5984,
|
191
|
+
"name" => "test1",
|
192
|
+
"user" => "cerales",
|
193
|
+
"password" => "password"
|
194
|
+
}
|
195
|
+
)
|
196
|
+
end
|
197
|
+
class Foot < ShyCouch::Data::CouchDocument
|
198
|
+
push_to ShyCouch::getDB(
|
199
|
+
"db"=> {
|
200
|
+
"host" => "localhost",
|
201
|
+
"port" => 5984,
|
202
|
+
"name" => "test2",
|
203
|
+
"user" => "cerales",
|
204
|
+
"password" => "password"
|
205
|
+
}
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
class TestCouchDocumentPushingDefinedOnClass < Test::Unit::TestCase
|
210
|
+
# Set up two different databases
|
211
|
+
# The class definitions here are implicitly a test, too
|
212
|
+
# TODO - find a way to test this nicely
|
213
|
+
def setup
|
214
|
+
Hand.target_db.init!
|
215
|
+
Foot.target_db.init!
|
216
|
+
@hand = Hand.new
|
217
|
+
@hand.push!
|
218
|
+
@foot = Foot.new
|
219
|
+
@foot.push!
|
220
|
+
end
|
221
|
+
|
222
|
+
def test_pushing_documents_with_different_class_push_to
|
223
|
+
# create an instance of both kinds of document and push them
|
224
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
225
|
+
hand = Hand.new
|
226
|
+
hand.push!
|
227
|
+
end
|
228
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
229
|
+
foot = Foot.new
|
230
|
+
foot.push!
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def test_pulling_documents_with_different_class_push_to
|
235
|
+
# Create a couple of documents
|
236
|
+
# Test it on the doc instance
|
237
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
238
|
+
@hand.pull!
|
239
|
+
end
|
240
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
241
|
+
@foot.pull!
|
242
|
+
end
|
243
|
+
|
244
|
+
# Test it on the database objects
|
245
|
+
handDB = Hand.target_db
|
246
|
+
footDB = Foot.target_db
|
247
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
248
|
+
hand2 = handDB.pull_document @hand
|
249
|
+
assert_equal @hand, hand2
|
250
|
+
end
|
251
|
+
assert_nothing_raised ShyCouch::ShyCouchError do
|
252
|
+
foot2 = footDB.pull_document @foot
|
253
|
+
assert_equal @foot, foot2
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_class_push_and_pull_overriden_by_method_argument
|
259
|
+
# push some documents, try to pull them from a db where they don't exist
|
260
|
+
hand = Hand.new
|
261
|
+
foot = Foot.new
|
262
|
+
hand.push!
|
263
|
+
foot.push!
|
264
|
+
|
265
|
+
assert_raises ShyCouch::ResourceNotFound do
|
266
|
+
hand.pull! :pull_from => Foot.target_db
|
267
|
+
foot.pull! :pull_from => Hand.target_db
|
268
|
+
end
|
269
|
+
assert_nothing_raised ShyCouch::ResourceNotFound do
|
270
|
+
# verify that they can be pulled from their default db
|
271
|
+
hand.pull!
|
272
|
+
foot.pull!
|
273
|
+
end
|
274
|
+
|
275
|
+
# now test that they can be successfully pulled from other db
|
276
|
+
hand.push! :push_to => Foot.target_db
|
277
|
+
foot.push! :push_to => Hand.target_db
|
278
|
+
|
279
|
+
assert_nothing_raised ShyCouch::ResourceNotFound do
|
280
|
+
hand.pull! :pull_from => Foot.target_db
|
281
|
+
foot.pull! :pull_from => Foot.target_db
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
def test_pushing_documents_in_collection
|
286
|
+
# TODO
|
287
|
+
end
|
288
|
+
|
289
|
+
def teardown
|
290
|
+
super
|
291
|
+
Hand.target_db.delete!
|
292
|
+
Foot.target_db.delete!
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
143
296
|
class CouchDocumentCollectionPushingAndPulling < ShyCouchTestHelper
|
144
297
|
|
145
298
|
def setup
|
@@ -163,6 +316,17 @@ class CouchDocumentCollectionPushingAndPulling < ShyCouchTestHelper
|
|
163
316
|
assert_equal(item, collection2[i])
|
164
317
|
end
|
165
318
|
end
|
319
|
+
|
320
|
+
def test_pull_doc_without_id
|
321
|
+
document = ShyCouch::Data::CouchDocument.new
|
322
|
+
assert_raises ShyCouch::DocumentValidationError do
|
323
|
+
document.pull!
|
324
|
+
end
|
325
|
+
assert_raises ShyCouch::DocumentValidationError do
|
326
|
+
document = document.pull
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
166
330
|
|
167
331
|
def test_pull_all_docs_forcefully
|
168
332
|
# tests pulling w/ exclamation yo
|
@@ -238,4 +402,4 @@ end
|
|
238
402
|
# super
|
239
403
|
# end
|
240
404
|
#
|
241
|
-
# end
|
405
|
+
# end
|
data/test/test_couchdb_api.rb
CHANGED
@@ -3,16 +3,55 @@ require_relative '../lib/ShyCouch.rb'
|
|
3
3
|
|
4
4
|
class TestCouchDBAPI < Test::Unit::TestCase
|
5
5
|
def setup
|
6
|
-
|
7
|
-
|
6
|
+
valid_settings = $settings
|
7
|
+
$database = ShyCouch.getDB(valid_settings)
|
8
8
|
end
|
9
9
|
|
10
10
|
def teardown
|
11
|
-
|
12
|
-
|
11
|
+
$database.delete!
|
12
|
+
$database = nil
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_connection
|
16
|
-
|
16
|
+
assert_equal(true, $database.connect["ok"])
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
|
+
def test_invalid_settings
|
20
|
+
invalid_settings = []
|
21
|
+
invalid_settings << {
|
22
|
+
"db"=> {
|
23
|
+
# Has "database" instead of "name"
|
24
|
+
"host" => "localhost",
|
25
|
+
"port" => 5984,
|
26
|
+
"database" => "test",
|
27
|
+
"user" => "cerales",
|
28
|
+
"password" => "password"
|
29
|
+
}
|
30
|
+
}
|
31
|
+
invalid_settings << {
|
32
|
+
"db" => {
|
33
|
+
# Missing port
|
34
|
+
"host" => "localhost",
|
35
|
+
"name" => "test"
|
36
|
+
}
|
37
|
+
}
|
38
|
+
invalid_settings << {
|
39
|
+
"db" => {
|
40
|
+
# Has username but no password
|
41
|
+
"host" => "localhost",
|
42
|
+
"port" => 5984,
|
43
|
+
"database" => "test",
|
44
|
+
"user" => "cerales"
|
45
|
+
}
|
46
|
+
}
|
47
|
+
# should throw an error when the settings hash is invalid
|
48
|
+
invalid_settings.each do |settings|
|
49
|
+
assert_raises ShyCouch::DatabaseError do
|
50
|
+
ShyCouch::CouchDatabase.new settings
|
51
|
+
end
|
52
|
+
assert_raises ArgumentError do
|
53
|
+
ShyCouch::CouchDatabase.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|