couchrest_model 1.0.0.beta7

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 (69) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +320 -0
  3. data/Rakefile +71 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +180 -0
  7. data/lib/couchrest/model.rb +10 -0
  8. data/lib/couchrest/model/associations.rb +207 -0
  9. data/lib/couchrest/model/attribute_protection.rb +74 -0
  10. data/lib/couchrest/model/attributes.rb +75 -0
  11. data/lib/couchrest/model/base.rb +111 -0
  12. data/lib/couchrest/model/callbacks.rb +27 -0
  13. data/lib/couchrest/model/casted_array.rb +39 -0
  14. data/lib/couchrest/model/casted_model.rb +68 -0
  15. data/lib/couchrest/model/class_proxy.rb +122 -0
  16. data/lib/couchrest/model/collection.rb +260 -0
  17. data/lib/couchrest/model/design_doc.rb +126 -0
  18. data/lib/couchrest/model/document_queries.rb +82 -0
  19. data/lib/couchrest/model/errors.rb +23 -0
  20. data/lib/couchrest/model/extended_attachments.rb +73 -0
  21. data/lib/couchrest/model/persistence.rb +141 -0
  22. data/lib/couchrest/model/properties.rb +144 -0
  23. data/lib/couchrest/model/property.rb +96 -0
  24. data/lib/couchrest/model/support/couchrest.rb +19 -0
  25. data/lib/couchrest/model/support/hash.rb +9 -0
  26. data/lib/couchrest/model/typecast.rb +170 -0
  27. data/lib/couchrest/model/validations.rb +68 -0
  28. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  29. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  30. data/lib/couchrest/model/validations/uniqueness.rb +45 -0
  31. data/lib/couchrest/model/views.rb +167 -0
  32. data/lib/couchrest_model.rb +56 -0
  33. data/spec/couchrest/assocations_spec.rb +213 -0
  34. data/spec/couchrest/attachment_spec.rb +148 -0
  35. data/spec/couchrest/attribute_protection_spec.rb +153 -0
  36. data/spec/couchrest/base_spec.rb +463 -0
  37. data/spec/couchrest/casted_model_spec.rb +424 -0
  38. data/spec/couchrest/casted_spec.rb +75 -0
  39. data/spec/couchrest/class_proxy_spec.rb +132 -0
  40. data/spec/couchrest/inherited_spec.rb +40 -0
  41. data/spec/couchrest/persistence_spec.rb +409 -0
  42. data/spec/couchrest/property_spec.rb +804 -0
  43. data/spec/couchrest/subclass_spec.rb +99 -0
  44. data/spec/couchrest/validations.rb +73 -0
  45. data/spec/couchrest/view_spec.rb +463 -0
  46. data/spec/fixtures/attachments/README +3 -0
  47. data/spec/fixtures/attachments/couchdb.png +0 -0
  48. data/spec/fixtures/attachments/test.html +11 -0
  49. data/spec/fixtures/base.rb +139 -0
  50. data/spec/fixtures/more/article.rb +35 -0
  51. data/spec/fixtures/more/card.rb +17 -0
  52. data/spec/fixtures/more/cat.rb +19 -0
  53. data/spec/fixtures/more/course.rb +25 -0
  54. data/spec/fixtures/more/event.rb +8 -0
  55. data/spec/fixtures/more/invoice.rb +14 -0
  56. data/spec/fixtures/more/person.rb +9 -0
  57. data/spec/fixtures/more/question.rb +7 -0
  58. data/spec/fixtures/more/service.rb +10 -0
  59. data/spec/fixtures/more/user.rb +22 -0
  60. data/spec/fixtures/views/lib.js +3 -0
  61. data/spec/fixtures/views/test_view/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/only-map.js +4 -0
  63. data/spec/fixtures/views/test_view/test-map.js +3 -0
  64. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  65. data/spec/spec.opts +5 -0
  66. data/spec/spec_helper.rb +48 -0
  67. data/utils/remap.rb +27 -0
  68. data/utils/subset.rb +30 -0
  69. metadata +232 -0
@@ -0,0 +1,463 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path("../../spec_helper", __FILE__)
4
+ require File.join(FIXTURE_PATH, 'more', 'cat')
5
+ require File.join(FIXTURE_PATH, 'more', 'article')
6
+ require File.join(FIXTURE_PATH, 'more', 'course')
7
+ require File.join(FIXTURE_PATH, 'more', 'card')
8
+ require File.join(FIXTURE_PATH, 'base')
9
+
10
+ describe "Model Base" do
11
+
12
+ before(:each) do
13
+ @obj = WithDefaultValues.new
14
+ end
15
+
16
+ describe "instance database connection" do
17
+ it "should use the default database" do
18
+ @obj.database.name.should == 'couchrest-model-test'
19
+ end
20
+
21
+ it "should override the default db" do
22
+ @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test')
23
+ @obj.database.name.should == 'couchrest-extendedmodel-test'
24
+ @obj.database.delete!
25
+ end
26
+ end
27
+
28
+ describe "a new model" do
29
+ it "should be a new document" do
30
+ @obj = Basic.new
31
+ @obj.rev.should be_nil
32
+ @obj.should be_new
33
+ @obj.should be_new_document
34
+ @obj.should be_new_record
35
+ end
36
+
37
+ it "should not failed on a nil value in argument" do
38
+ @obj = Basic.new(nil)
39
+ @obj.should == { 'couchrest-type' => 'Basic' }
40
+ end
41
+ end
42
+
43
+ describe "ActiveModel compatability Basic" do
44
+
45
+ before(:each) do
46
+ @obj = Basic.new(nil)
47
+ end
48
+
49
+ describe "#to_key" do
50
+ context "when the document is new" do
51
+ it "returns nil" do
52
+ @obj.to_key.should be_nil
53
+ end
54
+ end
55
+
56
+ context "when the document is not new" do
57
+ it "returns id in an array" do
58
+ @obj.save
59
+ @obj.to_key.should eql([@obj['_id']])
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "#to_param" do
65
+ context "when the document is new" do
66
+ it "returns nil" do
67
+ @obj.to_param.should be_nil
68
+ end
69
+ end
70
+
71
+ context "when the document is not new" do
72
+ it "returns id" do
73
+ @obj.save
74
+ @obj.to_param.should eql(@obj['_id'])
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "#persisted?" do
80
+ context "when the document is new" do
81
+ it "returns false" do
82
+ @obj.persisted?.should == false
83
+ end
84
+ end
85
+
86
+ context "when the document is not new" do
87
+ it "returns id" do
88
+ @obj.save
89
+ @obj.persisted?.should == true
90
+ end
91
+ end
92
+ end
93
+
94
+ describe "#model_name" do
95
+ it "returns the name of the model" do
96
+ @obj.class.model_name.should eql('Basic')
97
+ WithDefaultValues.model_name.human.should eql("With default values")
98
+ end
99
+ end
100
+
101
+
102
+ end
103
+
104
+ describe "update attributes without saving" do
105
+ before(:each) do
106
+ a = Article.get "big-bad-danger" rescue nil
107
+ a.destroy if a
108
+ @art = Article.new(:title => "big bad danger")
109
+ @art.save
110
+ end
111
+ it "should work for attribute= methods" do
112
+ @art['title'].should == "big bad danger"
113
+ @art.update_attributes_without_saving('date' => Time.now, :title => "super danger")
114
+ @art['title'].should == "super danger"
115
+ end
116
+ it "should silently ignore _id" do
117
+ @art.update_attributes_without_saving('_id' => 'foobar')
118
+ @art['_id'].should_not == 'foobar'
119
+ end
120
+ it "should silently ignore _rev" do
121
+ @art.update_attributes_without_saving('_rev' => 'foobar')
122
+ @art['_rev'].should_not == 'foobar'
123
+ end
124
+ it "should silently ignore created_at" do
125
+ @art.update_attributes_without_saving('created_at' => 'foobar')
126
+ @art['created_at'].should_not == 'foobar'
127
+ end
128
+ it "should silently ignore updated_at" do
129
+ @art.update_attributes_without_saving('updated_at' => 'foobar')
130
+ @art['updated_at'].should_not == 'foobar'
131
+ end
132
+ it "should also work using attributes= alias" do
133
+ @art.respond_to?(:attributes=).should be_true
134
+ @art.attributes = {'date' => Time.now, :title => "something else"}
135
+ @art['title'].should == "something else"
136
+ end
137
+
138
+ it "should not flip out if an attribute= method is missing and ignore it" do
139
+ lambda {
140
+ @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
141
+ }.should_not raise_error
142
+ @art.slug.should == "big-bad-danger"
143
+ end
144
+
145
+ #it "should not change other attributes if there is an error" do
146
+ # lambda {
147
+ # @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
148
+ # }.should raise_error
149
+ # @art['title'].should == "big bad danger"
150
+ #end
151
+ end
152
+
153
+ describe "update attributes" do
154
+ before(:each) do
155
+ a = Article.get "big-bad-danger" rescue nil
156
+ a.destroy if a
157
+ @art = Article.new(:title => "big bad danger")
158
+ @art.save
159
+ end
160
+ it "should save" do
161
+ @art['title'].should == "big bad danger"
162
+ @art.update_attributes('date' => Time.now, :title => "super danger")
163
+ loaded = Article.get(@art.id)
164
+ loaded['title'].should == "super danger"
165
+ end
166
+ end
167
+
168
+ describe "with default" do
169
+ it "should have the default value set at initalization" do
170
+ @obj.preset.should == {:right => 10, :top_align => false}
171
+ end
172
+
173
+ it "should have the default false value explicitly assigned" do
174
+ @obj.default_false.should == false
175
+ end
176
+
177
+ it "should automatically call a proc default at initialization" do
178
+ @obj.set_by_proc.should be_an_instance_of(Time)
179
+ @obj.set_by_proc.should == @obj.set_by_proc
180
+ @obj.set_by_proc.should < Time.now
181
+ end
182
+
183
+ it "should let you overwrite the default values" do
184
+ obj = WithDefaultValues.new(:preset => 'test')
185
+ obj.preset = 'test'
186
+ end
187
+
188
+ it "should work with a default empty array" do
189
+ obj = WithDefaultValues.new(:tags => ['spec'])
190
+ obj.tags.should == ['spec']
191
+ end
192
+
193
+ it "should set default value of read-only property" do
194
+ obj = WithDefaultValues.new
195
+ obj.read_only_with_default.should == 'generic'
196
+ end
197
+ end
198
+
199
+ describe "simplified way of setting property types" do
200
+ it "should set defaults" do
201
+ obj = WithSimplePropertyType.new
202
+ obj.preset.should eql('none')
203
+ end
204
+
205
+ it "should handle arrays" do
206
+ obj = WithSimplePropertyType.new(:tags => ['spec'])
207
+ obj.tags.should == ['spec']
208
+ end
209
+ end
210
+
211
+ describe "a doc with template values (CR::Model spec)" do
212
+ before(:all) do
213
+ WithTemplateAndUniqueID.all.map{|o| o.destroy(true)}
214
+ WithTemplateAndUniqueID.database.bulk_delete
215
+ @tmpl = WithTemplateAndUniqueID.new
216
+ @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1')
217
+ end
218
+ it "should have fields set when new" do
219
+ @tmpl.preset.should == 'value'
220
+ end
221
+ it "shouldn't override explicitly set values" do
222
+ @tmpl2.preset.should == 'not_value'
223
+ end
224
+ it "shouldn't override existing documents" do
225
+ @tmpl2.save
226
+ tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id)
227
+ @tmpl2.preset.should == 'not_value'
228
+ tmpl2_reloaded.preset.should == 'not_value'
229
+ end
230
+ end
231
+
232
+
233
+ describe "finding all instances of a model" do
234
+ before(:all) do
235
+ WithTemplateAndUniqueID.req_design_doc_refresh
236
+ WithTemplateAndUniqueID.all.map{|o| o.destroy(true)}
237
+ WithTemplateAndUniqueID.database.bulk_delete
238
+ WithTemplateAndUniqueID.new('important-field' => '1').save
239
+ WithTemplateAndUniqueID.new('important-field' => '2').save
240
+ WithTemplateAndUniqueID.new('important-field' => '3').save
241
+ WithTemplateAndUniqueID.new('important-field' => '4').save
242
+ end
243
+ it "should make the design doc" do
244
+ WithTemplateAndUniqueID.all
245
+ d = WithTemplateAndUniqueID.design_doc
246
+ d['views']['all']['map'].should include('WithTemplateAndUniqueID')
247
+ end
248
+ it "should find all" do
249
+ rs = WithTemplateAndUniqueID.all
250
+ rs.length.should == 4
251
+ end
252
+ end
253
+
254
+ describe "counting all instances of a model" do
255
+ before(:each) do
256
+ @db = reset_test_db!
257
+ WithTemplateAndUniqueID.req_design_doc_refresh
258
+ end
259
+
260
+ it ".count should return 0 if there are no docuemtns" do
261
+ WithTemplateAndUniqueID.count.should == 0
262
+ end
263
+
264
+ it ".count should return the number of documents" do
265
+ WithTemplateAndUniqueID.new('important-field' => '1').save
266
+ WithTemplateAndUniqueID.new('important-field' => '2').save
267
+ WithTemplateAndUniqueID.new('important-field' => '3').save
268
+
269
+ WithTemplateAndUniqueID.count.should == 3
270
+ end
271
+ end
272
+
273
+ describe "finding the first instance of a model" do
274
+ before(:each) do
275
+ @db = reset_test_db!
276
+ # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
277
+ WithTemplateAndUniqueID.new('important-field' => '1').save
278
+ WithTemplateAndUniqueID.new('important-field' => '2').save
279
+ WithTemplateAndUniqueID.new('important-field' => '3').save
280
+ WithTemplateAndUniqueID.new('important-field' => '4').save
281
+ end
282
+ it "should make the design doc" do
283
+ WithTemplateAndUniqueID.all
284
+ d = WithTemplateAndUniqueID.design_doc
285
+ d['views']['all']['map'].should include('WithTemplateAndUniqueID')
286
+ end
287
+ it "should find first" do
288
+ rs = WithTemplateAndUniqueID.first
289
+ rs['important-field'].should == "1"
290
+ end
291
+ it "should return nil if no instances are found" do
292
+ WithTemplateAndUniqueID.all.each {|obj| obj.destroy }
293
+ WithTemplateAndUniqueID.first.should be_nil
294
+ end
295
+ end
296
+
297
+ describe "lazily refreshing the design document" do
298
+ before(:all) do
299
+ @db = reset_test_db!
300
+ WithTemplateAndUniqueID.new('important-field' => '1').save
301
+ end
302
+ it "should not save the design doc twice" do
303
+ WithTemplateAndUniqueID.all
304
+ WithTemplateAndUniqueID.req_design_doc_refresh
305
+ WithTemplateAndUniqueID.refresh_design_doc
306
+ rev = WithTemplateAndUniqueID.design_doc['_rev']
307
+ WithTemplateAndUniqueID.req_design_doc_refresh
308
+ WithTemplateAndUniqueID.refresh_design_doc
309
+ WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
310
+ end
311
+ end
312
+
313
+ describe "getting a model with a subobject field" do
314
+ before(:all) do
315
+ course_doc = {
316
+ "title" => "Metaphysics 410",
317
+ "professor" => {
318
+ "name" => ["Mark", "Hinchliff"]
319
+ },
320
+ "ends_at" => "2008/12/19 13:00:00 +0800"
321
+ }
322
+ r = Course.database.save_doc course_doc
323
+ @course = Course.get r['id']
324
+ end
325
+ it "should load the course" do
326
+ @course["professor"]["name"][1].should == "Hinchliff"
327
+ end
328
+ it "should instantiate the professor as a person" do
329
+ @course['professor'].last_name.should == "Hinchliff"
330
+ end
331
+ it "should instantiate the ends_at as a Time" do
332
+ @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800")
333
+ end
334
+ end
335
+
336
+ describe "timestamping" do
337
+ before(:each) do
338
+ oldart = Article.get "saving-this" rescue nil
339
+ oldart.destroy if oldart
340
+ @art = Article.new(:title => "Saving this")
341
+ @art.save
342
+ end
343
+
344
+ it "should define the updated_at and created_at getters and set the values" do
345
+ @obj.save
346
+ obj = WithDefaultValues.get(@obj.id)
347
+ obj.should be_an_instance_of(WithDefaultValues)
348
+ obj.created_at.should be_an_instance_of(Time)
349
+ obj.updated_at.should be_an_instance_of(Time)
350
+ obj.created_at.to_s.should == @obj.updated_at.to_s
351
+ end
352
+
353
+ it "should not change created_at on update" do
354
+ 2.times do
355
+ lambda do
356
+ @art.save
357
+ end.should_not change(@art, :created_at)
358
+ end
359
+ end
360
+
361
+ it "should set the time on create" do
362
+ (Time.now - @art.created_at).should < 2
363
+ foundart = Article.get @art.id
364
+ foundart.created_at.should == foundart.updated_at
365
+ end
366
+ it "should set the time on update" do
367
+ @art.save
368
+ @art.created_at.should < @art.updated_at
369
+ end
370
+ end
371
+
372
+ describe "getter and setter methods" do
373
+ it "should try to call the arg= method before setting :arg in the hash" do
374
+ @doc = WithGetterAndSetterMethods.new(:arg => "foo")
375
+ @doc['arg'].should be_nil
376
+ @doc[:arg].should be_nil
377
+ @doc.other_arg.should == "foo-foo"
378
+ end
379
+ end
380
+
381
+ describe "initialization" do
382
+ it "should call after_initialize method if available" do
383
+ @doc = WithAfterInitializeMethod.new
384
+ @doc['some_value'].should eql('value')
385
+ end
386
+ end
387
+
388
+ describe "recursive validation on a model" do
389
+ before :each do
390
+ reset_test_db!
391
+ @cat = Cat.new(:name => 'Sockington')
392
+ end
393
+
394
+ it "should not save if a nested casted model is invalid" do
395
+ @cat.favorite_toy = CatToy.new
396
+ @cat.should_not be_valid
397
+ @cat.save.should be_false
398
+ lambda{@cat.save!}.should raise_error
399
+ end
400
+
401
+ it "should save when nested casted model is valid" do
402
+ @cat.favorite_toy = CatToy.new(:name => 'Squeaky')
403
+ @cat.should be_valid
404
+ @cat.save.should be_true
405
+ lambda{@cat.save!}.should_not raise_error
406
+ end
407
+
408
+ it "should not save when nested collection contains an invalid casted model" do
409
+ @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
410
+ @cat.should_not be_valid
411
+ @cat.save.should be_false
412
+ lambda{@cat.save!}.should raise_error
413
+ end
414
+
415
+ it "should save when nested collection contains valid casted models" do
416
+ @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
417
+ @cat.should be_valid
418
+ @cat.save.should be_true
419
+ lambda{@cat.save!}.should_not raise_error
420
+ end
421
+
422
+ it "should not fail if the nested casted model doesn't have validation" do
423
+ Cat.property :trainer, Person
424
+ Cat.validates_presence_of :name
425
+ cat = Cat.new(:name => 'Mr Bigglesworth')
426
+ cat.trainer = Person.new
427
+ cat.should be_valid
428
+ cat.save.should be_true
429
+ end
430
+ end
431
+
432
+ describe "searching the contents of a model" do
433
+ before :each do
434
+ @db = reset_test_db!
435
+
436
+ names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"]
437
+ names.each { |name| Cat.create(:name => name) }
438
+
439
+ search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'},
440
+ 'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" }
441
+ @db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}})
442
+ end
443
+
444
+ it "should be able to paginate through a large set of search results" do
445
+ if couchdb_lucene_available?
446
+ names = []
447
+ Cat.paginated_each(:design_doc => "search", :view_name => "cats",
448
+ :q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat|
449
+ cat.should_not be_nil
450
+ names << cat.name
451
+ end
452
+
453
+ names.size.should == 5
454
+ names.should include('Sockington')
455
+ names.should include('Smitty')
456
+ names.should include('Sammy')
457
+ names.should include('Samson')
458
+ names.should include('Simon')
459
+ end
460
+ end
461
+ end
462
+
463
+ end