couchrest_model-radiant 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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