openlogic-couchrest_model 1.0.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 (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,498 @@
1
+ # encoding: utf-8
2
+ require "spec_helper"
3
+
4
+ class WithCastedModelMixin
5
+ include CouchRest::Model::Embeddable
6
+ property :name
7
+ property :no_value
8
+ property :details, Object, :default => {}
9
+ property :casted_attribute, WithCastedModelMixin
10
+ end
11
+
12
+ class OldFashionedMixin < Hash
13
+ include CouchRest::Model::CastedModel
14
+ property :name
15
+ end
16
+
17
+ class DummyModel < CouchRest::Model::Base
18
+ use_database TEST_SERVER.default_database
19
+ raise "Default DB not set" if TEST_SERVER.default_database.nil?
20
+ property :casted_attribute, WithCastedModelMixin
21
+ property :keywords, [String]
22
+ property :old_casted_attribute, OldFashionedMixin
23
+ property :sub_models do |child|
24
+ child.property :title
25
+ end
26
+ property :param_free_sub_models do
27
+ property :title
28
+ end
29
+ end
30
+
31
+ class WithCastedCallBackModel
32
+ include CouchRest::Model::Embeddable
33
+ property :name
34
+ property :run_before_validation
35
+ property :run_after_validation
36
+
37
+ validates_presence_of :run_before_validation
38
+
39
+ before_validation do |object|
40
+ object.run_before_validation = true
41
+ end
42
+ after_validation do |object|
43
+ object.run_after_validation = true
44
+ end
45
+ end
46
+
47
+ class CastedCallbackDoc < CouchRest::Model::Base
48
+ use_database TEST_SERVER.default_database
49
+ raise "Default DB not set" if TEST_SERVER.default_database.nil?
50
+ property :callback_model, WithCastedCallBackModel
51
+ end
52
+
53
+ describe CouchRest::Model::Embeddable do
54
+
55
+ describe "isolated" do
56
+ before(:each) do
57
+ @obj = WithCastedModelMixin.new
58
+ end
59
+ it "should automatically include the property mixin and define getters and setters" do
60
+ @obj.name = 'Matt'
61
+ @obj.name.should == 'Matt'
62
+ end
63
+
64
+ it "should allow override of default" do
65
+ @obj = WithCastedModelMixin.new(:name => 'Eric', :details => {'color' => 'orange'})
66
+ @obj.name.should == 'Eric'
67
+ @obj.details['color'].should == 'orange'
68
+ end
69
+ it "should always return base_doc? as false" do
70
+ @obj.base_doc?.should be_false
71
+ end
72
+ it "should call after_initialize callback if available" do
73
+ klass = Class.new do
74
+ include CouchRest::Model::CastedModel
75
+ after_initialize :set_name
76
+ property :name
77
+ def set_name; self.name = "foobar"; end
78
+ end
79
+ @obj = klass.new
80
+ @obj.name.should eql("foobar")
81
+ end
82
+ it "should allow override of initialize with super" do
83
+ klass = Class.new do
84
+ include CouchRest::Model::Embeddable
85
+ after_initialize :set_name
86
+ property :name
87
+ def set_name; self.name = "foobar"; end
88
+ def initialize(attrs = {}); super(); end
89
+ end
90
+ @obj = klass.new
91
+ @obj.name.should eql("foobar")
92
+ end
93
+ end
94
+
95
+ describe "casted as an attribute, but without a value" do
96
+ before(:each) do
97
+ @obj = DummyModel.new
98
+ @casted_obj = @obj.casted_attribute
99
+ end
100
+ it "should be nil" do
101
+ @casted_obj.should == nil
102
+ end
103
+ end
104
+
105
+ describe "anonymous sub casted models" do
106
+ before :each do
107
+ @obj = DummyModel.new
108
+ end
109
+ it "should be empty initially" do
110
+ @obj.sub_models.should_not be_nil
111
+ @obj.sub_models.should be_empty
112
+ end
113
+ it "should be updatable using a hash" do
114
+ @obj.sub_models << {:title => 'test'}
115
+ @obj.sub_models.first.title.should eql('test')
116
+ end
117
+ it "should be empty intitally (without params)" do
118
+ @obj.param_free_sub_models.should_not be_nil
119
+ @obj.param_free_sub_models.should be_empty
120
+ end
121
+ it "should be updatable using a hash (without params)" do
122
+ @obj.param_free_sub_models << {:title => 'test'}
123
+ @obj.param_free_sub_models.first.title.should eql('test')
124
+ end
125
+ end
126
+
127
+ describe "casted as attribute" do
128
+ before(:each) do
129
+ casted = {:name => 'not whatever'}
130
+ @obj = DummyModel.new(:casted_attribute => {:name => 'whatever', :casted_attribute => casted})
131
+ @casted_obj = @obj.casted_attribute
132
+ end
133
+
134
+ it "should be available from its parent" do
135
+ @casted_obj.should be_an_instance_of(WithCastedModelMixin)
136
+ end
137
+
138
+ it "should have the getters defined" do
139
+ @casted_obj.name.should == 'whatever'
140
+ end
141
+
142
+ it "should know who casted it" do
143
+ @casted_obj.casted_by.should == @obj
144
+ end
145
+
146
+ it "should know which property casted it" do
147
+ @casted_obj.casted_by_property.should == @obj.properties.detect{|p| p.to_s == 'casted_attribute'}
148
+ end
149
+
150
+ it "should return nil for the 'no_value' attribute" do
151
+ @casted_obj.no_value.should be_nil
152
+ end
153
+
154
+ it "should return nil for the unknown attribute" do
155
+ @casted_obj["unknown"].should be_nil
156
+ end
157
+
158
+ it "should return {} for the hash attribute" do
159
+ @casted_obj.details.should == {}
160
+ end
161
+
162
+ it "should cast its own attributes" do
163
+ @casted_obj.casted_attribute.should be_instance_of(WithCastedModelMixin)
164
+ end
165
+
166
+ it "should raise an error if save or update_attributes called" do
167
+ expect { @casted_obj.casted_attribute.save }.to raise_error(NoMethodError)
168
+ expect { @casted_obj.casted_attribute.update_attributes(:name => "Fubar") }.to raise_error(NoMethodError)
169
+ end
170
+ end
171
+
172
+ # Basic testing for an old fashioned casted hash
173
+ describe "old hash casted as attribute" do
174
+ before :each do
175
+ @obj = DummyModel.new(:old_casted_attribute => {:name => 'Testing'})
176
+ @casted_obj = @obj.old_casted_attribute
177
+ end
178
+ it "should be available from its parent" do
179
+ @casted_obj.should be_an_instance_of(OldFashionedMixin)
180
+ end
181
+
182
+ it "should have the getters defined" do
183
+ @casted_obj.name.should == 'Testing'
184
+ end
185
+
186
+ it "should know who casted it" do
187
+ @casted_obj.casted_by.should == @obj
188
+ end
189
+
190
+ it "should know which property casted it" do
191
+ @casted_obj.casted_by_property.should == @obj.properties.detect{|p| p.to_s == 'old_casted_attribute'}
192
+ end
193
+
194
+ it "should return nil for the unknown attribute" do
195
+ @casted_obj["unknown"].should be_nil
196
+ end
197
+ end
198
+
199
+ describe "casted as an array of a different type" do
200
+ before(:each) do
201
+ @obj = DummyModel.new(:keywords => ['couch', 'sofa', 'relax', 'canapé'])
202
+ end
203
+
204
+ it "should cast the array properly" do
205
+ @obj.keywords.should be_kind_of(Array)
206
+ @obj.keywords.first.should == 'couch'
207
+ end
208
+ end
209
+
210
+ describe "update attributes without saving" do
211
+ before(:each) do
212
+ @question = Question.new(:q => "What is your quest?", :a => "To seek the Holy Grail")
213
+ end
214
+ it "should work for attribute= methods" do
215
+ @question.q.should == "What is your quest?"
216
+ @question['a'].should == "To seek the Holy Grail"
217
+ @question.update_attributes_without_saving(:q => "What is your favorite color?", 'a' => "Blue")
218
+ @question['q'].should == "What is your favorite color?"
219
+ @question.a.should == "Blue"
220
+ end
221
+
222
+ it "should also work for attributes= alias" do
223
+ @question.respond_to?(:attributes=).should be_true
224
+ @question.attributes = {:q => "What is your favorite color?", 'a' => "Blue"}
225
+ @question['q'].should == "What is your favorite color?"
226
+ @question.a.should == "Blue"
227
+ end
228
+
229
+ it "should flip out if an attribute= method is missing" do
230
+ lambda {
231
+ @q.update_attributes_without_saving('foo' => "something", :a => "No green")
232
+ }.should raise_error(NoMethodError)
233
+ end
234
+
235
+ it "should not change any attributes if there is an error" do
236
+ lambda {
237
+ @q.update_attributes_without_saving('foo' => "something", :a => "No green")
238
+ }.should raise_error(NoMethodError)
239
+ @question.q.should == "What is your quest?"
240
+ @question.a.should == "To seek the Holy Grail"
241
+ end
242
+
243
+ end
244
+
245
+ describe "saved document with casted models" do
246
+ before(:each) do
247
+ reset_test_db!
248
+ @obj = DummyModel.new(:casted_attribute => {:name => 'whatever'})
249
+ @obj.save.should be_true
250
+ @obj = DummyModel.get(@obj.id)
251
+ end
252
+
253
+ it "should be able to load with the casted models" do
254
+ casted_obj = @obj.casted_attribute
255
+ casted_obj.should_not be_nil
256
+ casted_obj.should be_an_instance_of(WithCastedModelMixin)
257
+ end
258
+
259
+ it "should have defined getters for the casted model" do
260
+ casted_obj = @obj.casted_attribute
261
+ casted_obj.name.should == "whatever"
262
+ end
263
+
264
+ it "should have defined setters for the casted model" do
265
+ casted_obj = @obj.casted_attribute
266
+ casted_obj.name = "test"
267
+ casted_obj.name.should == "test"
268
+ end
269
+
270
+ it "should retain an override of a casted model attribute's default" do
271
+ casted_obj = @obj.casted_attribute
272
+ casted_obj.details['color'] = 'orange'
273
+ @obj.save
274
+ casted_obj = DummyModel.get(@obj.id).casted_attribute
275
+ casted_obj.details['color'].should == 'orange'
276
+ end
277
+
278
+ end
279
+
280
+ describe "saving document with array of casted models and validation" do
281
+ before :each do
282
+ @cat = Cat.new :name => "felix"
283
+ @cat.save
284
+ end
285
+
286
+ it "should save" do
287
+ toy = CatToy.new :name => "Mouse"
288
+ @cat.toys.push(toy)
289
+ @cat.save.should be_true
290
+ @cat = Cat.get @cat.id
291
+ @cat.toys.class.should == CouchRest::Model::CastedArray
292
+ @cat.toys.first.class.should == CatToy
293
+ @cat.toys.first.should === toy
294
+ end
295
+
296
+ it "should fail because name is not present" do
297
+ toy = CatToy.new
298
+ @cat.toys.push(toy)
299
+ @cat.should_not be_valid
300
+ @cat.save.should be_false
301
+ end
302
+
303
+ it "should not fail if the casted model doesn't have validation" do
304
+ Cat.property :masters, [Person], :default => []
305
+ Cat.validates_presence_of :name
306
+ cat = Cat.new(:name => 'kitty')
307
+ cat.should be_valid
308
+ cat.masters.push Person.new
309
+ cat.should be_valid
310
+ end
311
+ end
312
+
313
+ describe "calling valid?" do
314
+ before :each do
315
+ @cat = Cat.new
316
+ @toy1 = CatToy.new
317
+ @toy2 = CatToy.new
318
+ @toy3 = CatToy.new
319
+ @cat.favorite_toy = @toy1
320
+ @cat.toys << @toy2
321
+ @cat.toys << @toy3
322
+ end
323
+
324
+ describe "on the top document" do
325
+ it "should put errors on all invalid casted models" do
326
+ @cat.should_not be_valid
327
+ @cat.errors.should_not be_empty
328
+ @toy1.errors.should_not be_empty
329
+ @toy2.errors.should_not be_empty
330
+ @toy3.errors.should_not be_empty
331
+ end
332
+
333
+ it "should not put errors on valid casted models" do
334
+ @toy1.name = "Feather"
335
+ @toy2.name = "Twine"
336
+ @cat.should_not be_valid
337
+ @cat.errors.should_not be_empty
338
+ @toy1.errors.should be_empty
339
+ @toy2.errors.should be_empty
340
+ @toy3.errors.should_not be_empty
341
+ end
342
+
343
+ it "should not use dperecated ActiveModel options" do
344
+ ActiveSupport::Deprecation.should_not_receive(:warn)
345
+ @cat.should_not be_valid
346
+ end
347
+ end
348
+
349
+ describe "on a casted model property" do
350
+ it "should only validate itself" do
351
+ @toy1.should_not be_valid
352
+ @toy1.errors.should_not be_empty
353
+ @cat.errors.should be_empty
354
+ @toy2.errors.should be_empty
355
+ @toy3.errors.should be_empty
356
+ end
357
+ end
358
+
359
+ describe "on a casted model inside a casted collection" do
360
+ it "should only validate itself" do
361
+ @toy2.should_not be_valid
362
+ @toy2.errors.should_not be_empty
363
+ @cat.errors.should be_empty
364
+ @toy1.errors.should be_empty
365
+ @toy3.errors.should be_empty
366
+ end
367
+ end
368
+ end
369
+
370
+ describe "calling new? on a casted model" do
371
+ before :each do
372
+ reset_test_db!
373
+ @cat = Cat.new(:name => 'Sockington')
374
+ @favorite_toy = CatToy.new(:name => 'Catnip Ball')
375
+ @cat.favorite_toy = @favorite_toy
376
+ @cat.toys << CatToy.new(:name => 'Fuzzy Stick')
377
+ end
378
+
379
+ it "should be true on new" do
380
+ CatToy.new.should be_new
381
+ CatToy.new.new_record?.should be_true
382
+ end
383
+
384
+ it "should be true after assignment" do
385
+ @cat.should be_new
386
+ @cat.favorite_toy.should be_new
387
+ @cat.toys.first.should be_new
388
+ end
389
+
390
+ it "should not be true after create or save" do
391
+ @cat.create
392
+ @cat.save
393
+ @cat.favorite_toy.should_not be_new
394
+ @cat.toys.first.casted_by.should eql(@cat)
395
+ @cat.toys.first.should_not be_new
396
+ end
397
+
398
+ it "should not be true after get from the database" do
399
+ @cat.save
400
+ @cat = Cat.get(@cat.id)
401
+ @cat.favorite_toy.should_not be_new
402
+ @cat.toys.first.should_not be_new
403
+ end
404
+
405
+ it "should still be true after a failed create or save" do
406
+ @cat.name = nil
407
+ @cat.create.should be_false
408
+ @cat.save.should be_false
409
+ @cat.favorite_toy.should be_new
410
+ @cat.toys.first.should be_new
411
+ end
412
+ end
413
+
414
+ describe "calling base_doc from a nested casted model" do
415
+ before :each do
416
+ @course = Course.new(:title => 'Science 101')
417
+ @professor = Person.new(:name => ['Professor', 'Plum'])
418
+ @cat = Cat.new(:name => 'Scratchy')
419
+ @toy1 = CatToy.new
420
+ @toy2 = CatToy.new
421
+ @course.professor = @professor
422
+ @professor.pet = @cat
423
+ @cat.favorite_toy = @toy1
424
+ @cat.toys << @toy2
425
+ end
426
+
427
+ it 'should let you copy over casted arrays' do
428
+ question = Question.new
429
+ @course.questions << question
430
+ new_course = Course.new
431
+ new_course.questions = @course.questions
432
+ new_course.questions.should include(question)
433
+ end
434
+
435
+ it "should reference the top document for" do
436
+ @course.base_doc.should === @course
437
+ @professor.casted_by.should === @course
438
+ @professor.base_doc.should === @course
439
+ @cat.base_doc.should === @course
440
+ @toy1.base_doc.should === @course
441
+ @toy2.base_doc.should === @course
442
+ end
443
+
444
+ it "should call setter on top document" do
445
+ @toy1.base_doc.should_not be_nil
446
+ @toy1.base_doc.title = 'Tom Foolery'
447
+ @course.title.should == 'Tom Foolery'
448
+ end
449
+
450
+ it "should return nil if not yet casted" do
451
+ person = Person.new
452
+ person.base_doc.should == nil
453
+ end
454
+ end
455
+
456
+ describe "calling base_doc.save from a nested casted model" do
457
+ before :each do
458
+ reset_test_db!
459
+ @cat = Cat.new(:name => 'Snowball')
460
+ @toy = CatToy.new
461
+ @cat.favorite_toy = @toy
462
+ end
463
+
464
+ it "should not save parent document when casted model is invalid" do
465
+ @toy.should_not be_valid
466
+ @toy.base_doc.save.should be_false
467
+ lambda{@toy.base_doc.save!}.should raise_error
468
+ end
469
+
470
+ it "should save parent document when nested casted model is valid" do
471
+ @toy.name = "Mr Squeaks"
472
+ @toy.should be_valid
473
+ @toy.base_doc.save.should be_true
474
+ lambda{@toy.base_doc.save!}.should_not raise_error
475
+ end
476
+ end
477
+
478
+ describe "callbacks" do
479
+ before(:each) do
480
+ @doc = CastedCallbackDoc.new
481
+ @model = WithCastedCallBackModel.new
482
+ @doc.callback_model = @model
483
+ end
484
+
485
+ describe "validate" do
486
+ it "should run before_validation before validating" do
487
+ @model.run_before_validation.should be_nil
488
+ @model.should be_valid
489
+ @model.run_before_validation.should be_true
490
+ end
491
+ it "should run after_validation after validating" do
492
+ @model.run_after_validation.should be_nil
493
+ @model.should be_valid
494
+ @model.run_after_validation.should be_true
495
+ end
496
+ end
497
+ end
498
+ end