couchrest 0.33 → 0.34

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.
Files changed (45) hide show
  1. data/README.md +8 -127
  2. data/Rakefile +20 -36
  3. data/THANKS.md +2 -1
  4. data/history.txt +25 -0
  5. data/lib/couchrest.rb +5 -4
  6. data/lib/couchrest/core/database.rb +26 -21
  7. data/lib/couchrest/core/document.rb +4 -3
  8. data/lib/couchrest/helper/streamer.rb +11 -4
  9. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  10. data/lib/couchrest/mixins/callbacks.rb +187 -138
  11. data/lib/couchrest/mixins/collection.rb +3 -16
  12. data/lib/couchrest/mixins/extended_attachments.rb +1 -1
  13. data/lib/couchrest/mixins/extended_document_mixins.rb +1 -0
  14. data/lib/couchrest/mixins/properties.rb +71 -44
  15. data/lib/couchrest/mixins/validation.rb +18 -29
  16. data/lib/couchrest/more/casted_model.rb +29 -1
  17. data/lib/couchrest/more/extended_document.rb +73 -25
  18. data/lib/couchrest/more/property.rb +20 -1
  19. data/lib/couchrest/support/class.rb +81 -67
  20. data/lib/couchrest/support/rails.rb +12 -5
  21. data/lib/couchrest/validation/auto_validate.rb +5 -9
  22. data/lib/couchrest/validation/validators/confirmation_validator.rb +11 -3
  23. data/lib/couchrest/validation/validators/format_validator.rb +8 -3
  24. data/lib/couchrest/validation/validators/length_validator.rb +10 -5
  25. data/lib/couchrest/validation/validators/numeric_validator.rb +6 -1
  26. data/lib/couchrest/validation/validators/required_field_validator.rb +8 -3
  27. data/spec/couchrest/core/couchrest_spec.rb +48 -2
  28. data/spec/couchrest/core/database_spec.rb +22 -10
  29. data/spec/couchrest/core/document_spec.rb +9 -1
  30. data/spec/couchrest/helpers/streamer_spec.rb +31 -2
  31. data/spec/couchrest/more/attribute_protection_spec.rb +94 -0
  32. data/spec/couchrest/more/casted_extended_doc_spec.rb +2 -4
  33. data/spec/couchrest/more/casted_model_spec.rb +230 -1
  34. data/spec/couchrest/more/extended_doc_attachment_spec.rb +2 -2
  35. data/spec/couchrest/more/extended_doc_spec.rb +173 -15
  36. data/spec/couchrest/more/extended_doc_view_spec.rb +17 -10
  37. data/spec/couchrest/more/property_spec.rb +97 -3
  38. data/spec/fixtures/more/article.rb +4 -3
  39. data/spec/fixtures/more/card.rb +1 -1
  40. data/spec/fixtures/more/cat.rb +5 -3
  41. data/spec/fixtures/more/event.rb +4 -1
  42. data/spec/fixtures/more/invoice.rb +2 -2
  43. data/spec/fixtures/more/person.rb +1 -0
  44. data/spec/fixtures/more/user.rb +22 -0
  45. metadata +46 -13
@@ -65,22 +65,27 @@ describe CouchRest::Database do
65
65
 
66
66
  describe "saving a view" do
67
67
  before(:each) do
68
- @view = {'test' => {'map' => 'function(doc) {
69
- if (doc.word && !/\W/.test(doc.word)) {
70
- emit(doc.word,null);
68
+ @view = {'test' => {'map' => <<-JS
69
+ function(doc) {
70
+ var reg = new RegExp("\\\\W");
71
+ if (doc.word && !reg.test(doc.word)) {
72
+ emit(doc.word,null);
73
+ }
71
74
  }
72
- }'}}
75
+ JS
76
+ }}
73
77
  @db.save_doc({
74
78
  "_id" => "_design/test",
75
79
  :views => @view
76
80
  })
77
81
  end
78
82
  it "should work properly" do
79
- @db.bulk_save([
83
+ r = @db.bulk_save([
80
84
  {"word" => "once"},
81
85
  {"word" => "and again"}
82
86
  ])
83
- @db.view('test/test')['total_rows'].should == 1
87
+ r = @db.view('test/test')
88
+ r['total_rows'].should == 1
84
89
  end
85
90
  it "should round trip" do
86
91
  @db.get("_design/test")['views'].should == @view
@@ -131,9 +136,16 @@ describe CouchRest::Database do
131
136
  rs = @db.view('first/test', :include_docs => true) do |row|
132
137
  rows << row
133
138
  end
134
- rows.length.should == 4
139
+ rows.length.should == 3
135
140
  rs["total_rows"].should == 3
136
141
  end
142
+ it "should accept a block with several params" do
143
+ rows = []
144
+ rs = @db.view('first/test', :include_docs => true, :limit => 2) do |row|
145
+ rows << row
146
+ end
147
+ rows.length.should == 2
148
+ end
137
149
  end
138
150
 
139
151
  describe "GET (document by id) when the doc exists" do
@@ -253,7 +265,7 @@ describe CouchRest::Database do
253
265
  describe "PUT attachment from file" do
254
266
  before(:each) do
255
267
  filename = FIXTURE_PATH + '/attachments/couchdb.png'
256
- @file = File.open(filename)
268
+ @file = File.open(filename, "rb")
257
269
  end
258
270
  after(:each) do
259
271
  @file.close
@@ -552,7 +564,7 @@ describe CouchRest::Database do
552
564
  newdoc['artist'].should == 'Zappa'
553
565
  end
554
566
  it "should fail without an _id" do
555
- lambda{@db.copy({"not"=>"a real doc"})}.should raise_error(ArgumentError)
567
+ lambda{@db.copy_doc({"not"=>"a real doc"})}.should raise_error(ArgumentError)
556
568
  end
557
569
  end
558
570
  describe "to an existing location" do
@@ -711,4 +723,4 @@ describe CouchRest::Database do
711
723
  end
712
724
 
713
725
 
714
- end
726
+ end
@@ -83,6 +83,14 @@ describe CouchRest::Document do
83
83
  @doc.id.should == @resp["id"]
84
84
  @doc.rev.should == @resp["rev"]
85
85
  end
86
+ it "should generate a correct URI" do
87
+ @doc.uri.should == "#{@db.root}/#{@doc.id}"
88
+ URI.parse(@doc.uri).to_s.should == @doc.uri
89
+ end
90
+ it "should generate a correct URI with revision" do
91
+ @doc.uri(true).should == "#{@db.root}/#{@doc.id}?rev=#{@doc.rev}"
92
+ URI.parse(@doc.uri(true)).to_s.should == @doc.uri(true)
93
+ end
86
94
  end
87
95
 
88
96
  describe "bulk saving" do
@@ -264,4 +272,4 @@ describe "dealing with attachments" do
264
272
  end
265
273
  end
266
274
 
267
- end
275
+ end
@@ -9,6 +9,14 @@ describe CouchRest::Streamer do
9
9
  @streamer = CouchRest::Streamer.new(@db)
10
10
  @docs = (1..1000).collect{|i| {:integer => i, :string => i.to_s}}
11
11
  @db.bulk_save(@docs)
12
+ @db.save_doc({
13
+ "_id" => "_design/first",
14
+ :views => {
15
+ :test => {
16
+ :map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}"
17
+ }
18
+ }
19
+ })
12
20
  end
13
21
 
14
22
  it "should yield each row in a view" do
@@ -19,5 +27,26 @@ describe CouchRest::Streamer do
19
27
  end
20
28
  count.should == 1001
21
29
  end
22
-
23
- end
30
+
31
+ it "should accept several params" do
32
+ count = 0
33
+ @streamer.view("_design/first/_view/test", :include_docs => true, :limit => 5) do |row|
34
+ count += 1
35
+ end
36
+ count.should == 5
37
+ end
38
+
39
+ it "should accept both view formats" do
40
+ count = 0
41
+ @streamer.view("_design/first/_view/test") do |row|
42
+ count += 1
43
+ end
44
+ count.should == 2000
45
+ count = 0
46
+ @streamer.view("first/test") do |row|
47
+ count += 1
48
+ end
49
+ count.should == 2000
50
+ end
51
+
52
+ end
@@ -0,0 +1,94 @@
1
+ require File.expand_path("../../../spec_helper", __FILE__)
2
+
3
+ describe "ExtendedDocument", "no declarations" do
4
+ class NoProtection < CouchRest::ExtendedDocument
5
+ use_database TEST_SERVER.default_database
6
+ property :name
7
+ property :phone
8
+ end
9
+
10
+ it "should not protect anything through new" do
11
+ user = NoProtection.new(:name => "will", :phone => "555-5555")
12
+
13
+ user.name.should == "will"
14
+ user.phone.should == "555-5555"
15
+ end
16
+
17
+ it "should not protect anything through attributes=" do
18
+ user = NoProtection.new
19
+ user.attributes = {:name => "will", :phone => "555-5555"}
20
+
21
+ user.name.should == "will"
22
+ user.phone.should == "555-5555"
23
+ end
24
+ end
25
+
26
+ describe "ExtendedDocument", "accessible flag" do
27
+ class WithAccessible < CouchRest::ExtendedDocument
28
+ use_database TEST_SERVER.default_database
29
+ property :name, :accessible => true
30
+ property :admin, :default => false
31
+ end
32
+
33
+ it "should recognize accessible properties" do
34
+ props = WithAccessible.accessible_properties.map { |prop| prop.name}
35
+ props.should include("name")
36
+ props.should_not include("admin")
37
+ end
38
+
39
+ it "should protect non-accessible properties set through new" do
40
+ user = WithAccessible.new(:name => "will", :admin => true)
41
+
42
+ user.name.should == "will"
43
+ user.admin.should == false
44
+ end
45
+
46
+ it "should protect non-accessible properties set through attributes=" do
47
+ user = WithAccessible.new
48
+ user.attributes = {:name => "will", :admin => true}
49
+
50
+ user.name.should == "will"
51
+ user.admin.should == false
52
+ end
53
+ end
54
+
55
+ describe "ExtendedDocument", "protected flag" do
56
+ class WithProtected < CouchRest::ExtendedDocument
57
+ use_database TEST_SERVER.default_database
58
+ property :name
59
+ property :admin, :default => false, :protected => true
60
+ end
61
+
62
+ it "should recognize protected properties" do
63
+ props = WithProtected.protected_properties.map { |prop| prop.name}
64
+ props.should_not include("name")
65
+ props.should include("admin")
66
+ end
67
+
68
+ it "should protect non-accessible properties set through new" do
69
+ user = WithProtected.new(:name => "will", :admin => true)
70
+
71
+ user.name.should == "will"
72
+ user.admin.should == false
73
+ end
74
+
75
+ it "should protect non-accessible properties set through attributes=" do
76
+ user = WithProtected.new
77
+ user.attributes = {:name => "will", :admin => true}
78
+
79
+ user.name.should == "will"
80
+ user.admin.should == false
81
+ end
82
+ end
83
+
84
+ describe "ExtendedDocument", "protected flag" do
85
+ class WithBoth < CouchRest::ExtendedDocument
86
+ use_database TEST_SERVER.default_database
87
+ property :name, :accessible => true
88
+ property :admin, :default => false, :protected => true
89
+ end
90
+
91
+ it "should raise an error when both are set" do
92
+ lambda { WithBoth.new }.should raise_error
93
+ end
94
+ end
@@ -43,16 +43,14 @@ describe "assigning a value to casted attribute after initializing an object" do
43
43
  @car.driver.should be_nil
44
44
  end
45
45
 
46
- # Note that this isn't casting the attribute, it's just assigning it a value
47
- # (see "should not cast attribute")
48
46
  it "should let you assign the value" do
49
47
  @car.driver = @driver
50
48
  @car.driver.name.should == 'Matt'
51
49
  end
52
50
 
53
- it "should not cast attribute" do
51
+ it "should cast attribute" do
54
52
  @car.driver = JSON.parse(JSON.generate(@driver))
55
- @car.driver.should_not be_instance_of(Driver)
53
+ @car.driver.should be_instance_of(Driver)
56
54
  end
57
55
 
58
56
  end
@@ -4,6 +4,8 @@ require File.expand_path('../../../spec_helper', __FILE__)
4
4
  require File.join(FIXTURE_PATH, 'more', 'card')
5
5
  require File.join(FIXTURE_PATH, 'more', 'cat')
6
6
  require File.join(FIXTURE_PATH, 'more', 'person')
7
+ require File.join(FIXTURE_PATH, 'more', 'question')
8
+ require File.join(FIXTURE_PATH, 'more', 'course')
7
9
 
8
10
 
9
11
  class WithCastedModelMixin < Hash
@@ -21,6 +23,26 @@ class DummyModel < CouchRest::ExtendedDocument
21
23
  property :keywords, :cast_as => ["String"]
22
24
  end
23
25
 
26
+ class CastedCallbackDoc < CouchRest::ExtendedDocument
27
+ use_database TEST_SERVER.default_database
28
+ raise "Default DB not set" if TEST_SERVER.default_database.nil?
29
+ property :callback_model, :cast_as => 'WithCastedCallBackModel'
30
+ end
31
+ class WithCastedCallBackModel < Hash
32
+ include CouchRest::CastedModel
33
+ include CouchRest::Validation
34
+ property :name
35
+ property :run_before_validate
36
+ property :run_after_validate
37
+
38
+ before_validate do |object|
39
+ object.run_before_validate = true
40
+ end
41
+ after_validate do |object|
42
+ object.run_after_validate = true
43
+ end
44
+ end
45
+
24
46
  describe CouchRest::CastedModel do
25
47
 
26
48
  describe "A non hash class including CastedModel" do
@@ -106,7 +128,40 @@ describe CouchRest::CastedModel do
106
128
  @obj.keywords.should be_an_instance_of(Array)
107
129
  @obj.keywords.first.should == 'couch'
108
130
  end
131
+ end
132
+
133
+ describe "update attributes without saving" do
134
+ before(:each) do
135
+ @question = Question.new(:q => "What is your quest?", :a => "To seek the Holy Grail")
136
+ end
137
+ it "should work for attribute= methods" do
138
+ @question.q.should == "What is your quest?"
139
+ @question['a'].should == "To seek the Holy Grail"
140
+ @question.update_attributes_without_saving(:q => "What is your favorite color?", 'a' => "Blue")
141
+ @question['q'].should == "What is your favorite color?"
142
+ @question.a.should == "Blue"
143
+ end
144
+
145
+ it "should also work for attributes= alias" do
146
+ @question.respond_to?(:attributes=).should be_true
147
+ @question.attributes = {:q => "What is your favorite color?", 'a' => "Blue"}
148
+ @question['q'].should == "What is your favorite color?"
149
+ @question.a.should == "Blue"
150
+ end
151
+
152
+ it "should flip out if an attribute= method is missing" do
153
+ lambda {
154
+ @q.update_attributes_without_saving('foo' => "something", :a => "No green")
155
+ }.should raise_error(NoMethodError)
156
+ end
109
157
 
158
+ it "should not change any attributes if there is an error" do
159
+ lambda {
160
+ @q.update_attributes_without_saving('foo' => "something", :a => "No green")
161
+ }.should raise_error(NoMethodError)
162
+ @question.q.should == "What is your quest?"
163
+ @question.a.should == "To seek the Holy Grail"
164
+ end
110
165
  end
111
166
 
112
167
  describe "saved document with casted models" do
@@ -154,6 +209,10 @@ describe CouchRest::CastedModel do
154
209
  toy = CatToy.new :name => "Mouse"
155
210
  @cat.toys.push(toy)
156
211
  @cat.save.should be_true
212
+ @cat = Cat.get @cat.id
213
+ @cat.toys.class.should == CastedArray
214
+ @cat.toys.first.class.should == CatToy
215
+ @cat.toys.first.should === toy
157
216
  end
158
217
 
159
218
  it "should fail because name is not present" do
@@ -165,13 +224,183 @@ describe CouchRest::CastedModel do
165
224
 
166
225
  it "should not fail if the casted model doesn't have validation" do
167
226
  Cat.property :masters, :cast_as => ['Person'], :default => []
168
- Cat.validates_present :name
227
+ Cat.validates_presence_of :name
169
228
  cat = Cat.new(:name => 'kitty')
170
229
  cat.should be_valid
171
230
  cat.masters.push Person.new
172
231
  cat.should be_valid
173
232
  end
233
+ end
234
+
235
+ describe "calling valid?" do
236
+ before :each do
237
+ @cat = Cat.new
238
+ @toy1 = CatToy.new
239
+ @toy2 = CatToy.new
240
+ @toy3 = CatToy.new
241
+ @cat.favorite_toy = @toy1
242
+ @cat.toys << @toy2
243
+ @cat.toys << @toy3
244
+ end
245
+
246
+ describe "on the top document" do
247
+ it "should put errors on all invalid casted models" do
248
+ @cat.should_not be_valid
249
+ @cat.errors.should_not be_empty
250
+ @toy1.errors.should_not be_empty
251
+ @toy2.errors.should_not be_empty
252
+ @toy3.errors.should_not be_empty
253
+ end
254
+
255
+ it "should not put errors on valid casted models" do
256
+ @toy1.name = "Feather"
257
+ @toy2.name = "Twine"
258
+ @cat.should_not be_valid
259
+ @cat.errors.should_not be_empty
260
+ @toy1.errors.should be_empty
261
+ @toy2.errors.should be_empty
262
+ @toy3.errors.should_not be_empty
263
+ end
264
+ end
265
+
266
+ describe "on a casted model property" do
267
+ it "should only validate itself" do
268
+ @toy1.should_not be_valid
269
+ @toy1.errors.should_not be_empty
270
+ @cat.errors.should be_empty
271
+ @toy2.errors.should be_empty
272
+ @toy3.errors.should be_empty
273
+ end
274
+ end
275
+
276
+ describe "on a casted model inside a casted collection" do
277
+ it "should only validate itself" do
278
+ @toy2.should_not be_valid
279
+ @toy2.errors.should_not be_empty
280
+ @cat.errors.should be_empty
281
+ @toy1.errors.should be_empty
282
+ @toy3.errors.should be_empty
283
+ end
284
+ end
285
+ end
286
+
287
+ describe "calling new? on a casted model" do
288
+ before :each do
289
+ reset_test_db!
290
+ @cat = Cat.new(:name => 'Sockington')
291
+ @favorite_toy = CatToy.new(:name => 'Catnip Ball')
292
+ @cat.favorite_toy = @favorite_toy
293
+ @cat.toys << CatToy.new(:name => 'Fuzzy Stick')
294
+ end
174
295
 
296
+ it "should be true on new" do
297
+ CatToy.new.should be_new
298
+ CatToy.new.new_record?.should be_true
299
+ end
300
+
301
+ it "should be true after assignment" do
302
+ @cat.should be_new
303
+ @cat.favorite_toy.should be_new
304
+ @cat.toys.first.should be_new
305
+ end
306
+
307
+ it "should not be true after create or save" do
308
+ @cat.create
309
+ @cat.save
310
+ @cat.favorite_toy.should_not be_new
311
+ @cat.toys.first.should_not be_new
312
+ end
313
+
314
+ it "should not be true after get from the database" do
315
+ @cat.save
316
+ @cat = Cat.get(@cat.id)
317
+ @cat.favorite_toy.should_not be_new
318
+ @cat.toys.first.should_not be_new
319
+ end
320
+
321
+ it "should still be true after a failed create or save" do
322
+ @cat.name = nil
323
+ @cat.create.should be_false
324
+ @cat.save.should be_false
325
+ @cat.favorite_toy.should be_new
326
+ @cat.toys.first.should be_new
327
+ end
328
+ end
329
+
330
+ describe "calling base_doc from a nested casted model" do
331
+ before :each do
332
+ @course = Course.new(:title => 'Science 101')
333
+ @professor = Person.new(:name => 'Professor Plum')
334
+ @cat = Cat.new(:name => 'Scratchy')
335
+ @toy1 = CatToy.new
336
+ @toy2 = CatToy.new
337
+ @course.professor = @professor
338
+ @professor.pet = @cat
339
+ @cat.favorite_toy = @toy1
340
+ @cat.toys << @toy2
341
+ end
342
+
343
+ it "should reference the top document for" do
344
+ @course.base_doc.should === @course
345
+ @professor.casted_by.should === @course
346
+ @professor.base_doc.should === @course
347
+ @cat.base_doc.should === @course
348
+ @toy1.base_doc.should === @course
349
+ @toy2.base_doc.should === @course
350
+ end
351
+
352
+ it "should call setter on top document" do
353
+ @toy1.base_doc.should_not be_nil
354
+ @toy1.base_doc.title = 'Tom Foolery'
355
+ @course.title.should == 'Tom Foolery'
356
+ end
357
+
358
+ it "should return nil if not yet casted" do
359
+ person = Person.new
360
+ person.base_doc.should == nil
361
+ end
362
+ end
363
+
364
+ describe "calling base_doc.save from a nested casted model" do
365
+ before :each do
366
+ reset_test_db!
367
+ @cat = Cat.new(:name => 'Snowball')
368
+ @toy = CatToy.new
369
+ @cat.favorite_toy = @toy
370
+ end
371
+
372
+ it "should not save parent document when casted model is invalid" do
373
+ @toy.should_not be_valid
374
+ @toy.base_doc.save.should be_false
375
+ lambda{@toy.base_doc.save!}.should raise_error
376
+ end
377
+
378
+ it "should save parent document when nested casted model is valid" do
379
+ @toy.name = "Mr Squeaks"
380
+ @toy.should be_valid
381
+ @toy.base_doc.save.should be_true
382
+ lambda{@toy.base_doc.save!}.should_not raise_error
383
+ end
175
384
  end
176
385
 
386
+ describe "callbacks" do
387
+ before(:each) do
388
+ @doc = CastedCallbackDoc.new
389
+ @model = WithCastedCallBackModel.new
390
+ @doc.callback_model = @model
391
+ end
392
+
393
+ describe "validate" do
394
+ it "should run before_validate before validating" do
395
+ @model.run_before_validate.should be_nil
396
+ @model.should be_valid
397
+ @model.run_before_validate.should be_true
398
+ end
399
+ it "should run after_validate after validating" do
400
+ @model.run_after_validate.should be_nil
401
+ @model.should be_valid
402
+ @model.run_after_validate.should be_true
403
+ end
404
+ end
405
+ end
177
406
  end