couchrest_model-radiant 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 (73) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +19 -0
  3. data/Rakefile +74 -0
  4. data/THANKS.md +21 -0
  5. data/history.txt +207 -0
  6. data/lib/couchrest/model.rb +10 -0
  7. data/lib/couchrest/model/associations.rb +223 -0
  8. data/lib/couchrest/model/base.rb +111 -0
  9. data/lib/couchrest/model/callbacks.rb +27 -0
  10. data/lib/couchrest/model/casted_array.rb +39 -0
  11. data/lib/couchrest/model/casted_model.rb +68 -0
  12. data/lib/couchrest/model/class_proxy.rb +122 -0
  13. data/lib/couchrest/model/collection.rb +263 -0
  14. data/lib/couchrest/model/configuration.rb +51 -0
  15. data/lib/couchrest/model/design_doc.rb +123 -0
  16. data/lib/couchrest/model/document_queries.rb +83 -0
  17. data/lib/couchrest/model/errors.rb +23 -0
  18. data/lib/couchrest/model/extended_attachments.rb +77 -0
  19. data/lib/couchrest/model/persistence.rb +155 -0
  20. data/lib/couchrest/model/properties.rb +208 -0
  21. data/lib/couchrest/model/property.rb +97 -0
  22. data/lib/couchrest/model/property_protection.rb +71 -0
  23. data/lib/couchrest/model/support/couchrest.rb +19 -0
  24. data/lib/couchrest/model/support/hash.rb +9 -0
  25. data/lib/couchrest/model/typecast.rb +175 -0
  26. data/lib/couchrest/model/validations.rb +68 -0
  27. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  28. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  29. data/lib/couchrest/model/validations/uniqueness.rb +44 -0
  30. data/lib/couchrest/model/views.rb +160 -0
  31. data/lib/couchrest/railtie.rb +12 -0
  32. data/lib/couchrest_model.rb +62 -0
  33. data/lib/rails/generators/couchrest_model.rb +16 -0
  34. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  35. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  36. data/spec/couchrest/assocations_spec.rb +196 -0
  37. data/spec/couchrest/attachment_spec.rb +176 -0
  38. data/spec/couchrest/base_spec.rb +463 -0
  39. data/spec/couchrest/casted_model_spec.rb +438 -0
  40. data/spec/couchrest/casted_spec.rb +75 -0
  41. data/spec/couchrest/class_proxy_spec.rb +132 -0
  42. data/spec/couchrest/configuration_spec.rb +78 -0
  43. data/spec/couchrest/inherited_spec.rb +40 -0
  44. data/spec/couchrest/persistence_spec.rb +415 -0
  45. data/spec/couchrest/property_protection_spec.rb +192 -0
  46. data/spec/couchrest/property_spec.rb +871 -0
  47. data/spec/couchrest/subclass_spec.rb +99 -0
  48. data/spec/couchrest/validations.rb +85 -0
  49. data/spec/couchrest/view_spec.rb +463 -0
  50. data/spec/fixtures/attachments/README +3 -0
  51. data/spec/fixtures/attachments/couchdb.png +0 -0
  52. data/spec/fixtures/attachments/test.html +11 -0
  53. data/spec/fixtures/base.rb +139 -0
  54. data/spec/fixtures/more/article.rb +35 -0
  55. data/spec/fixtures/more/card.rb +17 -0
  56. data/spec/fixtures/more/cat.rb +19 -0
  57. data/spec/fixtures/more/client.rb +6 -0
  58. data/spec/fixtures/more/course.rb +25 -0
  59. data/spec/fixtures/more/event.rb +8 -0
  60. data/spec/fixtures/more/invoice.rb +14 -0
  61. data/spec/fixtures/more/person.rb +9 -0
  62. data/spec/fixtures/more/question.rb +7 -0
  63. data/spec/fixtures/more/sale_entry.rb +9 -0
  64. data/spec/fixtures/more/sale_invoice.rb +13 -0
  65. data/spec/fixtures/more/service.rb +10 -0
  66. data/spec/fixtures/more/user.rb +22 -0
  67. data/spec/fixtures/views/lib.js +3 -0
  68. data/spec/fixtures/views/test_view/lib.js +3 -0
  69. data/spec/fixtures/views/test_view/only-map.js +4 -0
  70. data/spec/fixtures/views/test_view/test-map.js +3 -0
  71. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  72. data/spec/spec_helper.rb +48 -0
  73. metadata +263 -0
@@ -0,0 +1,192 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe "Model Attributes" do
4
+
5
+ describe "no declarations" do
6
+ class NoProtection < CouchRest::Model::Base
7
+ use_database TEST_SERVER.default_database
8
+ property :name
9
+ property :phone
10
+ end
11
+
12
+ it "should not protect anything through new" do
13
+ user = NoProtection.new(:name => "will", :phone => "555-5555")
14
+
15
+ user.name.should == "will"
16
+ user.phone.should == "555-5555"
17
+ end
18
+
19
+ it "should not protect anything through attributes=" do
20
+ user = NoProtection.new
21
+ user.attributes = {:name => "will", :phone => "555-5555"}
22
+
23
+ user.name.should == "will"
24
+ user.phone.should == "555-5555"
25
+ end
26
+
27
+ it "should recreate from the database properly" do
28
+ user = NoProtection.new
29
+ user.name = "will"
30
+ user.phone = "555-5555"
31
+ user.save!
32
+
33
+ user = NoProtection.get(user.id)
34
+ user.name.should == "will"
35
+ user.phone.should == "555-5555"
36
+ end
37
+
38
+ it "should provide a list of all properties as accessible" do
39
+ user = NoProtection.new(:name => "will", :phone => "555-5555")
40
+ user.accessible_properties.length.should eql(2)
41
+ user.protected_properties.should be_empty
42
+ end
43
+ end
44
+
45
+ describe "Model Base", "accessible flag" do
46
+ class WithAccessible < CouchRest::Model::Base
47
+ use_database TEST_SERVER.default_database
48
+ property :name, :accessible => true
49
+ property :admin, :default => false
50
+ end
51
+
52
+ it { expect { WithAccessible.new(nil) }.to_not raise_error }
53
+
54
+ it "should recognize accessible properties" do
55
+ props = WithAccessible.accessible_properties.map { |prop| prop.name}
56
+ props.should include("name")
57
+ props.should_not include("admin")
58
+ end
59
+
60
+ it "should protect non-accessible properties set through new" do
61
+ user = WithAccessible.new(:name => "will", :admin => true)
62
+
63
+ user.name.should == "will"
64
+ user.admin.should == false
65
+ end
66
+
67
+ it "should protect non-accessible properties set through attributes=" do
68
+ user = WithAccessible.new
69
+ user.attributes = {:name => "will", :admin => true}
70
+
71
+ user.name.should == "will"
72
+ user.admin.should == false
73
+ end
74
+
75
+ it "should provide correct accessible and protected property lists" do
76
+ user = WithAccessible.new(:name => 'will', :admin => true)
77
+ user.accessible_properties.map{|p| p.to_s}.should eql(['name'])
78
+ user.protected_properties.map{|p| p.to_s}.should eql(['admin'])
79
+ end
80
+ end
81
+
82
+ describe "Model Base", "protected flag" do
83
+ class WithProtected < CouchRest::Model::Base
84
+ use_database TEST_SERVER.default_database
85
+ property :name
86
+ property :admin, :default => false, :protected => true
87
+ end
88
+
89
+ it { expect { WithProtected.new(nil) }.to_not raise_error }
90
+
91
+ it "should recognize protected properties" do
92
+ props = WithProtected.protected_properties.map { |prop| prop.name}
93
+ props.should_not include("name")
94
+ props.should include("admin")
95
+ end
96
+
97
+ it "should protect non-accessible properties set through new" do
98
+ user = WithProtected.new(:name => "will", :admin => true)
99
+
100
+ user.name.should == "will"
101
+ user.admin.should == false
102
+ end
103
+
104
+ it "should protect non-accessible properties set through attributes=" do
105
+ user = WithProtected.new
106
+ user.attributes = {:name => "will", :admin => true}
107
+
108
+ user.name.should == "will"
109
+ user.admin.should == false
110
+ end
111
+
112
+ it "should not modify the provided attribute hash" do
113
+ user = WithProtected.new
114
+ attrs = {:name => "will", :admin => true}
115
+ user.attributes = attrs
116
+ attrs[:admin].should be_true
117
+ attrs[:name].should eql('will')
118
+ end
119
+
120
+ it "should provide correct accessible and protected property lists" do
121
+ user = WithProtected.new(:name => 'will', :admin => true)
122
+ user.accessible_properties.map{|p| p.to_s}.should eql(['name'])
123
+ user.protected_properties.map{|p| p.to_s}.should eql(['admin'])
124
+ end
125
+
126
+ end
127
+
128
+ describe "Model Base", "mixing protected and accessible flags" do
129
+ class WithBothAndUnspecified < CouchRest::Model::Base
130
+ use_database TEST_SERVER.default_database
131
+ property :name, :accessible => true
132
+ property :admin, :default => false, :protected => true
133
+ property :phone, :default => 'unset phone number'
134
+ end
135
+
136
+ it { expect { WithBothAndUnspecified.new }.to_not raise_error }
137
+
138
+ it 'should assume that any unspecified property is protected by default' do
139
+ user = WithBothAndUnspecified.new(:name => 'will', :admin => true, :phone => '555-1234')
140
+
141
+ user.name.should == 'will'
142
+ user.admin.should == false
143
+ user.phone.should == 'unset phone number'
144
+ end
145
+
146
+ end
147
+
148
+ describe "from database" do
149
+ class WithProtected < CouchRest::Model::Base
150
+ use_database TEST_SERVER.default_database
151
+ property :name
152
+ property :admin, :default => false, :protected => true
153
+ view_by :name
154
+ end
155
+
156
+ before(:each) do
157
+ @user = WithProtected.new
158
+ @user.name = "will"
159
+ @user.admin = true
160
+ @user.save!
161
+ end
162
+
163
+ def verify_attrs(user)
164
+ user.name.should == "will"
165
+ user.admin.should == true
166
+ end
167
+
168
+ it "Base#get should not strip protected attributes" do
169
+ reloaded = WithProtected.get( @user.id )
170
+ verify_attrs reloaded
171
+ end
172
+
173
+ it "Base#get! should not strip protected attributes" do
174
+ reloaded = WithProtected.get!( @user.id )
175
+ verify_attrs reloaded
176
+ end
177
+
178
+ it "Base#all should not strip protected attributes" do
179
+ # all creates a CollectionProxy
180
+ docs = WithProtected.all(:key => @user.id)
181
+ docs.length.should == 1
182
+ reloaded = docs.first
183
+ verify_attrs reloaded
184
+ end
185
+
186
+ it "views should not strip protected attributes" do
187
+ docs = WithProtected.by_name(:startkey => "will", :endkey => "will")
188
+ reloaded = docs.first
189
+ verify_attrs reloaded
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,871 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../../spec_helper', __FILE__)
3
+ require File.join(FIXTURE_PATH, 'more', 'cat')
4
+ require File.join(FIXTURE_PATH, 'more', 'person')
5
+ require File.join(FIXTURE_PATH, 'more', 'card')
6
+ require File.join(FIXTURE_PATH, 'more', 'invoice')
7
+ require File.join(FIXTURE_PATH, 'more', 'service')
8
+ require File.join(FIXTURE_PATH, 'more', 'event')
9
+ require File.join(FIXTURE_PATH, 'more', 'user')
10
+ require File.join(FIXTURE_PATH, 'more', 'course')
11
+
12
+
13
+ describe "Model properties" do
14
+
15
+ before(:each) do
16
+ reset_test_db!
17
+ @card = Card.new(:first_name => "matt")
18
+ end
19
+
20
+ it "should be accessible from the object" do
21
+ @card.properties.should be_an_instance_of(Array)
22
+ @card.properties.map{|p| p.name}.should include("first_name")
23
+ end
24
+
25
+ it "should let you access a property value (getter)" do
26
+ @card.first_name.should == "matt"
27
+ end
28
+
29
+ it "should let you set a property value (setter)" do
30
+ @card.last_name = "Aimonetti"
31
+ @card.last_name.should == "Aimonetti"
32
+ end
33
+
34
+ it "should not let you set a property value if it's read only" do
35
+ lambda{@card.read_only_value = "test"}.should raise_error
36
+ end
37
+
38
+ it "should let you use an alias for an attribute" do
39
+ @card.last_name = "Aimonetti"
40
+ @card.family_name.should == "Aimonetti"
41
+ @card.family_name.should == @card.last_name
42
+ end
43
+
44
+ it "should let you use an alias for a casted attribute" do
45
+ @card.cast_alias = Person.new(:name => ["Aimonetti"])
46
+ @card.cast_alias.name.should == ["Aimonetti"]
47
+ @card.calias.name.should == ["Aimonetti"]
48
+ card = Card.new(:first_name => "matt", :cast_alias => {:name => ["Aimonetti"]})
49
+ card.cast_alias.name.should == ["Aimonetti"]
50
+ card.calias.name.should == ["Aimonetti"]
51
+ end
52
+
53
+
54
+ it "should be auto timestamped" do
55
+ @card.created_at.should be_nil
56
+ @card.updated_at.should be_nil
57
+ @card.save.should be_true
58
+ @card.created_at.should_not be_nil
59
+ @card.updated_at.should_not be_nil
60
+ end
61
+
62
+ describe '#read_attribute' do
63
+ it "should let you use read_attribute method" do
64
+ @card.last_name = "Aimonetti"
65
+ @card.read_attribute(:last_name).should eql('Aimonetti')
66
+ @card.read_attribute('last_name').should eql('Aimonetti')
67
+ last_name_prop = @card.properties.find{|p| p.name == 'last_name'}
68
+ @card.read_attribute(last_name_prop).should eql('Aimonetti')
69
+ end
70
+
71
+ it 'should raise an error if the property does not exist' do
72
+ expect { @card.read_attribute(:this_property_should_not_exist) }.to raise_error(ArgumentError)
73
+ end
74
+ end
75
+
76
+ describe '#write_attribute' do
77
+ it "should let you use write_attribute method" do
78
+ @card.write_attribute(:last_name, 'Aimonetti 1')
79
+ @card.last_name.should eql('Aimonetti 1')
80
+ @card.write_attribute('last_name', 'Aimonetti 2')
81
+ @card.last_name.should eql('Aimonetti 2')
82
+ last_name_prop = @card.properties.find{|p| p.name == 'last_name'}
83
+ @card.write_attribute(last_name_prop, 'Aimonetti 3')
84
+ @card.last_name.should eql('Aimonetti 3')
85
+ end
86
+
87
+ it 'should raise an error if the property does not exist' do
88
+ expect { @card.write_attribute(:this_property_should_not_exist, 823) }.to raise_error(ArgumentError)
89
+ end
90
+
91
+
92
+ it "should let you use write_attribute on readonly properties" do
93
+ lambda {
94
+ @card.read_only_value = "foo"
95
+ }.should raise_error
96
+ @card.write_attribute(:read_only_value, "foo")
97
+ @card.read_only_value.should == 'foo'
98
+ end
99
+
100
+ it "should cast via write_attribute" do
101
+ @card.write_attribute(:cast_alias, {:name => ["Sam", "Lown"]})
102
+ @card.cast_alias.class.should eql(Person)
103
+ @card.cast_alias.name.last.should eql("Lown")
104
+ end
105
+
106
+ it "should not cast via write_attribute if property not casted" do
107
+ @card.write_attribute(:first_name, {:name => "Sam"})
108
+ @card.first_name.class.should eql(Hash)
109
+ @card.first_name[:name].should eql("Sam")
110
+ end
111
+ end
112
+
113
+ describe "mass updating attributes without property" do
114
+
115
+ describe "when mass_assign_any_attribute false" do
116
+
117
+ it "should not allow them to be set" do
118
+ @card.attributes = {:test => 'fooobar'}
119
+ @card['test'].should be_nil
120
+ end
121
+
122
+ end
123
+
124
+ describe "when mass_assign_any_attribute true" do
125
+ before(:each) do
126
+ # dup Card class so that no other tests are effected
127
+ card_class = Card.dup
128
+ card_class.class_eval do
129
+ mass_assign_any_attribute true
130
+ end
131
+ @card = card_class.new(:first_name => 'Sam')
132
+ end
133
+
134
+ it 'should allow them to be updated' do
135
+ @card.attributes = {:test => 'fooobar'}
136
+ @card['test'].should eql('fooobar')
137
+ end
138
+ end
139
+ end
140
+
141
+
142
+ describe "mass assignment protection" do
143
+
144
+ it "should not store protected attribute using mass assignment" do
145
+ cat_toy = CatToy.new(:name => "Zorro")
146
+ cat = Cat.create(:name => "Helena", :toys => [cat_toy], :favorite_toy => cat_toy, :number => 1)
147
+ cat.number.should be_nil
148
+ cat.number = 1
149
+ cat.save
150
+ cat.number.should == 1
151
+ end
152
+
153
+ it "should not store protected attribute when 'declare accessible poperties, assume all the rest are protected'" do
154
+ user = User.create(:name => "Marcos Tapajós", :admin => true)
155
+ user.admin.should be_nil
156
+ end
157
+
158
+ it "should not store protected attribute when 'declare protected properties, assume all the rest are accessible'" do
159
+ user = SpecialUser.create(:name => "Marcos Tapajós", :admin => true)
160
+ user.admin.should be_nil
161
+ end
162
+
163
+ end
164
+
165
+ describe "validation" do
166
+ before(:each) do
167
+ @invoice = Invoice.new(:client_name => "matt", :employee_name => "Chris", :location => "San Diego, CA")
168
+ end
169
+
170
+ it "should be able to be validated" do
171
+ @card.valid?.should == true
172
+ end
173
+
174
+ it "should let you validate the presence of an attribute" do
175
+ @card.first_name = nil
176
+ @card.should_not be_valid
177
+ @card.errors.should_not be_empty
178
+ @card.errors[:first_name].should == ["can't be blank"]
179
+ end
180
+
181
+ it "should let you look up errors for a field by a string name" do
182
+ @card.first_name = nil
183
+ @card.should_not be_valid
184
+ @card.errors['first_name'].should == ["can't be blank"]
185
+ end
186
+
187
+ it "should validate the presence of 2 attributes" do
188
+ @invoice.clear
189
+ @invoice.should_not be_valid
190
+ @invoice.errors.should_not be_empty
191
+ @invoice.errors[:client_name].should == ["can't be blank"]
192
+ @invoice.errors[:employee_name].should_not be_empty
193
+ end
194
+
195
+ it "should let you set an error message" do
196
+ @invoice.location = nil
197
+ @invoice.valid?
198
+ @invoice.errors[:location].should == ["Hey stupid!, you forgot the location"]
199
+ end
200
+
201
+ it "should validate before saving" do
202
+ @invoice.location = nil
203
+ @invoice.should_not be_valid
204
+ @invoice.save.should be_false
205
+ @invoice.should be_new
206
+ end
207
+ end
208
+
209
+ describe "casting" do
210
+ before(:each) do
211
+ @course = Course.new(:title => 'Relaxation')
212
+ end
213
+
214
+ describe "when value is nil" do
215
+ it "leaves the value unchanged" do
216
+ @course.title = nil
217
+ @course['title'].should == nil
218
+ end
219
+ end
220
+
221
+ describe "when type primitive is an Object" do
222
+ it "it should not cast given value" do
223
+ @course.participants = [{}, 'q', 1]
224
+ @course['participants'].should == [{}, 'q', 1]
225
+ end
226
+
227
+ it "should cast started_on to Date" do
228
+ @course.started_on = Date.today
229
+ @course['started_on'].should be_an_instance_of(Date)
230
+ end
231
+ end
232
+
233
+ describe "when type primitive is a String" do
234
+ it "keeps string value unchanged" do
235
+ value = "1.0"
236
+ @course.title = value
237
+ @course['title'].should equal(value)
238
+ end
239
+
240
+ it "it casts to string representation of the value" do
241
+ @course.title = 1.0
242
+ @course['title'].should eql("1.0")
243
+ end
244
+ end
245
+
246
+ describe 'when type primitive is a Float' do
247
+ it 'returns same value if a float' do
248
+ value = 24.0
249
+ @course.estimate = value
250
+ @course['estimate'].should equal(value)
251
+ end
252
+
253
+ it 'returns float representation of a zero string integer' do
254
+ @course.estimate = '0'
255
+ @course['estimate'].should eql(0.0)
256
+ end
257
+
258
+ it 'returns float representation of a positive string integer' do
259
+ @course.estimate = '24'
260
+ @course['estimate'].should eql(24.0)
261
+ end
262
+
263
+ it 'returns float representation of a negative string integer' do
264
+ @course.estimate = '-24'
265
+ @course['estimate'].should eql(-24.0)
266
+ end
267
+
268
+ it 'returns float representation of a zero string float' do
269
+ @course.estimate = '0.0'
270
+ @course['estimate'].should eql(0.0)
271
+ end
272
+
273
+ it 'returns float representation of a positive string float' do
274
+ @course.estimate = '24.35'
275
+ @course['estimate'].should eql(24.35)
276
+ end
277
+
278
+ it 'returns float representation of a negative string float' do
279
+ @course.estimate = '-24.35'
280
+ @course['estimate'].should eql(-24.35)
281
+ end
282
+
283
+ it 'returns float representation of a zero string float, with no leading digits' do
284
+ @course.estimate = '.0'
285
+ @course['estimate'].should eql(0.0)
286
+ end
287
+
288
+ it 'returns float representation of a positive string float, with no leading digits' do
289
+ @course.estimate = '.41'
290
+ @course['estimate'].should eql(0.41)
291
+ end
292
+
293
+ it 'returns float representation of a zero integer' do
294
+ @course.estimate = 0
295
+ @course['estimate'].should eql(0.0)
296
+ end
297
+
298
+ it 'returns float representation of a positive integer' do
299
+ @course.estimate = 24
300
+ @course['estimate'].should eql(24.0)
301
+ end
302
+
303
+ it 'returns float representation of a negative integer' do
304
+ @course.estimate = -24
305
+ @course['estimate'].should eql(-24.0)
306
+ end
307
+
308
+ it 'returns float representation of a zero decimal' do
309
+ @course.estimate = BigDecimal('0.0')
310
+ @course['estimate'].should eql(0.0)
311
+ end
312
+
313
+ it 'returns float representation of a positive decimal' do
314
+ @course.estimate = BigDecimal('24.35')
315
+ @course['estimate'].should eql(24.35)
316
+ end
317
+
318
+ it 'returns float representation of a negative decimal' do
319
+ @course.estimate = BigDecimal('-24.35')
320
+ @course['estimate'].should eql(-24.35)
321
+ end
322
+
323
+ it 'return float of a number with commas instead of points for decimals' do
324
+ @course.estimate = '23,35'
325
+ @course['estimate'].should eql(23.35)
326
+ end
327
+
328
+ it "should handle numbers with commas and points" do
329
+ @course.estimate = '1,234.00'
330
+ @course.estimate.should eql(1234.00)
331
+ end
332
+
333
+ it "should handle a mis-match of commas and points and maintain the last one" do
334
+ @course.estimate = "1,232.434.123,323"
335
+ @course.estimate.should eql(1232434123.323)
336
+ end
337
+
338
+ [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value|
339
+ it "does not typecast non-numeric value #{value.inspect}" do
340
+ @course.estimate = value
341
+ @course['estimate'].should equal(value)
342
+ end
343
+ end
344
+
345
+ end
346
+
347
+ describe 'when type primitive is a Integer' do
348
+ it 'returns same value if an integer' do
349
+ value = 24
350
+ @course.hours = value
351
+ @course['hours'].should equal(value)
352
+ end
353
+
354
+ it 'returns integer representation of a zero string integer' do
355
+ @course.hours = '0'
356
+ @course['hours'].should eql(0)
357
+ end
358
+
359
+ it 'returns integer representation of a positive string integer' do
360
+ @course.hours = '24'
361
+ @course['hours'].should eql(24)
362
+ end
363
+
364
+ it 'returns integer representation of a negative string integer' do
365
+ @course.hours = '-24'
366
+ @course['hours'].should eql(-24)
367
+ end
368
+
369
+ it 'returns integer representation of a zero string float' do
370
+ @course.hours = '0.0'
371
+ @course['hours'].should eql(0)
372
+ end
373
+
374
+ it 'returns integer representation of a positive string float' do
375
+ @course.hours = '24.35'
376
+ @course['hours'].should eql(24)
377
+ end
378
+
379
+ it 'returns integer representation of a negative string float' do
380
+ @course.hours = '-24.35'
381
+ @course['hours'].should eql(-24)
382
+ end
383
+
384
+ it 'returns integer representation of a zero string float, with no leading digits' do
385
+ @course.hours = '.0'
386
+ @course['hours'].should eql(0)
387
+ end
388
+
389
+ it 'returns integer representation of a positive string float, with no leading digits' do
390
+ @course.hours = '.41'
391
+ @course['hours'].should eql(0)
392
+ end
393
+
394
+ it 'returns integer representation of a zero float' do
395
+ @course.hours = 0.0
396
+ @course['hours'].should eql(0)
397
+ end
398
+
399
+ it 'returns integer representation of a positive float' do
400
+ @course.hours = 24.35
401
+ @course['hours'].should eql(24)
402
+ end
403
+
404
+ it 'returns integer representation of a negative float' do
405
+ @course.hours = -24.35
406
+ @course['hours'].should eql(-24)
407
+ end
408
+
409
+ it 'returns integer representation of a zero decimal' do
410
+ @course.hours = '0.0'
411
+ @course['hours'].should eql(0)
412
+ end
413
+
414
+ it 'returns integer representation of a positive decimal' do
415
+ @course.hours = '24.35'
416
+ @course['hours'].should eql(24)
417
+ end
418
+
419
+ it 'returns integer representation of a negative decimal' do
420
+ @course.hours = '-24.35'
421
+ @course['hours'].should eql(-24)
422
+ end
423
+
424
+ [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value|
425
+ it "does not typecast non-numeric value #{value.inspect}" do
426
+ @course.hours = value
427
+ @course['hours'].should equal(value)
428
+ end
429
+ end
430
+ end
431
+
432
+ describe 'when type primitive is a BigDecimal' do
433
+ it 'returns same value if a decimal' do
434
+ value = BigDecimal('24.0')
435
+ @course.profit = value
436
+ @course['profit'].should equal(value)
437
+ end
438
+
439
+ it 'returns decimal representation of a zero string integer' do
440
+ @course.profit = '0'
441
+ @course['profit'].should eql(BigDecimal('0.0'))
442
+ end
443
+
444
+ it 'returns decimal representation of a positive string integer' do
445
+ @course.profit = '24'
446
+ @course['profit'].should eql(BigDecimal('24.0'))
447
+ end
448
+
449
+ it 'returns decimal representation of a negative string integer' do
450
+ @course.profit = '-24'
451
+ @course['profit'].should eql(BigDecimal('-24.0'))
452
+ end
453
+
454
+ it 'returns decimal representation of a zero string float' do
455
+ @course.profit = '0.0'
456
+ @course['profit'].should eql(BigDecimal('0.0'))
457
+ end
458
+
459
+ it 'returns decimal representation of a positive string float' do
460
+ @course.profit = '24.35'
461
+ @course['profit'].should eql(BigDecimal('24.35'))
462
+ end
463
+
464
+ it 'returns decimal representation of a negative string float' do
465
+ @course.profit = '-24.35'
466
+ @course['profit'].should eql(BigDecimal('-24.35'))
467
+ end
468
+
469
+ it 'returns decimal representation of a zero string float, with no leading digits' do
470
+ @course.profit = '.0'
471
+ @course['profit'].should eql(BigDecimal('0.0'))
472
+ end
473
+
474
+ it 'returns decimal representation of a positive string float, with no leading digits' do
475
+ @course.profit = '.41'
476
+ @course['profit'].should eql(BigDecimal('0.41'))
477
+ end
478
+
479
+ it 'returns decimal representation of a zero integer' do
480
+ @course.profit = 0
481
+ @course['profit'].should eql(BigDecimal('0.0'))
482
+ end
483
+
484
+ it 'returns decimal representation of a positive integer' do
485
+ @course.profit = 24
486
+ @course['profit'].should eql(BigDecimal('24.0'))
487
+ end
488
+
489
+ it 'returns decimal representation of a negative integer' do
490
+ @course.profit = -24
491
+ @course['profit'].should eql(BigDecimal('-24.0'))
492
+ end
493
+
494
+ it 'returns decimal representation of a zero float' do
495
+ @course.profit = 0.0
496
+ @course['profit'].should eql(BigDecimal('0.0'))
497
+ end
498
+
499
+ it 'returns decimal representation of a positive float' do
500
+ @course.profit = 24.35
501
+ @course['profit'].should eql(BigDecimal('24.35'))
502
+ end
503
+
504
+ it 'returns decimal representation of a negative float' do
505
+ @course.profit = -24.35
506
+ @course['profit'].should eql(BigDecimal('-24.35'))
507
+ end
508
+
509
+ [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value|
510
+ it "does not typecast non-numeric value #{value.inspect}" do
511
+ @course.profit = value
512
+ @course['profit'].should equal(value)
513
+ end
514
+ end
515
+ end
516
+
517
+ describe 'when type primitive is a DateTime' do
518
+ describe 'and value given as a hash with keys like :year, :month, etc' do
519
+ it 'builds a DateTime instance from hash values' do
520
+ @course.updated_at = {
521
+ :year => '2006',
522
+ :month => '11',
523
+ :day => '23',
524
+ :hour => '12',
525
+ :min => '0',
526
+ :sec => '0'
527
+ }
528
+ result = @course['updated_at']
529
+
530
+ result.should be_kind_of(DateTime)
531
+ result.year.should eql(2006)
532
+ result.month.should eql(11)
533
+ result.day.should eql(23)
534
+ result.hour.should eql(12)
535
+ result.min.should eql(0)
536
+ result.sec.should eql(0)
537
+ end
538
+ end
539
+
540
+ describe 'and value is a string' do
541
+ it 'parses the string' do
542
+ @course.updated_at = 'Dec, 2006'
543
+ @course['updated_at'].month.should == 12
544
+ end
545
+ end
546
+
547
+ it 'does not typecast non-datetime values' do
548
+ @course.updated_at = 'not-datetime'
549
+ @course['updated_at'].should eql('not-datetime')
550
+ end
551
+ end
552
+
553
+ describe 'when type primitive is a Date' do
554
+ describe 'and value given as a hash with keys like :year, :month, etc' do
555
+ it 'builds a Date instance from hash values' do
556
+ @course.started_on = {
557
+ :year => '2007',
558
+ :month => '3',
559
+ :day => '25'
560
+ }
561
+ result = @course['started_on']
562
+
563
+ result.should be_kind_of(Date)
564
+ result.year.should eql(2007)
565
+ result.month.should eql(3)
566
+ result.day.should eql(25)
567
+ end
568
+ end
569
+
570
+ describe 'and value is a string' do
571
+ it 'parses the string' do
572
+ @course.started_on = 'Dec 20th, 2006'
573
+ @course.started_on.month.should == 12
574
+ @course.started_on.day.should == 20
575
+ @course.started_on.year.should == 2006
576
+ end
577
+ end
578
+
579
+ it 'does not typecast non-date values' do
580
+ @course.started_on = 'not-date'
581
+ @course['started_on'].should eql('not-date')
582
+ end
583
+ end
584
+
585
+ describe 'when type primitive is a Time' do
586
+ describe 'and value given as a hash with keys like :year, :month, etc' do
587
+ it 'builds a Time instance from hash values' do
588
+ @course.ends_at = {
589
+ :year => '2006',
590
+ :month => '11',
591
+ :day => '23',
592
+ :hour => '12',
593
+ :min => '0',
594
+ :sec => '0'
595
+ }
596
+ result = @course['ends_at']
597
+
598
+ result.should be_kind_of(Time)
599
+ result.year.should eql(2006)
600
+ result.month.should eql(11)
601
+ result.day.should eql(23)
602
+ result.hour.should eql(12)
603
+ result.min.should eql(0)
604
+ result.sec.should eql(0)
605
+ end
606
+ end
607
+
608
+ describe 'and value is a string' do
609
+ it 'parses the string' do
610
+ t = Time.now
611
+ @course.ends_at = t.strftime('%Y/%m/%d %H:%M:%S %z')
612
+ @course['ends_at'].year.should eql(t.year)
613
+ @course['ends_at'].month.should eql(t.month)
614
+ @course['ends_at'].day.should eql(t.day)
615
+ @course['ends_at'].hour.should eql(t.hour)
616
+ @course['ends_at'].min.should eql(t.min)
617
+ @course['ends_at'].sec.should eql(t.sec)
618
+ end
619
+ it 'parses the string without offset' do
620
+ t = Time.now
621
+ @course.ends_at = t.strftime("%Y-%m-%d %H:%M:%S")
622
+ @course['ends_at'].year.should eql(t.year)
623
+ @course['ends_at'].month.should eql(t.month)
624
+ @course['ends_at'].day.should eql(t.day)
625
+ @course['ends_at'].hour.should eql(t.hour)
626
+ @course['ends_at'].min.should eql(t.min)
627
+ @course['ends_at'].sec.should eql(t.sec)
628
+ end
629
+ end
630
+
631
+ it 'does not typecast non-time values' do
632
+ @course.ends_at = 'not-time'
633
+ @course['ends_at'].should eql('not-time')
634
+ end
635
+ end
636
+
637
+ describe 'when type primitive is a Class' do
638
+ it 'returns same value if a class' do
639
+ value = Course
640
+ @course.klass = value
641
+ @course['klass'].should equal(value)
642
+ end
643
+
644
+ it 'returns the class if found' do
645
+ @course.klass = 'Course'
646
+ @course['klass'].should eql(Course)
647
+ end
648
+
649
+ it 'does not typecast non-class values' do
650
+ @course.klass = 'NoClass'
651
+ @course['klass'].should eql('NoClass')
652
+ end
653
+ end
654
+
655
+ describe 'when type primitive is a Boolean' do
656
+
657
+ [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].each do |value|
658
+ it "returns true when value is #{value.inspect}" do
659
+ @course.active = value
660
+ @course['active'].should be_true
661
+ end
662
+ end
663
+
664
+ [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].each do |value|
665
+ it "returns false when value is #{value.inspect}" do
666
+ @course.active = value
667
+ @course['active'].should be_false
668
+ end
669
+ end
670
+
671
+ [ 'string', 2, 1.0, BigDecimal('1.0'), DateTime.now, Time.now, Date.today, Class, Object.new, ].each do |value|
672
+ it "does not typecast value #{value.inspect}" do
673
+ @course.active = value
674
+ @course['active'].should equal(value)
675
+ end
676
+ end
677
+
678
+ it "should respond to requests with ? modifier" do
679
+ @course.active = nil
680
+ @course.active?.should be_false
681
+ @course.active = false
682
+ @course.active?.should be_false
683
+ @course.active = true
684
+ @course.active?.should be_true
685
+ end
686
+
687
+ it "should respond to requests with ? modifier on TrueClass" do
688
+ @course.very_active = nil
689
+ @course.very_active?.should be_false
690
+ @course.very_active = false
691
+ @course.very_active?.should be_false
692
+ @course.very_active = true
693
+ @course.very_active?.should be_true
694
+ end
695
+ end
696
+
697
+ end
698
+ end
699
+
700
+ describe "properties of array of casted models" do
701
+
702
+ before(:each) do
703
+ @course = Course.new :title => 'Test Course'
704
+ end
705
+
706
+ it "should allow attribute to be set from an array of objects" do
707
+ @course.questions = [Question.new(:q => "works?"), Question.new(:q => "Meaning of Life?")]
708
+ @course.questions.length.should eql(2)
709
+ end
710
+
711
+ it "should allow attribute to be set from an array of hashes" do
712
+ @course.questions = [{:q => "works?"}, {:q => "Meaning of Life?"}]
713
+ @course.questions.length.should eql(2)
714
+ @course.questions.last.q.should eql("Meaning of Life?")
715
+ @course.questions.last.class.should eql(Question) # typecasting
716
+ end
717
+
718
+ it "should allow attribute to be set from hash with ordered keys and objects" do
719
+ @course.questions = { '0' => Question.new(:q => "Test1"), '1' => Question.new(:q => 'Test2') }
720
+ @course.questions.length.should eql(2)
721
+ @course.questions.last.q.should eql('Test2')
722
+ @course.questions.last.class.should eql(Question)
723
+ end
724
+
725
+ it "should allow attribute to be set from hash with ordered keys and sub-hashes" do
726
+ @course.questions = { '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} }
727
+ @course.questions.length.should eql(2)
728
+ @course.questions.last.q.should eql('Test2')
729
+ @course.questions.last.class.should eql(Question)
730
+ end
731
+
732
+ it "should allow attribute to be set from hash with ordered keys and HashWithIndifferentAccess" do
733
+ # This is similar to what you'd find in an HTML POST parameters
734
+ hash = HashWithIndifferentAccess.new({ '0' => {:q => "Test1"}, '1' => {:q => 'Test2'} })
735
+ @course.questions = hash
736
+ @course.questions.length.should eql(2)
737
+ @course.questions.last.q.should eql('Test2')
738
+ @course.questions.last.class.should eql(Question)
739
+ end
740
+
741
+
742
+ it "should raise an error if attempting to set single value for array type" do
743
+ lambda {
744
+ @course.questions = Question.new(:q => 'test1')
745
+ }.should raise_error
746
+ end
747
+
748
+
749
+ end
750
+
751
+ describe "a casted model retrieved from the database" do
752
+ before(:each) do
753
+ reset_test_db!
754
+ @cat = Cat.new(:name => 'Stimpy')
755
+ @cat.favorite_toy = CatToy.new(:name => 'Stinky')
756
+ @cat.toys << CatToy.new(:name => 'Feather')
757
+ @cat.toys << CatToy.new(:name => 'Mouse')
758
+ @cat.save
759
+ @cat = Cat.get(@cat.id)
760
+ end
761
+
762
+ describe "as a casted property" do
763
+ it "should already be casted_by its parent" do
764
+ @cat.favorite_toy.casted_by.should === @cat
765
+ end
766
+ end
767
+
768
+ describe "from a casted collection" do
769
+ it "should already be casted_by its parent" do
770
+ @cat.toys[0].casted_by.should === @cat
771
+ @cat.toys[1].casted_by.should === @cat
772
+ end
773
+ end
774
+ end
775
+
776
+ describe "Property Class" do
777
+
778
+ it "should provide name as string" do
779
+ property = CouchRest::Model::Property.new(:test, String)
780
+ property.name.should eql('test')
781
+ property.to_s.should eql('test')
782
+ end
783
+
784
+ it "should provide class from type" do
785
+ property = CouchRest::Model::Property.new(:test, String)
786
+ property.type_class.should eql(String)
787
+ end
788
+
789
+ it "should provide base class from type in array" do
790
+ property = CouchRest::Model::Property.new(:test, [String])
791
+ property.type_class.should eql(String)
792
+ end
793
+
794
+ it "should raise error if type as string requested" do
795
+ lambda {
796
+ property = CouchRest::Model::Property.new(:test, 'String')
797
+ }.should raise_error
798
+ end
799
+
800
+ it "should leave type nil and return class as nil also" do
801
+ property = CouchRest::Model::Property.new(:test, nil)
802
+ property.type.should be_nil
803
+ property.type_class.should be_nil
804
+ end
805
+
806
+ it "should convert empty type array to [Object]" do
807
+ property = CouchRest::Model::Property.new(:test, [])
808
+ property.type_class.should eql(Object)
809
+ end
810
+
811
+ it "should set init method option or leave as 'new'" do
812
+ # (bad example! Time already typecast)
813
+ property = CouchRest::Model::Property.new(:test, Time)
814
+ property.init_method.should eql('new')
815
+ property = CouchRest::Model::Property.new(:test, Time, :init_method => 'parse')
816
+ property.init_method.should eql('parse')
817
+ end
818
+
819
+ ## Property Casting method. More thoroughly tested earlier.
820
+
821
+ describe "casting" do
822
+ it "should cast a value" do
823
+ property = CouchRest::Model::Property.new(:test, Date)
824
+ parent = mock("FooObject")
825
+ property.cast(parent, "2010-06-16").should eql(Date.new(2010, 6, 16))
826
+ property.cast_value(parent, "2010-06-16").should eql(Date.new(2010, 6, 16))
827
+ end
828
+
829
+ it "should cast an array of values" do
830
+ property = CouchRest::Model::Property.new(:test, [Date])
831
+ parent = mock("FooObject")
832
+ property.cast(parent, ["2010-06-01", "2010-06-02"]).should eql([Date.new(2010, 6, 1), Date.new(2010, 6, 2)])
833
+ end
834
+
835
+ it "should set a CastedArray on array of Objects" do
836
+ property = CouchRest::Model::Property.new(:test, [Object])
837
+ parent = mock("FooObject")
838
+ property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
839
+ end
840
+
841
+ it "should not set a CastedArray on array of Strings" do
842
+ property = CouchRest::Model::Property.new(:test, [String])
843
+ parent = mock("FooObject")
844
+ property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should_not eql(CouchRest::Model::CastedArray)
845
+ end
846
+
847
+ it "should raise and error if value is array when type is not" do
848
+ property = CouchRest::Model::Property.new(:test, Date)
849
+ parent = mock("FooClass")
850
+ lambda {
851
+ cast = property.cast(parent, [Date.new(2010, 6, 1)])
852
+ }.should raise_error
853
+ end
854
+
855
+
856
+ it "should set parent as casted_by object in CastedArray" do
857
+ property = CouchRest::Model::Property.new(:test, [Object])
858
+ parent = mock("FooObject")
859
+ property.cast(parent, ["2010-06-01", "2010-06-02"]).casted_by.should eql(parent)
860
+ end
861
+
862
+ it "should set casted_by on new value" do
863
+ property = CouchRest::Model::Property.new(:test, CatToy)
864
+ parent = mock("CatObject")
865
+ cast = property.cast(parent, {:name => 'catnip'})
866
+ cast.casted_by.should eql(parent)
867
+ end
868
+
869
+ end
870
+
871
+ end