samlown-couchrest_extended_document 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 (70) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +46 -0
  3. data/Rakefile +67 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +130 -0
  7. data/lib/couchrest/casted_array.rb +25 -0
  8. data/lib/couchrest/casted_model.rb +59 -0
  9. data/lib/couchrest/extended_document.rb +318 -0
  10. data/lib/couchrest/mixins.rb +12 -0
  11. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  12. data/lib/couchrest/mixins/callbacks.rb +532 -0
  13. data/lib/couchrest/mixins/class_proxy.rb +120 -0
  14. data/lib/couchrest/mixins/collection.rb +260 -0
  15. data/lib/couchrest/mixins/design_doc.rb +121 -0
  16. data/lib/couchrest/mixins/document_queries.rb +80 -0
  17. data/lib/couchrest/mixins/extended_attachments.rb +70 -0
  18. data/lib/couchrest/mixins/properties.rb +154 -0
  19. data/lib/couchrest/mixins/validation.rb +245 -0
  20. data/lib/couchrest/mixins/views.rb +148 -0
  21. data/lib/couchrest/monkeypatches.rb +5 -0
  22. data/lib/couchrest/property.rb +50 -0
  23. data/lib/couchrest/support/couchrest.rb +56 -0
  24. data/lib/couchrest/support/rails.rb +42 -0
  25. data/lib/couchrest/typecast.rb +175 -0
  26. data/lib/couchrest/validation/auto_validate.rb +156 -0
  27. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  28. data/lib/couchrest/validation/validation_errors.rb +125 -0
  29. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  30. data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
  31. data/lib/couchrest/validation/validators/format_validator.rb +122 -0
  32. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  33. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  34. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  35. data/lib/couchrest/validation/validators/length_validator.rb +139 -0
  36. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  37. data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
  38. data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
  39. data/spec/couchrest/attribute_protection_spec.rb +150 -0
  40. data/spec/couchrest/casted_extended_doc_spec.rb +79 -0
  41. data/spec/couchrest/casted_model_spec.rb +406 -0
  42. data/spec/couchrest/extended_doc_attachment_spec.rb +135 -0
  43. data/spec/couchrest/extended_doc_inherited_spec.rb +40 -0
  44. data/spec/couchrest/extended_doc_spec.rb +824 -0
  45. data/spec/couchrest/extended_doc_subclass_spec.rb +99 -0
  46. data/spec/couchrest/extended_doc_view_spec.rb +473 -0
  47. data/spec/couchrest/property_spec.rb +628 -0
  48. data/spec/fixtures/attachments/README +3 -0
  49. data/spec/fixtures/attachments/couchdb.png +0 -0
  50. data/spec/fixtures/attachments/test.html +11 -0
  51. data/spec/fixtures/more/article.rb +35 -0
  52. data/spec/fixtures/more/card.rb +22 -0
  53. data/spec/fixtures/more/cat.rb +22 -0
  54. data/spec/fixtures/more/course.rb +22 -0
  55. data/spec/fixtures/more/event.rb +8 -0
  56. data/spec/fixtures/more/invoice.rb +17 -0
  57. data/spec/fixtures/more/person.rb +9 -0
  58. data/spec/fixtures/more/question.rb +6 -0
  59. data/spec/fixtures/more/service.rb +12 -0
  60. data/spec/fixtures/more/user.rb +22 -0
  61. data/spec/fixtures/views/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/lib.js +3 -0
  63. data/spec/fixtures/views/test_view/only-map.js +4 -0
  64. data/spec/fixtures/views/test_view/test-map.js +3 -0
  65. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  66. data/spec/spec.opts +5 -0
  67. data/spec/spec_helper.rb +49 -0
  68. data/utils/remap.rb +27 -0
  69. data/utils/subset.rb +30 -0
  70. metadata +181 -0
@@ -0,0 +1,135 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ describe "ExtendedDocument attachments" do
4
+
5
+ describe "#has_attachment?" do
6
+ before(:each) do
7
+ reset_test_db!
8
+ @obj = Basic.new
9
+ @obj.save.should == true
10
+ @file = File.open(FIXTURE_PATH + '/attachments/test.html')
11
+ @attachment_name = 'my_attachment'
12
+ @obj.create_attachment(:file => @file, :name => @attachment_name)
13
+ end
14
+
15
+ it 'should return false if there is no attachment' do
16
+ @obj.has_attachment?('bogus').should be_false
17
+ end
18
+
19
+ it 'should return true if there is an attachment' do
20
+ @obj.has_attachment?(@attachment_name).should be_true
21
+ end
22
+
23
+ it 'should return true if an object with an attachment is reloaded' do
24
+ @obj.save.should be_true
25
+ reloaded_obj = Basic.get(@obj.id)
26
+ reloaded_obj.has_attachment?(@attachment_name).should be_true
27
+ end
28
+
29
+ it 'should return false if an attachment has been removed' do
30
+ @obj.delete_attachment(@attachment_name)
31
+ @obj.has_attachment?(@attachment_name).should be_false
32
+ end
33
+ end
34
+
35
+ describe "creating an attachment" do
36
+ before(:each) do
37
+ @obj = Basic.new
38
+ @obj.save.should == true
39
+ @file_ext = File.open(FIXTURE_PATH + '/attachments/test.html')
40
+ @file_no_ext = File.open(FIXTURE_PATH + '/attachments/README')
41
+ @attachment_name = 'my_attachment'
42
+ @content_type = 'media/mp3'
43
+ end
44
+
45
+ it "should create an attachment from file with an extension" do
46
+ @obj.create_attachment(:file => @file_ext, :name => @attachment_name)
47
+ @obj.save.should == true
48
+ reloaded_obj = Basic.get(@obj.id)
49
+ reloaded_obj['_attachments'][@attachment_name].should_not be_nil
50
+ end
51
+
52
+ it "should create an attachment from file without an extension" do
53
+ @obj.create_attachment(:file => @file_no_ext, :name => @attachment_name)
54
+ @obj.save.should == true
55
+ reloaded_obj = Basic.get(@obj.id)
56
+ reloaded_obj['_attachments'][@attachment_name].should_not be_nil
57
+ end
58
+
59
+ it 'should raise ArgumentError if :file is missing' do
60
+ lambda{ @obj.create_attachment(:name => @attachment_name) }.should raise_error
61
+ end
62
+
63
+ it 'should raise ArgumentError if :name is missing' do
64
+ lambda{ @obj.create_attachment(:file => @file_ext) }.should raise_error
65
+ end
66
+
67
+ it 'should set the content-type if passed' do
68
+ @obj.create_attachment(:file => @file_ext, :name => @attachment_name, :content_type => @content_type)
69
+ @obj['_attachments'][@attachment_name]['content_type'].should == @content_type
70
+ end
71
+ end
72
+
73
+ describe 'reading, updating, and deleting an attachment' do
74
+ before(:each) do
75
+ @obj = Basic.new
76
+ @file = File.open(FIXTURE_PATH + '/attachments/test.html')
77
+ @attachment_name = 'my_attachment'
78
+ @obj.create_attachment(:file => @file, :name => @attachment_name)
79
+ @obj.save.should == true
80
+ @file.rewind
81
+ @content_type = 'media/mp3'
82
+ end
83
+
84
+ it 'should read an attachment that exists' do
85
+ @obj.read_attachment(@attachment_name).should == @file.read
86
+ end
87
+
88
+ it 'should update an attachment that exists' do
89
+ file = File.open(FIXTURE_PATH + '/attachments/README')
90
+ @file.should_not == file
91
+ @obj.update_attachment(:file => file, :name => @attachment_name)
92
+ @obj.save
93
+ reloaded_obj = Basic.get(@obj.id)
94
+ file.rewind
95
+ reloaded_obj.read_attachment(@attachment_name).should_not == @file.read
96
+ reloaded_obj.read_attachment(@attachment_name).should == file.read
97
+ end
98
+
99
+ it 'should se the content-type if passed' do
100
+ file = File.open(FIXTURE_PATH + '/attachments/README')
101
+ @file.should_not == file
102
+ @obj.update_attachment(:file => file, :name => @attachment_name, :content_type => @content_type)
103
+ @obj['_attachments'][@attachment_name]['content_type'].should == @content_type
104
+ end
105
+
106
+ it 'should delete an attachment that exists' do
107
+ @obj.delete_attachment(@attachment_name)
108
+ @obj.save
109
+ lambda{Basic.get(@obj.id).read_attachment(@attachment_name)}.should raise_error
110
+ end
111
+ end
112
+
113
+ describe "#attachment_url" do
114
+ before(:each) do
115
+ @obj = Basic.new
116
+ @file = File.open(FIXTURE_PATH + '/attachments/test.html')
117
+ @attachment_name = 'my_attachment'
118
+ @obj.create_attachment(:file => @file, :name => @attachment_name)
119
+ @obj.save.should == true
120
+ end
121
+
122
+ it 'should return nil if attachment does not exist' do
123
+ @obj.attachment_url('bogus').should be_nil
124
+ end
125
+
126
+ it 'should return the attachment URL as specified by CouchDB HttpDocumentApi' do
127
+ @obj.attachment_url(@attachment_name).should == "#{Basic.database}/#{@obj.id}/#{@attachment_name}"
128
+ end
129
+
130
+ it 'should return the attachment URI' do
131
+ @obj.attachment_uri(@attachment_name).should == "#{Basic.database.uri}/#{@obj.id}/#{@attachment_name}"
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,40 @@
1
+ require File.expand_path('../../spec_helper', __FILE__)
2
+
3
+ begin
4
+ require 'rubygems' unless ENV['SKIP_RUBYGEMS']
5
+ require 'activesupport'
6
+ ActiveSupport::JSON.backend = :JSONGem
7
+
8
+ class PlainParent
9
+ class_inheritable_accessor :foo
10
+ self.foo = :bar
11
+ end
12
+
13
+ class PlainChild < PlainParent
14
+ end
15
+
16
+ class ExtendedParent < CouchRest::ExtendedDocument
17
+ class_inheritable_accessor :foo
18
+ self.foo = :bar
19
+ end
20
+
21
+ class ExtendedChild < ExtendedParent
22
+ end
23
+
24
+ describe "Using chained inheritance without CouchRest::ExtendedDocument" do
25
+ it "should preserve inheritable attributes" do
26
+ PlainParent.foo.should == :bar
27
+ PlainChild.foo.should == :bar
28
+ end
29
+ end
30
+
31
+ describe "Using chained inheritance with CouchRest::ExtendedDocument" do
32
+ it "should preserve inheritable attributes" do
33
+ ExtendedParent.foo.should == :bar
34
+ ExtendedChild.foo.should == :bar
35
+ end
36
+ end
37
+
38
+ rescue LoadError
39
+ puts "This spec requires 'active_support' to be loaded"
40
+ end
@@ -0,0 +1,824 @@
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path("../../spec_helper", __FILE__)
4
+ require File.join(FIXTURE_PATH, 'more', 'article')
5
+ require File.join(FIXTURE_PATH, 'more', 'course')
6
+ require File.join(FIXTURE_PATH, 'more', 'card')
7
+ require File.join(FIXTURE_PATH, 'more', 'cat')
8
+
9
+ describe "ExtendedDocument" do
10
+
11
+ class WithDefaultValues < CouchRest::ExtendedDocument
12
+ use_database TEST_SERVER.default_database
13
+ property :preset, :type => 'Object', :default => {:right => 10, :top_align => false}
14
+ property :set_by_proc, :default => Proc.new{Time.now}, :cast_as => 'Time'
15
+ property :tags, :type => ['String'], :default => []
16
+ property :read_only_with_default, :default => 'generic', :read_only => true
17
+ property :default_false, :type => 'Boolean', :default => false
18
+ property :name
19
+ timestamps!
20
+ end
21
+
22
+ class WithCallBacks < CouchRest::ExtendedDocument
23
+ include ::CouchRest::Validation
24
+ use_database TEST_SERVER.default_database
25
+ property :name
26
+ property :run_before_validate
27
+ property :run_after_validate
28
+ property :run_before_save
29
+ property :run_after_save
30
+ property :run_before_create
31
+ property :run_after_create
32
+ property :run_before_update
33
+ property :run_after_update
34
+
35
+ before_validate do |object|
36
+ object.run_before_validate = true
37
+ end
38
+ after_validate do |object|
39
+ object.run_after_validate = true
40
+ end
41
+ before_save do |object|
42
+ object.run_before_save = true
43
+ end
44
+ after_save do |object|
45
+ object.run_after_save = true
46
+ end
47
+ before_create do |object|
48
+ object.run_before_create = true
49
+ end
50
+ after_create do |object|
51
+ object.run_after_create = true
52
+ end
53
+ before_update do |object|
54
+ object.run_before_update = true
55
+ end
56
+ after_update do |object|
57
+ object.run_after_update = true
58
+ end
59
+
60
+ property :run_one
61
+ property :run_two
62
+ property :run_three
63
+
64
+ before_save :run_one_method, :run_two_method do |object|
65
+ object.run_three = true
66
+ end
67
+ def run_one_method
68
+ self.run_one = true
69
+ end
70
+ def run_two_method
71
+ self.run_two = true
72
+ end
73
+
74
+ attr_accessor :run_it
75
+ property :conditional_one
76
+ property :conditional_two
77
+
78
+ before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it }
79
+ def conditional_one_method
80
+ self.conditional_one = true
81
+ end
82
+ def conditional_two_method
83
+ self.conditional_two = true
84
+ end
85
+ end
86
+
87
+ class WithTemplateAndUniqueID < CouchRest::ExtendedDocument
88
+ use_database TEST_SERVER.default_database
89
+ unique_id do |model|
90
+ model['important-field']
91
+ end
92
+ property :preset, :default => 'value'
93
+ property :has_no_default
94
+ end
95
+
96
+ class WithGetterAndSetterMethods < CouchRest::ExtendedDocument
97
+ use_database TEST_SERVER.default_database
98
+
99
+ property :other_arg
100
+ def arg
101
+ other_arg
102
+ end
103
+
104
+ def arg=(value)
105
+ self.other_arg = "foo-#{value}"
106
+ end
107
+ end
108
+
109
+ class WithAfterInitializeMethod < CouchRest::ExtendedDocument
110
+ use_database TEST_SERVER.default_database
111
+
112
+ property :some_value
113
+
114
+ def after_initialize
115
+ self.some_value ||= "value"
116
+ end
117
+
118
+ end
119
+
120
+
121
+ before(:each) do
122
+ @obj = WithDefaultValues.new
123
+ end
124
+
125
+ describe "instance database connection" do
126
+ it "should use the default database" do
127
+ @obj.database.name.should == 'couchrest-test'
128
+ end
129
+
130
+ it "should override the default db" do
131
+ @obj.database = TEST_SERVER.database!('couchrest-extendedmodel-test')
132
+ @obj.database.name.should == 'couchrest-extendedmodel-test'
133
+ @obj.database.delete!
134
+ end
135
+ end
136
+
137
+ describe "a new model" do
138
+ it "should be a new document" do
139
+ @obj = Basic.new
140
+ @obj.rev.should be_nil
141
+ @obj.should be_new
142
+ @obj.should be_new_document
143
+ @obj.should be_new_record
144
+ end
145
+
146
+ it "should not failed on a nil value in argument" do
147
+ @obj = Basic.new(nil)
148
+ @obj.should == { 'couchrest-type' => 'Basic' }
149
+ end
150
+ end
151
+
152
+ describe "creating a new document" do
153
+ it "should instantialize and save a document" do
154
+ article = Article.create(:title => 'my test')
155
+ article.title.should == 'my test'
156
+ article.should_not be_new
157
+ end
158
+
159
+ it "should trigger the create callbacks" do
160
+ doc = WithCallBacks.create(:name => 'my other test')
161
+ doc.run_before_create.should be_true
162
+ doc.run_after_create.should be_true
163
+ doc.run_before_save.should be_true
164
+ doc.run_after_save.should be_true
165
+ end
166
+ end
167
+
168
+ describe "update attributes without saving" do
169
+ before(:each) do
170
+ a = Article.get "big-bad-danger" rescue nil
171
+ a.destroy if a
172
+ @art = Article.new(:title => "big bad danger")
173
+ @art.save
174
+ end
175
+ it "should work for attribute= methods" do
176
+ @art['title'].should == "big bad danger"
177
+ @art.update_attributes_without_saving('date' => Time.now, :title => "super danger")
178
+ @art['title'].should == "super danger"
179
+ end
180
+ it "should silently ignore _id" do
181
+ @art.update_attributes_without_saving('_id' => 'foobar')
182
+ @art['_id'].should_not == 'foobar'
183
+ end
184
+ it "should silently ignore _rev" do
185
+ @art.update_attributes_without_saving('_rev' => 'foobar')
186
+ @art['_rev'].should_not == 'foobar'
187
+ end
188
+ it "should silently ignore created_at" do
189
+ @art.update_attributes_without_saving('created_at' => 'foobar')
190
+ @art['created_at'].should_not == 'foobar'
191
+ end
192
+ it "should silently ignore updated_at" do
193
+ @art.update_attributes_without_saving('updated_at' => 'foobar')
194
+ @art['updated_at'].should_not == 'foobar'
195
+ end
196
+ it "should also work using attributes= alias" do
197
+ @art.respond_to?(:attributes=).should be_true
198
+ @art.attributes = {'date' => Time.now, :title => "something else"}
199
+ @art['title'].should == "something else"
200
+ end
201
+
202
+ it "should flip out if an attribute= method is missing" do
203
+ lambda {
204
+ @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
205
+ }.should raise_error
206
+ end
207
+
208
+ it "should not change other attributes if there is an error" do
209
+ lambda {
210
+ @art.update_attributes_without_saving('slug' => "new-slug", :title => "super danger")
211
+ }.should raise_error
212
+ @art['title'].should == "big bad danger"
213
+ end
214
+ end
215
+
216
+ describe "update attributes" do
217
+ before(:each) do
218
+ a = Article.get "big-bad-danger" rescue nil
219
+ a.destroy if a
220
+ @art = Article.new(:title => "big bad danger")
221
+ @art.save
222
+ end
223
+ it "should save" do
224
+ @art['title'].should == "big bad danger"
225
+ @art.update_attributes('date' => Time.now, :title => "super danger")
226
+ loaded = Article.get(@art.id)
227
+ loaded['title'].should == "super danger"
228
+ end
229
+ end
230
+
231
+ describe "with default" do
232
+ it "should have the default value set at initalization" do
233
+ @obj.preset.should == {:right => 10, :top_align => false}
234
+ end
235
+
236
+ it "should have the default false value explicitly assigned" do
237
+ @obj.default_false.should == false
238
+ end
239
+
240
+ it "should automatically call a proc default at initialization" do
241
+ @obj.set_by_proc.should be_an_instance_of(Time)
242
+ @obj.set_by_proc.should == @obj.set_by_proc
243
+ @obj.set_by_proc.should < Time.now
244
+ end
245
+
246
+ it "should let you overwrite the default values" do
247
+ obj = WithDefaultValues.new(:preset => 'test')
248
+ obj.preset = 'test'
249
+ end
250
+
251
+ it "should work with a default empty array" do
252
+ obj = WithDefaultValues.new(:tags => ['spec'])
253
+ obj.tags.should == ['spec']
254
+ end
255
+
256
+ it "should set default value of read-only property" do
257
+ obj = WithDefaultValues.new
258
+ obj.read_only_with_default.should == 'generic'
259
+ end
260
+ end
261
+
262
+ describe "a doc with template values (CR::Model spec)" do
263
+ before(:all) do
264
+ WithTemplateAndUniqueID.all.map{|o| o.destroy(true)}
265
+ WithTemplateAndUniqueID.database.bulk_delete
266
+ @tmpl = WithTemplateAndUniqueID.new
267
+ @tmpl2 = WithTemplateAndUniqueID.new(:preset => 'not_value', 'important-field' => '1')
268
+ end
269
+ it "should have fields set when new" do
270
+ @tmpl.preset.should == 'value'
271
+ end
272
+ it "shouldn't override explicitly set values" do
273
+ @tmpl2.preset.should == 'not_value'
274
+ end
275
+ it "shouldn't override existing documents" do
276
+ @tmpl2.save
277
+ tmpl2_reloaded = WithTemplateAndUniqueID.get(@tmpl2.id)
278
+ @tmpl2.preset.should == 'not_value'
279
+ tmpl2_reloaded.preset.should == 'not_value'
280
+ end
281
+ end
282
+
283
+ describe "getting a model" do
284
+ before(:all) do
285
+ @art = Article.new(:title => 'All About Getting')
286
+ @art.save
287
+ end
288
+ it "should load and instantiate it" do
289
+ foundart = Article.get @art.id
290
+ foundart.title.should == "All About Getting"
291
+ end
292
+
293
+ it "should return nil if `get` is used and the document doesn't exist" do
294
+ foundart = Article.get 'matt aimonetti'
295
+ foundart.should be_nil
296
+ end
297
+
298
+ it "should raise an error if `get!` is used and the document doesn't exist" do
299
+ lambda{foundart = Article.get!('matt aimonetti')}.should raise_error
300
+ end
301
+ end
302
+
303
+ describe "getting a model with a subobjects array" do
304
+ before(:all) do
305
+ course_doc = {
306
+ "title" => "Metaphysics 200",
307
+ "questions" => [
308
+ {
309
+ "q" => "Carve the ___ of reality at the ___.",
310
+ "a" => ["beast","joints"]
311
+ },{
312
+ "q" => "Who layed the smack down on Leibniz's Law?",
313
+ "a" => "Willard Van Orman Quine"
314
+ }
315
+ ]
316
+ }
317
+ r = Course.database.save_doc course_doc
318
+ @course = Course.get r['id']
319
+ end
320
+ it "should load the course" do
321
+ @course.title.should == "Metaphysics 200"
322
+ end
323
+ it "should instantiate them as such" do
324
+ @course["questions"][0].a[0].should == "beast"
325
+ end
326
+ end
327
+
328
+ describe "finding all instances of a model" do
329
+ before(:all) do
330
+ WithTemplateAndUniqueID.req_design_doc_refresh
331
+ WithTemplateAndUniqueID.all.map{|o| o.destroy(true)}
332
+ WithTemplateAndUniqueID.database.bulk_delete
333
+ WithTemplateAndUniqueID.new('important-field' => '1').save
334
+ WithTemplateAndUniqueID.new('important-field' => '2').save
335
+ WithTemplateAndUniqueID.new('important-field' => '3').save
336
+ WithTemplateAndUniqueID.new('important-field' => '4').save
337
+ end
338
+ it "should make the design doc" do
339
+ WithTemplateAndUniqueID.all
340
+ d = WithTemplateAndUniqueID.design_doc
341
+ d['views']['all']['map'].should include('WithTemplateAndUniqueID')
342
+ end
343
+ it "should find all" do
344
+ rs = WithTemplateAndUniqueID.all
345
+ rs.length.should == 4
346
+ end
347
+ end
348
+
349
+ describe "counting all instances of a model" do
350
+ before(:each) do
351
+ @db = reset_test_db!
352
+ WithTemplateAndUniqueID.req_design_doc_refresh
353
+ end
354
+
355
+ it ".count should return 0 if there are no docuemtns" do
356
+ WithTemplateAndUniqueID.count.should == 0
357
+ end
358
+
359
+ it ".count should return the number of documents" do
360
+ WithTemplateAndUniqueID.new('important-field' => '1').save
361
+ WithTemplateAndUniqueID.new('important-field' => '2').save
362
+ WithTemplateAndUniqueID.new('important-field' => '3').save
363
+
364
+ WithTemplateAndUniqueID.count.should == 3
365
+ end
366
+ end
367
+
368
+ describe "finding the first instance of a model" do
369
+ before(:each) do
370
+ @db = reset_test_db!
371
+ # WithTemplateAndUniqueID.req_design_doc_refresh # Removed by Sam Lown, design doc should be loaded automatically
372
+ WithTemplateAndUniqueID.new('important-field' => '1').save
373
+ WithTemplateAndUniqueID.new('important-field' => '2').save
374
+ WithTemplateAndUniqueID.new('important-field' => '3').save
375
+ WithTemplateAndUniqueID.new('important-field' => '4').save
376
+ end
377
+ it "should make the design doc" do
378
+ WithTemplateAndUniqueID.all
379
+ d = WithTemplateAndUniqueID.design_doc
380
+ d['views']['all']['map'].should include('WithTemplateAndUniqueID')
381
+ end
382
+ it "should find first" do
383
+ rs = WithTemplateAndUniqueID.first
384
+ rs['important-field'].should == "1"
385
+ end
386
+ it "should return nil if no instances are found" do
387
+ WithTemplateAndUniqueID.all.each {|obj| obj.destroy }
388
+ WithTemplateAndUniqueID.first.should be_nil
389
+ end
390
+ end
391
+
392
+ describe "lazily refreshing the design document" do
393
+ before(:all) do
394
+ @db = reset_test_db!
395
+ WithTemplateAndUniqueID.new('important-field' => '1').save
396
+ end
397
+ it "should not save the design doc twice" do
398
+ WithTemplateAndUniqueID.all
399
+ WithTemplateAndUniqueID.req_design_doc_refresh
400
+ WithTemplateAndUniqueID.refresh_design_doc
401
+ rev = WithTemplateAndUniqueID.design_doc['_rev']
402
+ WithTemplateAndUniqueID.req_design_doc_refresh
403
+ WithTemplateAndUniqueID.refresh_design_doc
404
+ WithTemplateAndUniqueID.design_doc['_rev'].should eql(rev)
405
+ end
406
+ end
407
+
408
+ describe "getting a model with a subobject field" do
409
+ before(:all) do
410
+ course_doc = {
411
+ "title" => "Metaphysics 410",
412
+ "professor" => {
413
+ "name" => ["Mark", "Hinchliff"]
414
+ },
415
+ "ends_at" => "2008/12/19 13:00:00 +0800"
416
+ }
417
+ r = Course.database.save_doc course_doc
418
+ @course = Course.get r['id']
419
+ end
420
+ it "should load the course" do
421
+ @course["professor"]["name"][1].should == "Hinchliff"
422
+ end
423
+ it "should instantiate the professor as a person" do
424
+ @course['professor'].last_name.should == "Hinchliff"
425
+ end
426
+ it "should instantiate the ends_at as a Time" do
427
+ @course['ends_at'].should == Time.parse("2008/12/19 13:00:00 +0800")
428
+ end
429
+ end
430
+
431
+ describe "timestamping" do
432
+ before(:each) do
433
+ oldart = Article.get "saving-this" rescue nil
434
+ oldart.destroy if oldart
435
+ @art = Article.new(:title => "Saving this")
436
+ @art.save
437
+ end
438
+
439
+ it "should define the updated_at and created_at getters and set the values" do
440
+ @obj.save
441
+ obj = WithDefaultValues.get(@obj.id)
442
+ obj.should be_an_instance_of(WithDefaultValues)
443
+ obj.created_at.should be_an_instance_of(Time)
444
+ obj.updated_at.should be_an_instance_of(Time)
445
+ obj.created_at.to_s.should == @obj.updated_at.to_s
446
+ end
447
+
448
+ it "should not change created_at on update" do
449
+ 2.times do
450
+ lambda do
451
+ @art.save
452
+ end.should_not change(@art, :created_at)
453
+ end
454
+ end
455
+
456
+ it "should set the time on create" do
457
+ (Time.now - @art.created_at).should < 2
458
+ foundart = Article.get @art.id
459
+ foundart.created_at.should == foundart.updated_at
460
+ end
461
+ it "should set the time on update" do
462
+ @art.save
463
+ @art.created_at.should < @art.updated_at
464
+ end
465
+ end
466
+
467
+ describe "basic saving and retrieving" do
468
+ it "should work fine" do
469
+ @obj.name = "should be easily saved and retrieved"
470
+ @obj.save
471
+ saved_obj = WithDefaultValues.get(@obj.id)
472
+ saved_obj.should_not be_nil
473
+ end
474
+
475
+ it "should parse the Time attributes automatically" do
476
+ @obj.name = "should parse the Time attributes automatically"
477
+ @obj.set_by_proc.should be_an_instance_of(Time)
478
+ @obj.save
479
+ @obj.set_by_proc.should be_an_instance_of(Time)
480
+ saved_obj = WithDefaultValues.get(@obj.id)
481
+ saved_obj.set_by_proc.should be_an_instance_of(Time)
482
+ end
483
+ end
484
+
485
+ describe "saving a model" do
486
+ before(:all) do
487
+ @sobj = Basic.new
488
+ @sobj.save.should == true
489
+ end
490
+
491
+ it "should save the doc" do
492
+ doc = Basic.get(@sobj.id)
493
+ doc['_id'].should == @sobj.id
494
+ end
495
+
496
+ it "should be set for resaving" do
497
+ rev = @obj.rev
498
+ @sobj['another-key'] = "some value"
499
+ @sobj.save
500
+ @sobj.rev.should_not == rev
501
+ end
502
+
503
+ it "should set the id" do
504
+ @sobj.id.should be_an_instance_of(String)
505
+ end
506
+
507
+ it "should set the type" do
508
+ @sobj['couchrest-type'].should == 'Basic'
509
+ end
510
+
511
+ describe "save!" do
512
+
513
+ before(:each) do
514
+ @sobj = Card.new(:first_name => "Marcos", :last_name => "Tapajós")
515
+ end
516
+
517
+ it "should return true if save the document" do
518
+ @sobj.save!.should == true
519
+ end
520
+
521
+ it "should raise error if don't save the document" do
522
+ @sobj.first_name = nil
523
+ lambda { @sobj.save!.should == true }.should raise_error(RuntimeError)
524
+ end
525
+
526
+ end
527
+
528
+
529
+ end
530
+
531
+ describe "saving a model with a unique_id configured" do
532
+ before(:each) do
533
+ @art = Article.new
534
+ @old = Article.database.get('this-is-the-title') rescue nil
535
+ Article.database.delete_doc(@old) if @old
536
+ end
537
+
538
+ it "should be a new document" do
539
+ @art.should be_new
540
+ @art.title.should be_nil
541
+ end
542
+
543
+ it "should require the title" do
544
+ lambda{@art.save}.should raise_error
545
+ @art.title = 'This is the title'
546
+ @art.save.should == true
547
+ end
548
+
549
+ it "should not change the slug on update" do
550
+ @art.title = 'This is the title'
551
+ @art.save.should == true
552
+ @art.title = 'new title'
553
+ @art.save.should == true
554
+ @art.slug.should == 'this-is-the-title'
555
+ end
556
+
557
+ it "should raise an error when the slug is taken" do
558
+ @art.title = 'This is the title'
559
+ @art.save.should == true
560
+ @art2 = Article.new(:title => 'This is the title!')
561
+ lambda{@art2.save}.should raise_error
562
+ end
563
+
564
+ it "should set the slug" do
565
+ @art.title = 'This is the title'
566
+ @art.save.should == true
567
+ @art.slug.should == 'this-is-the-title'
568
+ end
569
+
570
+ it "should set the id" do
571
+ @art.title = 'This is the title'
572
+ @art.save.should == true
573
+ @art.id.should == 'this-is-the-title'
574
+ end
575
+ end
576
+
577
+ describe "saving a model with a unique_id lambda" do
578
+ before(:each) do
579
+ @templated = WithTemplateAndUniqueID.new
580
+ @old = WithTemplateAndUniqueID.get('very-important') rescue nil
581
+ @old.destroy if @old
582
+ end
583
+
584
+ it "should require the field" do
585
+ lambda{@templated.save}.should raise_error
586
+ @templated['important-field'] = 'very-important'
587
+ @templated.save.should == true
588
+ end
589
+
590
+ it "should save with the id" do
591
+ @templated['important-field'] = 'very-important'
592
+ @templated.save.should == true
593
+ t = WithTemplateAndUniqueID.get('very-important')
594
+ t.should == @templated
595
+ end
596
+
597
+ it "should not change the id on update" do
598
+ @templated['important-field'] = 'very-important'
599
+ @templated.save.should == true
600
+ @templated['important-field'] = 'not-important'
601
+ @templated.save.should == true
602
+ t = WithTemplateAndUniqueID.get('very-important')
603
+ t.should == @templated
604
+ end
605
+
606
+ it "should raise an error when the id is taken" do
607
+ @templated['important-field'] = 'very-important'
608
+ @templated.save.should == true
609
+ lambda{WithTemplateAndUniqueID.new('important-field' => 'very-important').save}.should raise_error
610
+ end
611
+
612
+ it "should set the id" do
613
+ @templated['important-field'] = 'very-important'
614
+ @templated.save.should == true
615
+ @templated.id.should == 'very-important'
616
+ end
617
+ end
618
+
619
+ describe "destroying an instance" do
620
+ before(:each) do
621
+ @dobj = Basic.new
622
+ @dobj.save.should == true
623
+ end
624
+ it "should return true" do
625
+ result = @dobj.destroy
626
+ result.should == true
627
+ end
628
+ it "should be resavable" do
629
+ @dobj.destroy
630
+ @dobj.rev.should be_nil
631
+ @dobj.id.should be_nil
632
+ @dobj.save.should == true
633
+ end
634
+ it "should make it go away" do
635
+ @dobj.destroy
636
+ lambda{Basic.get!(@dobj.id)}.should raise_error
637
+ end
638
+ end
639
+
640
+
641
+ describe "callbacks" do
642
+
643
+ before(:each) do
644
+ @doc = WithCallBacks.new
645
+ end
646
+
647
+
648
+ describe "validate" do
649
+ it "should run before_validate before validating" do
650
+ @doc.run_before_validate.should be_nil
651
+ @doc.should be_valid
652
+ @doc.run_before_validate.should be_true
653
+ end
654
+ it "should run after_validate after validating" do
655
+ @doc.run_after_validate.should be_nil
656
+ @doc.should be_valid
657
+ @doc.run_after_validate.should be_true
658
+ end
659
+ end
660
+
661
+ describe "save" do
662
+ it "should run the after filter after saving" do
663
+ @doc.run_after_save.should be_nil
664
+ @doc.save.should be_true
665
+ @doc.run_after_save.should be_true
666
+ end
667
+ it "should run the grouped callbacks before saving" do
668
+ @doc.run_one.should be_nil
669
+ @doc.run_two.should be_nil
670
+ @doc.run_three.should be_nil
671
+ @doc.save.should be_true
672
+ @doc.run_one.should be_true
673
+ @doc.run_two.should be_true
674
+ @doc.run_three.should be_true
675
+ end
676
+ it "should not run conditional callbacks" do
677
+ @doc.run_it = false
678
+ @doc.save.should be_true
679
+ @doc.conditional_one.should be_nil
680
+ @doc.conditional_two.should be_nil
681
+ end
682
+ it "should run conditional callbacks" do
683
+ @doc.run_it = true
684
+ @doc.save.should be_true
685
+ @doc.conditional_one.should be_true
686
+ @doc.conditional_two.should be_true
687
+ end
688
+ end
689
+ describe "create" do
690
+ it "should run the before save filter when creating" do
691
+ @doc.run_before_save.should be_nil
692
+ @doc.create.should_not be_nil
693
+ @doc.run_before_save.should be_true
694
+ end
695
+ it "should run the before create filter" do
696
+ @doc.run_before_create.should be_nil
697
+ @doc.create.should_not be_nil
698
+ @doc.create
699
+ @doc.run_before_create.should be_true
700
+ end
701
+ it "should run the after create filter" do
702
+ @doc.run_after_create.should be_nil
703
+ @doc.create.should_not be_nil
704
+ @doc.create
705
+ @doc.run_after_create.should be_true
706
+ end
707
+ end
708
+ describe "update" do
709
+
710
+ before(:each) do
711
+ @doc.save
712
+ end
713
+ it "should run the before update filter when updating an existing document" do
714
+ @doc.run_before_update.should be_nil
715
+ @doc.update
716
+ @doc.run_before_update.should be_true
717
+ end
718
+ it "should run the after update filter when updating an existing document" do
719
+ @doc.run_after_update.should be_nil
720
+ @doc.update
721
+ @doc.run_after_update.should be_true
722
+ end
723
+ it "should run the before update filter when saving an existing document" do
724
+ @doc.run_before_update.should be_nil
725
+ @doc.save
726
+ @doc.run_before_update.should be_true
727
+ end
728
+
729
+ end
730
+ end
731
+
732
+ describe "getter and setter methods" do
733
+ it "should try to call the arg= method before setting :arg in the hash" do
734
+ @doc = WithGetterAndSetterMethods.new(:arg => "foo")
735
+ @doc['arg'].should be_nil
736
+ @doc[:arg].should be_nil
737
+ @doc.other_arg.should == "foo-foo"
738
+ end
739
+ end
740
+
741
+ describe "initialization" do
742
+ it "should call after_initialize method if available" do
743
+ @doc = WithAfterInitializeMethod.new
744
+ @doc['some_value'].should eql('value')
745
+ end
746
+ end
747
+
748
+ describe "recursive validation on an extended document" do
749
+ before :each do
750
+ reset_test_db!
751
+ @cat = Cat.new(:name => 'Sockington')
752
+ end
753
+
754
+ it "should not save if a nested casted model is invalid" do
755
+ @cat.favorite_toy = CatToy.new
756
+ @cat.should_not be_valid
757
+ @cat.save.should be_false
758
+ lambda{@cat.save!}.should raise_error
759
+ end
760
+
761
+ it "should save when nested casted model is valid" do
762
+ @cat.favorite_toy = CatToy.new(:name => 'Squeaky')
763
+ @cat.should be_valid
764
+ @cat.save.should be_true
765
+ lambda{@cat.save!}.should_not raise_error
766
+ end
767
+
768
+ it "should not save when nested collection contains an invalid casted model" do
769
+ @cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
770
+ @cat.should_not be_valid
771
+ @cat.save.should be_false
772
+ lambda{@cat.save!}.should raise_error
773
+ end
774
+
775
+ it "should save when nested collection contains valid casted models" do
776
+ @cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
777
+ @cat.should be_valid
778
+ @cat.save.should be_true
779
+ lambda{@cat.save!}.should_not raise_error
780
+ end
781
+
782
+ it "should not fail if the nested casted model doesn't have validation" do
783
+ Cat.property :trainer, :cast_as => 'Person'
784
+ Cat.validates_presence_of :name
785
+ cat = Cat.new(:name => 'Mr Bigglesworth')
786
+ cat.trainer = Person.new
787
+ cat.trainer.validatable?.should be_false
788
+ cat.should be_valid
789
+ cat.save.should be_true
790
+ end
791
+ end
792
+
793
+ describe "searching the contents of an extended document" do
794
+ before :each do
795
+ @db = reset_test_db!
796
+
797
+ names = ["Fuzzy", "Whiskers", "Mr Bigglesworth", "Sockington", "Smitty", "Sammy", "Samson", "Simon"]
798
+ names.each { |name| Cat.create(:name => name) }
799
+
800
+ search_function = { 'defaults' => {'store' => 'no', 'index' => 'analyzed_no_norms'},
801
+ 'index' => "function(doc) { ret = new Document(); ret.add(doc['name'], {'field':'name'}); return ret; }" }
802
+ @db.save_doc({'_id' => '_design/search', 'fulltext' => {'cats' => search_function}})
803
+ end
804
+
805
+ it "should be able to paginate through a large set of search results" do
806
+ if couchdb_lucene_available?
807
+ names = []
808
+ Cat.paginated_each(:design_doc => "search", :view_name => "cats",
809
+ :q => 'name:S*', :search => true, :include_docs => true, :per_page => 3) do |cat|
810
+ cat.should_not be_nil
811
+ names << cat.name
812
+ end
813
+
814
+ names.size.should == 5
815
+ names.should include('Sockington')
816
+ names.should include('Smitty')
817
+ names.should include('Sammy')
818
+ names.should include('Samson')
819
+ names.should include('Simon')
820
+ end
821
+ end
822
+ end
823
+
824
+ end