couchobject 0.5.0 → 0.6.0

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 (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