couchrest 0.33 → 0.34

Sign up to get free protection for your applications and to get access to all the features.
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