couchobject 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +30 -6
  3. data/README.txt +580 -42
  4. data/TODO +2 -2
  5. data/config/hoe.rb +1 -1
  6. data/lib/couch_object.rb +7 -2
  7. data/lib/couch_object/database.rb +19 -34
  8. data/lib/couch_object/document.rb +13 -6
  9. data/lib/couch_object/error_classes.rb +110 -0
  10. data/lib/couch_object/persistable.rb +954 -36
  11. data/lib/couch_object/persistable/has_many_relations_array.rb +91 -0
  12. data/lib/couch_object/persistable/meta_classes.rb +568 -0
  13. data/lib/couch_object/persistable/overloaded_methods.rb +209 -0
  14. data/lib/couch_object/server.rb +1 -1
  15. data/lib/couch_object/utils.rb +44 -0
  16. data/lib/couch_object/version.rb +1 -1
  17. data/lib/couch_object/view.rb +129 -6
  18. data/script/console +0 -0
  19. data/script/destroy +0 -0
  20. data/script/generate +0 -0
  21. data/script/txt2html +0 -0
  22. data/spec/database_spec.rb +23 -31
  23. data/spec/database_spec.rb.orig +173 -0
  24. data/spec/document_spec.rb +21 -3
  25. data/spec/integration/database_integration_spec.rb +46 -15
  26. data/spec/integration/integration_helper.rb +3 -3
  27. data/spec/persistable/callback.rb +44 -0
  28. data/spec/persistable/callback_spec.rb +44 -0
  29. data/spec/persistable/cloning.rb +77 -0
  30. data/spec/persistable/cloning_spec.rb +77 -0
  31. data/spec/persistable/comparing_objects.rb +350 -0
  32. data/spec/persistable/comparing_objects_spec.rb +350 -0
  33. data/spec/persistable/deleting.rb +113 -0
  34. data/spec/persistable/deleting_spec.rb +113 -0
  35. data/spec/persistable/error_messages.rb +32 -0
  36. data/spec/persistable/error_messages_spec.rb +32 -0
  37. data/spec/persistable/loading.rb +339 -0
  38. data/spec/persistable/loading_spec.rb +339 -0
  39. data/spec/persistable/new_methods.rb +70 -0
  40. data/spec/persistable/new_methods_spec.rb +70 -0
  41. data/spec/persistable/persistable_helper.rb +194 -0
  42. data/spec/persistable/relations.rb +470 -0
  43. data/spec/persistable/relations_spec.rb +470 -0
  44. data/spec/persistable/saving.rb +137 -0
  45. data/spec/persistable/saving_spec.rb +137 -0
  46. data/spec/persistable/setting_storage_location.rb +65 -0
  47. data/spec/persistable/setting_storage_location_spec.rb +65 -0
  48. data/spec/persistable/timestamps.rb +76 -0
  49. data/spec/persistable/timestamps_spec.rb +76 -0
  50. data/spec/persistable/unsaved_changes.rb +211 -0
  51. data/spec/persistable/unsaved_changes_spec.rb +211 -0
  52. data/spec/server_spec.rb +5 -5
  53. data/spec/utils_spec.rb +60 -0
  54. data/spec/view_spec.rb +40 -7
  55. data/website/index.html +22 -7
  56. data/website/index.txt +13 -5
  57. metadata +93 -61
  58. data/bin/couch_ruby_view_requestor +0 -81
  59. data/lib/couch_object/model.rb +0 -5
  60. data/lib/couch_object/proc_condition.rb +0 -14
  61. data/spec/model_spec.rb +0 -5
  62. data/spec/persistable_spec.rb +0 -91
  63. data/spec/proc_condition_spec.rb +0 -26
@@ -0,0 +1,470 @@
1
+ require File.dirname(__FILE__) + '/persistable_helper.rb'
2
+
3
+ describe CouchObject::Persistable, "methods added and their values: " do
4
+ before(:each) do
5
+ @building = ApartmentBuilding.new
6
+ @inhabitant = Inhabitant.new
7
+ @apartment = Apartment.new
8
+
9
+ @building.stub!(:couch_load_has_many_relations).
10
+ and_return(CouchObject::Persistable::HasManyRelation.new(@building))
11
+ @inhabitant.stub!(:couch_load_has_many_relations).
12
+ and_return(CouchObject::Persistable::HasManyRelation.new(@inhabitant))
13
+ @apartment.stub!(:couch_load_has_many_relations).
14
+ and_return(CouchObject::Persistable::HasManyRelation.new(@apartment))
15
+ end
16
+
17
+ it "should give ApartmentBuilding inhabitant getters and setters" do
18
+ @building.respond_to?(:inhabitants).should == true
19
+ @building.respond_to?(:apartments).should == true
20
+ end
21
+
22
+ it "should give Inhabitant apartment_building getters and setters" do
23
+ @inhabitant.respond_to?(:apartment_building).should == true
24
+ @inhabitant.respond_to?(:apartment_building=).should == true
25
+ end
26
+
27
+ it "should give Apartment apartment_building getters and setters" do
28
+ @apartment.respond_to?(:apartment_building).should == true
29
+ @apartment.respond_to?(:apartment_building=).should == true
30
+ end
31
+
32
+ it "should be able to list the classes it has relations to" do
33
+ @building.respond_to?(:has_many).should == true
34
+ @building.respond_to?(:belongs_to).should == true
35
+ @apartment.respond_to?(:has_many).should == true
36
+ @apartment.respond_to?(:belongs_to).should == true
37
+ @inhabitant.respond_to?(:has_many).should == true
38
+ @inhabitant.respond_to?(:belongs_to).should == true
39
+ end
40
+
41
+ it "should be able to list as what it has belongs_to relations" do
42
+ @apartment.belongs_to.each do |what_it_belongs_to|
43
+ @apartment.respond_to?("belongs_to_#{what_it_belongs_to}_as".to_sym).
44
+ should == true
45
+ @apartment.send("belongs_to_#{what_it_belongs_to}_as".to_sym).
46
+ should == :apartments
47
+ end
48
+ @inhabitant.belongs_to.each do |what_it_belongs_to|
49
+ @inhabitant.respond_to?("belongs_to_#{what_it_belongs_to}_as".to_sym).
50
+ should == true
51
+ @inhabitant.send("belongs_to_#{what_it_belongs_to}_as".to_sym).
52
+ should == :inhabitants
53
+ end
54
+ end
55
+
56
+ it "should list the has_many relations as an array" do
57
+ @building.has_many.should be_a_kind_of(Array)
58
+ @apartment.has_many.should be_a_kind_of(Array)
59
+ @inhabitant.has_many.should be_a_kind_of(Array)
60
+ end
61
+
62
+ it "should list the belongs_to relation as a symbol or nil" do
63
+ @inhabitant.belongs_to.should be_a_kind_of(Array)
64
+ @apartment.belongs_to.should be_a_kind_of(Array)
65
+ @building.belongs_to.should == []
66
+ end
67
+
68
+ it "should be able to return the names of the accessor methods used to access its relations" do
69
+
70
+ @building.has_many.should == [:inhabitants, :apartments]
71
+ @inhabitant.belongs_to.should == [:apartment_building]
72
+ @apartment.belongs_to.should == [:apartment_building]
73
+ end
74
+
75
+ end
76
+
77
+ describe CouchObject::Persistable, "getting and setting relations: " do
78
+ before(:each) do
79
+ @building_1 = ApartmentBuilding.new
80
+ @building_2 = ApartmentBuilding.new
81
+ @inhabitant_1 = Inhabitant.new
82
+ @inhabitant_2 = Inhabitant.new
83
+ @apartment = Apartment.new
84
+
85
+ @db = mock("mock db")
86
+ # Should call the DB at all...
87
+ CouchObject::Database.should_receive(:open).exactly(0).and_return(@db)
88
+
89
+ @building_1.stub!(:couch_load_has_many_relations).and_return(CouchObject::Persistable::HasManyRelation.new(@building_1))
90
+ @building_2.stub!(:couch_load_has_many_relations).and_return(CouchObject::Persistable::HasManyRelation.new(@building_2))
91
+ @inhabitant_1.stub!(:couch_load_has_many_relations).and_return(CouchObject::Persistable::HasManyRelation.new(@inhabitant_1))
92
+ @inhabitant_2.stub!(:couch_load_has_many_relations).and_return(CouchObject::Persistable::HasManyRelation.new(@inhabitant_2))
93
+ @apartment.stub!(:couch_load_has_many_relations).and_return(CouchObject::Persistable::HasManyRelation.new(@apartment))
94
+ end
95
+
96
+ it "should return an empty array for empty has_many relatoins" do
97
+ @building_1.inhabitants.should be_a_kind_of(Array)
98
+ @building_1.inhabitants.should == []
99
+ end
100
+
101
+ it "should return nil for empty belongs_to relatoins" do
102
+ @inhabitant_1.apartment_building.should == nil
103
+ end
104
+
105
+ it "should be able to add any number of object to a has_many relation and get them again" do
106
+ @building_1.inhabitants.should == []
107
+ @building_1.inhabitants << @inhabitant_1 << @inhabitant_2
108
+ # @building_1.inhabitants.size.should == 2
109
+ # @building_1.inhabitants.should == [@inhabitant_1, @inhabitant_2]
110
+ end
111
+
112
+ it "should be able to assign a class that it belongs to and retrieve it" do
113
+ @inhabitant_1.apartment_building = @building_1
114
+ @inhabitant_1.apartment_building.should == @building_1
115
+ @inhabitant_1.apartment_building = @building_2
116
+ @inhabitant_1.apartment_building.should_not == @building_1
117
+ end
118
+
119
+ it "when setting a has_many relationsship, the related class should be updated so it has a belongs_to relationsship with the other class" do
120
+ @building_1.inhabitants << @inhabitant_1
121
+ @inhabitant_1.apartment_building.should == @building_1
122
+ end
123
+
124
+ it "should be able to remove an object from a has_many relationsship" do
125
+ @building_1.inhabitants << @inhabitant_1
126
+ @building_1.inhabitants.size.should == 1
127
+ @building_1.inhabitants.remove(@inhabitant_1)
128
+ @building_1.inhabitants.size.should == 0
129
+ @inhabitant_1.apartment_building.should == nil
130
+ end
131
+
132
+ it "when setting a belongs_to relationsship, the related class should be updated so it has a has_many relationsship to the other class" do
133
+ @inhabitant_1.apartment_building = @building_1
134
+ @building_1.inhabitants.size.should == 1
135
+ @building_1.inhabitants.first.should == @inhabitant_1
136
+ end
137
+
138
+ it "should be able to set a belongs_to relationship to a master that already has children" do
139
+ @building_1.inhabitants << @inhabitant_1
140
+ @building_1.inhabitants.size.should == 1
141
+ @inhabitant_2.apartment_building = @building_1
142
+ @building_1.inhabitants.size.should == 2
143
+ end
144
+
145
+ end
146
+
147
+ describe CouchObject::Persistable, "errors when creating classes with bad relations :)" do
148
+ it "should raise an error for badly formatted belongs_to relations" do
149
+ lambda {
150
+ class Garage
151
+ include CouchObject::Persistable
152
+ belongs_to :apartment_building # missing , :as => :something
153
+ end
154
+ }.should raise_error(CouchObject::Errors::BelongsToAssociationError)
155
+
156
+ lambda {
157
+ class Lamp
158
+ include CouchObject::Persistable
159
+ belongs_to # missing :apartment_building, :as => :something
160
+ end
161
+ }.should raise_error(CouchObject::Errors::BelongsToAssociationError)
162
+
163
+ lambda {
164
+ class Mouse
165
+ include CouchObject::Persistable
166
+ belongs_to :apartment_building, :as => :animal # is correct
167
+ end
168
+ }.should_not \
169
+ raise_error(CouchObject::Errors::BelongsToAssociationError)
170
+
171
+
172
+ end
173
+
174
+ it "should raise an error for badly formatted has_many relations" do
175
+ lambda {
176
+ class Oven
177
+ include CouchObject::Persistable
178
+ has_many # missing what it is related to :users
179
+ end
180
+ }.should raise_error(CouchObject::Errors::HasManyAssociationError)
181
+
182
+ lambda {
183
+ class FavoriteOven
184
+ include CouchObject::Persistable
185
+ has_many :users
186
+ end
187
+ }.should_not \
188
+ raise_error(CouchObject::Errors::HasManyAssociationError)
189
+
190
+ end
191
+
192
+ end
193
+
194
+ describe CouchObject::Persistable, "saving related objects: " do
195
+ before(:each) do
196
+ @building = ApartmentBuilding.new
197
+ @apartment = Apartment.new
198
+
199
+ @db = mock("mock db")
200
+
201
+ content = HTTPResponse.new(JSON.unparse({
202
+ "_id" => "123BAC",
203
+ "_rev" => "946B7D1C",
204
+ }))
205
+
206
+ @document_response = CouchObject::Response.new(content)
207
+ end
208
+
209
+ it "a class should save it's has_many relations" do
210
+ CouchObject::Database.should_receive(:open).exactly(3).and_return(@db)
211
+ @db.should_receive(:post).exactly(3).and_return(@document_response)
212
+
213
+ another_apartment = @apartment.clone
214
+
215
+ @apartment.new?.should == true
216
+ another_apartment.new?.should == true
217
+ @building.new?.should == true
218
+
219
+ # Should not load the relations because it is a new object
220
+ @building.apartments.class.should == \
221
+ CouchObject::Persistable::HasManyRelation
222
+
223
+ @building.apartments << @apartment << another_apartment
224
+ @building.apartments.size.should == 2
225
+
226
+ @building.inhabitants.class.should == \
227
+ CouchObject::Persistable::HasManyRelation
228
+
229
+ @building.inhabitants.size.should == 0
230
+
231
+ @apartment.new?.should == true
232
+ another_apartment.new?.should == true
233
+ @building.new?.should == true
234
+
235
+ # Should call save on itself which results in a post, and
236
+ # then on its children which also results in a post.
237
+ # And also load it's inhabitants relations
238
+ @building.save
239
+
240
+ @apartment.new?.should == false
241
+ another_apartment.new?.should == false
242
+
243
+ end
244
+
245
+ it "should save its belongs_to relation if it is unsaved" do
246
+ CouchObject::Database.should_receive(:open).exactly(2).and_return(@db)
247
+ @db.should_receive(:post).exactly(2).and_return(@document_response)
248
+
249
+ @building.new?.should == true
250
+ @apartment.new?.should == true
251
+ @building.apartments << @apartment
252
+
253
+ @apartment.save("foo")
254
+
255
+ @apartment.new?.should == false
256
+ @building.new?.should == false
257
+
258
+ @building.id.should_not == nil
259
+ @apartment.id.should_not == nil
260
+
261
+ end
262
+
263
+ it "a class that normally is in a belongs_to relation but doesn't have a relation set should be able to save" do
264
+ CouchObject::Database.should_receive(:open).exactly(1).and_return(@db)
265
+ @db.should_receive(:post).exactly(1).and_return(@document_response)
266
+
267
+ @apartment.new?.should == true
268
+ @apartment.save("foo")
269
+ @apartment.new?.should == false
270
+ end
271
+
272
+ it "should not load the belongs_to relations if they are not already loaded before saving" do
273
+ @inhabitant_content = %{ {"_id":"50F1A07A283C1879057AFE4C4B727D35","_rev":"1957693345","belongs_to":{"inhabitants":"12A64B5156002BFD66F23F559C5AD5FA"},"class":"Inhabitant","attributes":{}}
274
+ }
275
+
276
+ response = HTTPResponse.new(@inhabitant_content)
277
+
278
+ CouchObject::Database.should_receive(:open).exactly(1).and_return(@db)
279
+
280
+ @db.should_receive(:get).once.with("foo").and_return(response)
281
+
282
+ inhabitant = Inhabitant.get_with_smart_save("foo")
283
+ inhabitant.location.should_not == nil
284
+
285
+ # Telling the object not to load its relatives
286
+ inhabitant.save
287
+
288
+ end
289
+ end
290
+
291
+ describe CouchObject::Persistable, "loading object relations: " do
292
+ before(:each) do
293
+ @db = mock("mock db")
294
+
295
+ @view_content_no_rows = %{
296
+ {"total_rows":3,"rows":[]}
297
+ }
298
+
299
+ @apartment_building_content = %{ {"_id":"A26EDC59D99D50C2CB2615404D357E49","_rev":"1756574922","class":"ApartmentBuilding","attributes":{}}
300
+ }
301
+
302
+ @inhabitant_content = %{ {"_id":"50F1A07A283C1879057AFE4C4B727D35","_rev":"1957693345","belongs_to":{"inhabitants":"12A64B5156002BFD66F23F559C5AD5FA"},"class":"Inhabitant","attributes":{}}
303
+ }
304
+
305
+ @inhabitant_content_from_view = %{
306
+ {"total_rows":2,"offset":0,"rows":[{"id":"50F1A07A283C1879057AFE4C4B727D35","key":["12A64B5156002BFD66F23F559C5AD5FA","inhabitants"],"value":{"_id":"50F1A07A283C1879057AFE4C4B727D35","_rev":"1957693345","class":"Inhabitant","attributes":{},"belongs_to":{"inhabitants":"12A64B5156002BFD66F23F559C5AD5FA"}}}]}
307
+ }
308
+
309
+ @apartment_building_inhabitants = %{
310
+ {"total_rows":2,"offset":0,"rows":[{"id":"241757E4D7D2272C78048B4007399837","key":["12A64B5156002BFD66F23F559C5AD5FA","inhabitants"],"value":{"_id":"241757E4D7D2272C78048B4007399837","_rev":"2508041460","class":"Inhabitant","attributes":{},"belongs_to":{"inhabitants":"CCC19E6CAE557D058CC8E0961507AC4C"}}},{"id":"E94C93DEDB94BFC30B9AB396D4874D52","key":["12A64B5156002BFD66F23F559C5AD5FA","inhabitants"],"value":{"_id":"E94C93DEDB94BFC30B9AB396D4874D52","_rev":"960098874","class":"Inhabitant","attributes":{},"belongs_to":{"inhabitants":"CCC19E6CAE557D058CC8E0961507AC4C"}}}]}
311
+ }
312
+
313
+ end
314
+
315
+ it "should load all has_many relations when needed" do
316
+ response = HTTPResponse.new(@apartment_building_content)
317
+ response_relation = HTTPResponse.new(@apartment_building_inhabitants)
318
+
319
+ CouchObject::Database.should_receive(:open).exactly(2).and_return(@db)
320
+
321
+ @db.should_receive(:get).with("foo").and_return(response)
322
+
323
+ # has_many relations are loaded
324
+ @db.should_receive(:get).
325
+ with("_view/couch_object_has_many_relations/related_documents?" + \
326
+ "key=[%22A26EDC59D99D50C2CB2615404D357E49%22,%22inhabitants%22]").
327
+ and_return(response_relation)
328
+
329
+ apartment_building = ApartmentBuilding.get_by_id("foo")
330
+ apartment_building.location.should_not == nil
331
+
332
+ # The content should be loaded on first request!
333
+ apartment_building.inhabitants.size.should == 2
334
+
335
+ end
336
+
337
+ it "should load belongs_to relations when needed" do
338
+ response_inhabitant = HTTPResponse.new(@inhabitant_content)
339
+ response_inhabitant_from_view = HTTPResponse.
340
+ new(@inhabitant_content_from_view)
341
+ response_apartment_buil = HTTPResponse.new(@apartment_building_content)
342
+
343
+ # 1. class gets loaded
344
+ # 2. belongs_to relation is loaded
345
+ # 3. then self is added to the belongs_to relations has_many relation
346
+ # which is first loaded from the DB
347
+ # 4. ? I don't know where it gets called...
348
+ CouchObject::Database.should_receive(:open).exactly(4).and_return(@db)
349
+
350
+ @db.should_receive(:get).with("foo").once.and_return(response_inhabitant)
351
+ @db.should_receive(:get).once.
352
+ with("12A64B5156002BFD66F23F559C5AD5FA").
353
+ and_return(response_apartment_buil)
354
+ @db.should_receive(:get).once.with("_view/couch_object_has_many_relat" + \
355
+ "ions/related_documents?key=[%22A26EDC59D99D50C2CB2615404D357E49" + \
356
+ "%22,%22inhabitants%22]").and_return(response_inhabitant_from_view)
357
+
358
+ # 1)
359
+ inhabitant = Inhabitant.get_by_id("foo","foo_db")
360
+ inhabitant.new?.should == false
361
+
362
+ # 2)
363
+ # 3)
364
+ inhabitant.apartment_building.class.should == ApartmentBuilding
365
+ inhabitant.apartment_building.new?.should == false
366
+ inhabitant.apartment_building.inhabitants.first.should == inhabitant
367
+
368
+ end
369
+
370
+ it "should be possible to tell the object not to load its has many relations" do
371
+ response = HTTPResponse.new(@apartment_building_content)
372
+
373
+ CouchObject::Database.should_receive(:open).exactly(1).and_return(@db)
374
+
375
+ @db.should_receive(:get).with("foo").and_return(response)
376
+
377
+ apartment_building = ApartmentBuilding.get_by_id("foo")
378
+ apartment_building.location.should_not == nil
379
+
380
+ # Telling the object not to load its relatives
381
+ apartment_building.do_not_load_has_many_relations
382
+
383
+ # The content should be loaded on first request!
384
+ apartment_building.inhabitants.size.should == 0
385
+
386
+ end
387
+
388
+ it "should be possible to tell the object not to load its has many relations" do
389
+ response = HTTPResponse.new(@inhabitant_content)
390
+
391
+ CouchObject::Database.should_receive(:open).exactly(1).and_return(@db)
392
+
393
+ @db.should_receive(:get).once.with("foo").and_return(response)
394
+
395
+ inhabitant = Inhabitant.get("foo")
396
+ inhabitant.location.should_not == nil
397
+
398
+ # Telling the object not to load its relatives
399
+ inhabitant.do_not_load_belongs_to_relations
400
+
401
+ # The content should be loaded on first request!
402
+ inhabitant.apartment_building.should == nil
403
+
404
+ end
405
+
406
+ end
407
+
408
+ describe CouchObject::Persistable, "has_one relations" do
409
+ before(:each) do
410
+ @person = Person.new
411
+ @life = Life.new
412
+
413
+ @db = mock("mock db")
414
+
415
+ content = HTTPResponse.new(JSON.unparse({
416
+ "_id" => "123BAC",
417
+ "_rev" => "946B7D1C",
418
+ }))
419
+
420
+ @document_response = CouchObject::Response.new(content)
421
+ end
422
+
423
+ it "should be able to set a has_one relation" do
424
+ @person.life = @life
425
+ @life.person.should == @person
426
+
427
+ @person.life = nil
428
+ @life.person.should == nil
429
+
430
+ @life.person = @person
431
+ @person.life.should == @life
432
+ end
433
+
434
+ it "should be able to save its has_one relation" do
435
+ CouchObject::Database.should_receive(:open).exactly(2).and_return(@db)
436
+ @db.should_receive(:post).exactly(2).and_return(@document_response)
437
+ @person.life = @life
438
+ @person.save
439
+ @person.new?.should == false
440
+ @life.new?.should == false
441
+ end
442
+
443
+ it "the child should save it's has_one parent when it is a new object" do
444
+ CouchObject::Database.should_receive(:open).exactly(2).and_return(@db)
445
+ @db.should_receive(:post).exactly(2).and_return(@document_response)
446
+ @person.life = @life
447
+ @life.save
448
+ @person.new?.should == false
449
+ @life.new?.should == false
450
+ end
451
+
452
+ it "should only save the child when the parent isn't new" do
453
+ CouchObject::Database.should_receive(:open).exactly(1).and_return(@db)
454
+ @db.should_receive(:post).exactly(1).and_return(@document_response)
455
+ @person.life = @life
456
+ # fake the parent into being an old object
457
+ @person.new?.should == true
458
+ @person.instance_variable_set("@id", 123)
459
+ @person.instance_variable_set("@revision", 123)
460
+ @person.new?.should == false
461
+
462
+ @life.save
463
+
464
+ @life.new?.should == false
465
+ end
466
+
467
+
468
+
469
+
470
+ end