rpbertp13-dm-core 0.9.11.1

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 (131) hide show
  1. data/.autotest +26 -0
  2. data/.gitignore +18 -0
  3. data/CONTRIBUTING +51 -0
  4. data/FAQ +92 -0
  5. data/History.txt +52 -0
  6. data/MIT-LICENSE +22 -0
  7. data/Manifest.txt +130 -0
  8. data/QUICKLINKS +11 -0
  9. data/README.txt +143 -0
  10. data/Rakefile +32 -0
  11. data/SPECS +62 -0
  12. data/TODO +1 -0
  13. data/dm-core.gemspec +40 -0
  14. data/lib/dm-core.rb +217 -0
  15. data/lib/dm-core/adapters.rb +16 -0
  16. data/lib/dm-core/adapters/abstract_adapter.rb +209 -0
  17. data/lib/dm-core/adapters/data_objects_adapter.rb +716 -0
  18. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  19. data/lib/dm-core/adapters/mysql_adapter.rb +138 -0
  20. data/lib/dm-core/adapters/postgres_adapter.rb +189 -0
  21. data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
  22. data/lib/dm-core/associations.rb +207 -0
  23. data/lib/dm-core/associations/many_to_many.rb +147 -0
  24. data/lib/dm-core/associations/many_to_one.rb +107 -0
  25. data/lib/dm-core/associations/one_to_many.rb +315 -0
  26. data/lib/dm-core/associations/one_to_one.rb +61 -0
  27. data/lib/dm-core/associations/relationship.rb +221 -0
  28. data/lib/dm-core/associations/relationship_chain.rb +81 -0
  29. data/lib/dm-core/auto_migrations.rb +105 -0
  30. data/lib/dm-core/collection.rb +670 -0
  31. data/lib/dm-core/dependency_queue.rb +32 -0
  32. data/lib/dm-core/hook.rb +11 -0
  33. data/lib/dm-core/identity_map.rb +42 -0
  34. data/lib/dm-core/is.rb +16 -0
  35. data/lib/dm-core/logger.rb +232 -0
  36. data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
  37. data/lib/dm-core/migrator.rb +29 -0
  38. data/lib/dm-core/model.rb +526 -0
  39. data/lib/dm-core/naming_conventions.rb +84 -0
  40. data/lib/dm-core/property.rb +676 -0
  41. data/lib/dm-core/property_set.rb +169 -0
  42. data/lib/dm-core/query.rb +676 -0
  43. data/lib/dm-core/repository.rb +167 -0
  44. data/lib/dm-core/resource.rb +671 -0
  45. data/lib/dm-core/scope.rb +58 -0
  46. data/lib/dm-core/support.rb +7 -0
  47. data/lib/dm-core/support/array.rb +13 -0
  48. data/lib/dm-core/support/assertions.rb +8 -0
  49. data/lib/dm-core/support/errors.rb +23 -0
  50. data/lib/dm-core/support/kernel.rb +11 -0
  51. data/lib/dm-core/support/symbol.rb +41 -0
  52. data/lib/dm-core/transaction.rb +252 -0
  53. data/lib/dm-core/type.rb +160 -0
  54. data/lib/dm-core/type_map.rb +80 -0
  55. data/lib/dm-core/types.rb +19 -0
  56. data/lib/dm-core/types/boolean.rb +7 -0
  57. data/lib/dm-core/types/discriminator.rb +34 -0
  58. data/lib/dm-core/types/object.rb +24 -0
  59. data/lib/dm-core/types/paranoid_boolean.rb +34 -0
  60. data/lib/dm-core/types/paranoid_datetime.rb +33 -0
  61. data/lib/dm-core/types/serial.rb +9 -0
  62. data/lib/dm-core/types/text.rb +10 -0
  63. data/lib/dm-core/version.rb +3 -0
  64. data/script/all +4 -0
  65. data/script/performance.rb +282 -0
  66. data/script/profile.rb +87 -0
  67. data/spec/integration/association_spec.rb +1382 -0
  68. data/spec/integration/association_through_spec.rb +203 -0
  69. data/spec/integration/associations/many_to_many_spec.rb +449 -0
  70. data/spec/integration/associations/many_to_one_spec.rb +163 -0
  71. data/spec/integration/associations/one_to_many_spec.rb +188 -0
  72. data/spec/integration/auto_migrations_spec.rb +413 -0
  73. data/spec/integration/collection_spec.rb +1073 -0
  74. data/spec/integration/data_objects_adapter_spec.rb +32 -0
  75. data/spec/integration/dependency_queue_spec.rb +46 -0
  76. data/spec/integration/model_spec.rb +197 -0
  77. data/spec/integration/mysql_adapter_spec.rb +85 -0
  78. data/spec/integration/postgres_adapter_spec.rb +731 -0
  79. data/spec/integration/property_spec.rb +253 -0
  80. data/spec/integration/query_spec.rb +514 -0
  81. data/spec/integration/repository_spec.rb +61 -0
  82. data/spec/integration/resource_spec.rb +513 -0
  83. data/spec/integration/sqlite3_adapter_spec.rb +352 -0
  84. data/spec/integration/sti_spec.rb +273 -0
  85. data/spec/integration/strategic_eager_loading_spec.rb +156 -0
  86. data/spec/integration/transaction_spec.rb +60 -0
  87. data/spec/integration/type_spec.rb +275 -0
  88. data/spec/lib/logging_helper.rb +18 -0
  89. data/spec/lib/mock_adapter.rb +27 -0
  90. data/spec/lib/model_loader.rb +100 -0
  91. data/spec/lib/publicize_methods.rb +28 -0
  92. data/spec/models/content.rb +16 -0
  93. data/spec/models/vehicles.rb +34 -0
  94. data/spec/models/zoo.rb +48 -0
  95. data/spec/spec.opts +3 -0
  96. data/spec/spec_helper.rb +91 -0
  97. data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
  98. data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
  99. data/spec/unit/adapters/data_objects_adapter_spec.rb +632 -0
  100. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  101. data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
  102. data/spec/unit/associations/many_to_many_spec.rb +32 -0
  103. data/spec/unit/associations/many_to_one_spec.rb +159 -0
  104. data/spec/unit/associations/one_to_many_spec.rb +393 -0
  105. data/spec/unit/associations/one_to_one_spec.rb +7 -0
  106. data/spec/unit/associations/relationship_spec.rb +71 -0
  107. data/spec/unit/associations_spec.rb +242 -0
  108. data/spec/unit/auto_migrations_spec.rb +111 -0
  109. data/spec/unit/collection_spec.rb +182 -0
  110. data/spec/unit/data_mapper_spec.rb +35 -0
  111. data/spec/unit/identity_map_spec.rb +126 -0
  112. data/spec/unit/is_spec.rb +80 -0
  113. data/spec/unit/migrator_spec.rb +33 -0
  114. data/spec/unit/model_spec.rb +321 -0
  115. data/spec/unit/naming_conventions_spec.rb +36 -0
  116. data/spec/unit/property_set_spec.rb +90 -0
  117. data/spec/unit/property_spec.rb +753 -0
  118. data/spec/unit/query_spec.rb +571 -0
  119. data/spec/unit/repository_spec.rb +93 -0
  120. data/spec/unit/resource_spec.rb +649 -0
  121. data/spec/unit/scope_spec.rb +142 -0
  122. data/spec/unit/transaction_spec.rb +469 -0
  123. data/spec/unit/type_map_spec.rb +114 -0
  124. data/spec/unit/type_spec.rb +119 -0
  125. data/tasks/ci.rb +36 -0
  126. data/tasks/dm.rb +63 -0
  127. data/tasks/doc.rb +20 -0
  128. data/tasks/gemspec.rb +23 -0
  129. data/tasks/hoe.rb +46 -0
  130. data/tasks/install.rb +20 -0
  131. metadata +215 -0
@@ -0,0 +1,93 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe DataMapper::Repository do
4
+ before do
5
+ @adapter = mock('adapter')
6
+ @identity_map = mock('identity map', :[]= => nil)
7
+ @identity_maps = mock('identity maps', :[] => @identity_map)
8
+
9
+ @repository = repository(:mock)
10
+ @repository.stub!(:adapter).and_return(@adapter)
11
+
12
+ # TODO: stub out other external dependencies in repository
13
+ end
14
+
15
+ describe "managing transactions" do
16
+ it "should create a new Transaction with itself as argument when #transaction is called" do
17
+ transaction = mock('transaction')
18
+ DataMapper::Transaction.should_receive(:new).with(@repository).and_return(transaction)
19
+ @repository.transaction.should == transaction
20
+ end
21
+ end
22
+
23
+ it 'should provide .storage_exists?' do
24
+ @repository.should respond_to(:storage_exists?)
25
+ end
26
+
27
+ it '.storage_exists? should whether or not the storage exists' do
28
+ @adapter.should_receive(:storage_exists?).with(:vegetable).and_return(true)
29
+
30
+ @repository.storage_exists?(:vegetable).should == true
31
+ end
32
+
33
+ it "should provide persistance methods" do
34
+ @repository.should respond_to(:create)
35
+ @repository.should respond_to(:read_many)
36
+ @repository.should respond_to(:read_one)
37
+ @repository.should respond_to(:update)
38
+ @repository.should respond_to(:delete)
39
+ end
40
+
41
+ it "should be reused in inner scope" do
42
+ DataMapper.repository(:default) do |outer_repos|
43
+ DataMapper.repository(:default) do |inner_repos|
44
+ outer_repos.object_id.should == inner_repos.object_id
45
+ end
46
+ end
47
+ end
48
+
49
+ it 'should provide default_name' do
50
+ DataMapper::Repository.should respond_to(:default_name)
51
+ end
52
+
53
+ it 'should return :default for default_name' do
54
+ DataMapper::Repository.default_name.should == :default
55
+ end
56
+
57
+ describe "#migrate!" do
58
+ it "should call DataMapper::Migrator.migrate with itself as the repository argument" do
59
+ DataMapper::Migrator.should_receive(:migrate).with(@repository.name)
60
+
61
+ @repository.migrate!
62
+ end
63
+ end
64
+
65
+ describe "#auto_migrate!" do
66
+ it "should call DataMapper::AutoMigrator.auto_migrate with itself as the repository argument" do
67
+ DataMapper::AutoMigrator.should_receive(:auto_migrate).with(@repository.name)
68
+
69
+ @repository.auto_migrate!
70
+ end
71
+ end
72
+
73
+ describe "#auto_upgrade!" do
74
+ it "should call DataMapper::AutoMigrator.auto_upgrade with itself as the repository argument" do
75
+ DataMapper::AutoMigrator.should_receive(:auto_upgrade).with(@repository.name)
76
+
77
+ @repository.auto_upgrade!
78
+ end
79
+ end
80
+
81
+ describe "#map" do
82
+ it "should call type_map.map with the arguments" do
83
+ type_map = mock('type map')
84
+
85
+ @adapter.class.should_receive(:type_map).and_return(type_map)
86
+ DataMapper::TypeMap.should_receive(:new).with(type_map).and_return(type_map)
87
+
88
+ type_map.should_receive(:map).with(:type, :arg)
89
+
90
+ @repository.map(:type, :arg)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,649 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe DataMapper::Resource do
4
+
5
+ load_models_for_metaphor :zoo
6
+
7
+ describe '#attributes' do
8
+ it 'should return a hash of attribute-names and values' do
9
+ zoo = Zoo.new
10
+ zoo.name = "San Francisco"
11
+ zoo.description = "This is a pretty awesome zoo"
12
+ zoo.attributes.should == {
13
+ :name => "San Francisco", :description => "This is a pretty awesome zoo",
14
+ :id => nil, :inception => nil, :open => false, :size => nil, :mission => nil
15
+ }
16
+ end
17
+
18
+ it "should return a hash with all nil values if the instance is new and has no default values" do
19
+ Species.new.attributes.should == { :id => nil, :name => nil }
20
+ end
21
+
22
+ it 'should not include private attributes' do
23
+ Species.new.attributes.should == { :id => nil, :name => nil }
24
+ end
25
+ end
26
+
27
+ describe "#attributes=" do
28
+ before(:each) do
29
+ @zoo = Zoo.new(:name => "San Francisco", :size => 10)
30
+ @zoo.attributes = {:size => 12 }
31
+ end
32
+ it "should change a public property" do
33
+ @zoo.size.should == 12
34
+ end
35
+ it "should raise when attempting to change a property with a non-public writer" do
36
+ lambda { @zoo.attributes = {:mission => "Just keep some odd
37
+ critters, y'know?" } }.should raise_error
38
+ end
39
+ end
40
+
41
+ # ---------- REPOSITORY WRITE METHODS ---------------
42
+
43
+ describe '#save' do
44
+
45
+ describe 'with a new resource' do
46
+ it 'should set defaults before create'
47
+ it 'should create when dirty'
48
+ it 'should create when non-dirty, and it has a serial key'
49
+ end
50
+
51
+ describe 'with an existing resource' do
52
+ it 'should update'
53
+ end
54
+
55
+ end
56
+ end
57
+
58
+ # ---------- Old specs... BOOOOOOOOOO ---------------
59
+
60
+ describe DataMapper::Resource do
61
+ before(:each) do
62
+ Object.send(:remove_const, :Planet) if defined?(Planet)
63
+ class ::Planet
64
+ include DataMapper::Resource
65
+
66
+ storage_names[:legacy] = "dying_planets"
67
+
68
+ property :id, Integer, :key => true
69
+ property :name, String
70
+ property :age, Integer
71
+ property :core, String, :accessor => :private
72
+ property :type, Discriminator
73
+ property :data, Object, :track => :get
74
+
75
+ repository(:legacy) do
76
+ property :cowabunga, String
77
+ end
78
+
79
+ def age
80
+ attribute_get(:age)
81
+ end
82
+
83
+ def to_s
84
+ name
85
+ end
86
+ end
87
+
88
+ Object.send(:remove_const, :Phone) if defined?(Phone)
89
+ class ::Phone
90
+ include DataMapper::Resource
91
+
92
+ property :name, String, :key => true
93
+ property :awesomeness, Integer
94
+ end
95
+
96
+ Object.send(:remove_const, :Fruit) if defined?(Fruit)
97
+ class ::Fruit
98
+ include DataMapper::Resource
99
+
100
+ property :id, Integer, :key => true
101
+ property :name, String
102
+ end
103
+
104
+ Object.send(:remove_const, :Grain) if defined?(Grain)
105
+ class ::Grain
106
+ include DataMapper::Resource
107
+
108
+ property :id, Serial
109
+ property :name, String, :default => 'wheat'
110
+ end
111
+
112
+ Object.send(:remove_const, :Vegetable) if defined?(Vegetable)
113
+ class ::Vegetable
114
+ include DataMapper::Resource
115
+
116
+ property :id, Serial
117
+ property :name, String
118
+ end
119
+
120
+ Object.send(:remove_const, :Banana) if defined?(Banana)
121
+ class ::Banana < Fruit
122
+ property :type, Discriminator
123
+ end
124
+
125
+ Object.send(:remove_const, :Cyclist) if defined?(Cyclist)
126
+ class ::Cyclist
127
+ include DataMapper::Resource
128
+ property :id, Serial
129
+ property :victories, Integer
130
+ end
131
+
132
+ Fruit.auto_migrate!
133
+ Planet.auto_migrate!
134
+ Cyclist.auto_migrate!
135
+ end
136
+
137
+ it 'should provide #save' do
138
+ Planet.new.should respond_to(:save)
139
+ end
140
+
141
+ describe '#save' do
142
+ before(:each) do
143
+ @adapter = repository(:default).adapter
144
+ end
145
+
146
+ describe 'with a new resource' do
147
+ it 'should set defaults before create' do
148
+ resource = Grain.new
149
+
150
+ resource.should_not be_dirty
151
+ resource.should be_new_record
152
+ resource.instance_variable_get('@name').should be_nil
153
+
154
+ @adapter.should_receive(:create).with([ resource ]).and_return(1)
155
+
156
+ resource.save.should be_true
157
+
158
+ resource.instance_variable_get('@name').should == 'wheat'
159
+ end
160
+
161
+ it 'should create when dirty' do
162
+ resource = Vegetable.new(:id => 1, :name => 'Potato')
163
+
164
+ resource.should be_dirty
165
+ resource.should be_new_record
166
+
167
+ @adapter.should_receive(:create).with([ resource ]).and_return(1)
168
+
169
+ resource.save.should be_true
170
+ end
171
+
172
+ it 'should create when non-dirty, and it has a serial key' do
173
+ resource = Vegetable.new
174
+
175
+ resource.should_not be_dirty
176
+ resource.should be_new_record
177
+ resource.model.key.any? { |p| p.serial? }.should be_true
178
+
179
+ @adapter.should_receive(:create).with([ resource ]).and_return(1)
180
+
181
+ resource.save.should be_true
182
+ end
183
+
184
+ it 'should not create when non-dirty, and is has a non-serial key' do
185
+ resource = Fruit.new
186
+
187
+ resource.should_not be_dirty
188
+ resource.should be_new_record
189
+ resource.model.key.any? { |p| p.serial? }.should be_false
190
+
191
+ resource.save.should be_false
192
+ end
193
+
194
+ it 'should return true even if the object is not dirty' do
195
+ resource = Cyclist.new
196
+ resource.victories = "0 victories"
197
+ resource.save.should be_true
198
+
199
+ resource.should_not be_dirty
200
+ resource.should_not be_new_record
201
+ resource.save.should be_true
202
+ end
203
+
204
+ describe 'for integer fields' do
205
+
206
+ it "should save strings without digits as nil" do
207
+ resource = Cyclist.new
208
+ resource.victories = "none"
209
+ resource.save.should be_true
210
+ resource.victories.should be_nil
211
+ end
212
+
213
+ it "should save strings beginning with non-digits as nil" do
214
+ resource = Cyclist.new
215
+ resource.victories = "almost 5"
216
+ resource.save.should be_true
217
+ resource.victories.should be_nil
218
+ end
219
+
220
+ it 'should save strings beginning with negative numbers as that number' do
221
+ resource = Cyclist.new
222
+ resource.victories = "-4 victories"
223
+ resource.save.should be_true
224
+ resource.victories.should == -4
225
+ end
226
+
227
+ it 'should save strings beginning with 0 as 0' do
228
+ resource = Cyclist.new
229
+ resource.victories = "0 victories"
230
+ resource.save.should be_true
231
+ resource.victories.should == 0
232
+ end
233
+
234
+ it 'should save strings beginning with positive numbers as that number' do
235
+ resource = Cyclist.new
236
+ resource.victories = "23 victories"
237
+ resource.save.should be_true
238
+ resource.victories.should == 23
239
+ end
240
+
241
+ end
242
+
243
+ end
244
+
245
+ describe 'with an existing resource' do
246
+ it 'should update' do
247
+ resource = Vegetable.new(:name => 'Potato')
248
+ resource.instance_variable_set('@new_record', false)
249
+
250
+ resource.should be_dirty
251
+ resource.should_not be_new_record
252
+
253
+ @adapter.should_receive(:update).with(resource.dirty_attributes, resource.to_query).and_return(1)
254
+
255
+ resource.save.should be_true
256
+ end
257
+ end
258
+ end
259
+
260
+ it "should be able to overwrite to_s" do
261
+ Planet.new(:name => 'Mercury').to_s.should == 'Mercury'
262
+ end
263
+
264
+ describe "storage names" do
265
+ it "should use its class name by default" do
266
+ Planet.storage_name.should == "planets"
267
+ end
268
+
269
+ it "should allow changing using #default_storage_name" do
270
+ Planet.class_eval <<-EOF.margin
271
+ @storage_names.clear
272
+ def self.default_storage_name
273
+ "Superplanet"
274
+ end
275
+ EOF
276
+
277
+ Planet.storage_name.should == "superplanets"
278
+ Planet.class_eval <<-EOF.margin
279
+ @storage_names.clear
280
+ def self.default_storage_name
281
+ self.name
282
+ end
283
+ EOF
284
+ end
285
+ end
286
+
287
+ it "should require a key" do
288
+ lambda do
289
+ DataMapper::Model.new("stuff") do
290
+ property :name, String
291
+ end.new
292
+ end.should raise_error(DataMapper::IncompleteResourceError)
293
+ end
294
+
295
+ it "should hold repository-specific properties" do
296
+ Planet.properties(:legacy).should have_property(:cowabunga)
297
+ Planet.properties.should_not have_property(:cowabunga)
298
+ end
299
+
300
+ it "should track the classes that include it" do
301
+ DataMapper::Resource.descendants.clear
302
+ klass = Class.new { include DataMapper::Resource }
303
+ DataMapper::Resource.descendants.should == Set.new([klass])
304
+ end
305
+
306
+ it "should return an instance of the created object" do
307
+ Planet.create(:name => 'Venus', :age => 1_000_000, :id => 42).should be_a_kind_of(Planet)
308
+ end
309
+
310
+ it 'should provide persistance methods' do
311
+ planet = Planet.new
312
+ planet.should respond_to(:new_record?)
313
+ planet.should respond_to(:save)
314
+ planet.should respond_to(:destroy)
315
+ end
316
+
317
+ it "should have attributes" do
318
+ attributes = { :name => 'Jupiter', :age => 1_000_000, :id => 42, :type => Planet, :data => nil }
319
+ jupiter = Planet.new(attributes)
320
+ jupiter.attributes.should == attributes
321
+ end
322
+
323
+ it "should be able to set attributes" do
324
+ attributes = { :name => 'Jupiter', :age => 1_000_000, :id => 42, :type => Planet, :data => nil }
325
+ jupiter = Planet.new(attributes)
326
+ jupiter.attributes.should == attributes
327
+
328
+ new_attributes = attributes.merge( :age => 2_500_000 )
329
+ jupiter.attributes = new_attributes
330
+ jupiter.attributes.should == new_attributes
331
+ end
332
+
333
+ it "should be able to set attributes using update_attributes" do
334
+ attributes = { :name => 'Jupiter', :age => 1_000_000, :id => 42, :type => Planet, :data => nil }
335
+ jupiter = Planet.new(attributes)
336
+ jupiter.attributes.should == attributes
337
+
338
+ new_age = { :age => 3_700_000 }
339
+ jupiter.update_attributes(new_age).should be_true
340
+ jupiter.age.should == 3_700_000
341
+ jupiter.attributes.should == attributes.merge(new_age)
342
+ end
343
+
344
+ # Illustrates a possible controller situation, where an expected params
345
+ # key does not exist.
346
+ it "update_attributes(nil) should raise an exception" do
347
+ hincapie = Cyclist.new
348
+ params = {}
349
+ lambda {
350
+ hincapie.update_attributes(params[:does_not_exist])
351
+ }.should raise_error(ArgumentError)
352
+ end
353
+
354
+ it "update_attributes(:not_a_hash) should raise an exception" do
355
+ hincapie = Cyclist.new
356
+ lambda {
357
+ hincapie.update_attributes(:not_a_hash).should be_false
358
+ }.should raise_error(ArgumentError)
359
+ end
360
+
361
+ # :core is a private accessor so Ruby should raise NameError
362
+ it "should not be able to set private attributes" do
363
+ lambda {
364
+ jupiter = Planet.new({ :core => "Molten Metal" })
365
+ }.should raise_error(ArgumentError)
366
+ end
367
+
368
+ it "should not mark attributes dirty if they are similar after update" do
369
+ jupiter = Planet.new(:name => 'Jupiter', :age => 1_000_000, :id => 42, :data => { :a => "Yeah!" })
370
+ jupiter.save.should be_true
371
+
372
+ # discriminator will be set automatically
373
+ jupiter.type.should == Planet
374
+
375
+ jupiter.attributes = { :name => 'Jupiter', :age => 1_000_000, :data => { :a => "Yeah!" } }
376
+
377
+ jupiter.attribute_dirty?(:name).should be_false
378
+ jupiter.attribute_dirty?(:age).should be_false
379
+ jupiter.attribute_dirty?(:core).should be_false
380
+ jupiter.attribute_dirty?(:data).should be_false
381
+
382
+ jupiter.dirty?.should be_false
383
+ end
384
+
385
+ it "should not mark attributes dirty if they are similar after typecasting" do
386
+ jupiter = Planet.new(:name => 'Jupiter', :age => 1_000_000, :id => 42, :type => Planet)
387
+ jupiter.save.should be_true
388
+ jupiter.dirty?.should be_false
389
+
390
+ jupiter.age = '1_000_000'
391
+ jupiter.attribute_dirty?(:age).should be_false
392
+ jupiter.dirty?.should be_false
393
+ end
394
+
395
+ it "should track attributes" do
396
+
397
+ # So attribute tracking is a feature of the Resource,
398
+ # not the Property. Properties are class-level declarations.
399
+ # Instance-level operations like this happen in Resource with methods
400
+ # and ivars it sets up. Like a @dirty_attributes Array for example to
401
+ # track dirty attributes.
402
+
403
+ mars = Planet.new :name => 'Mars'
404
+ # #attribute_loaded? and #attribute_dirty? are a bit verbose,
405
+ # but I like the consistency and grouping of the methods.
406
+
407
+ # initialize-set values are dirty as well. DM sets ivars
408
+ # directly when materializing, so an ivar won't exist
409
+ # if the value wasn't loaded by DM initially. Touching that
410
+ # ivar at all will declare it, so at that point it's loaded.
411
+ # This means #attribute_loaded?'s implementation could be very
412
+ # similar (if not identical) to:
413
+ # def attribute_loaded?(name)
414
+ # instance_variable_defined?("@#{name}")
415
+ # end
416
+ mars.attribute_loaded?(:name).should be_true
417
+ mars.attribute_dirty?(:id).should be_false
418
+ mars.attribute_dirty?(:name).should be_true
419
+ mars.attribute_loaded?(:age).should be_false
420
+ mars.attribute_dirty?(:data).should be_false
421
+
422
+ mars.age.should be_nil
423
+
424
+ # So accessing a value should ensure it's loaded.
425
+ # XXX: why? if the @ivar isn't set, which it wouldn't be in this
426
+ # case because mars is a new_record?, then perhaps it should return
427
+ # false
428
+ # mars.attribute_loaded?(:age).should be_true
429
+
430
+ # A value should be able to be both loaded and nil.
431
+ mars.age.should be_nil
432
+
433
+ # Unless you call #[]= it's not dirty.
434
+ mars.attribute_dirty?(:age).should be_false
435
+
436
+ mars.age = 30
437
+ mars.data = { :a => "Yeah!" }
438
+
439
+ # Obviously. :-)
440
+ mars.attribute_dirty?(:age).should be_true
441
+ mars.attribute_dirty?(:data).should be_true
442
+ end
443
+
444
+ it "should mark the key as dirty, if it is a natural key and has been set" do
445
+ phone = Phone.new
446
+ phone.name = 'iPhone'
447
+ phone.attribute_dirty?(:name).should be_true
448
+ end
449
+
450
+ it 'should return the dirty attributes' do
451
+ pluto = Planet.new(:name => 'Pluto', :age => 500_000)
452
+ pluto.attribute_dirty?(:name).should be_true
453
+ pluto.attribute_dirty?(:age).should be_true
454
+ end
455
+
456
+ it 'should overwite old dirty attributes with new ones' do
457
+ pluto = Planet.new(:name => 'Pluto', :age => 500_000)
458
+ pluto.dirty_attributes.size.should == 2
459
+ pluto.attribute_dirty?(:name).should be_true
460
+ pluto.attribute_dirty?(:age).should be_true
461
+ pluto.name = "pluto"
462
+ pluto.dirty_attributes.size.should == 2
463
+ pluto.attribute_dirty?(:name).should be_true
464
+ pluto.attribute_dirty?(:age).should be_true
465
+ end
466
+
467
+ it 'should provide a key' do
468
+ Planet.new.should respond_to(:key)
469
+ end
470
+
471
+ it 'should store and retrieve default values' do
472
+ Planet.property(:satellite_count, Integer, :default => 0)
473
+ # stupid example but it's reliable and works
474
+ Planet.property(:orbit_period, Float, :default => lambda { |r,p| p.name.to_s.length })
475
+ earth = Planet.new(:name => 'Earth')
476
+ earth.satellite_count.should == 0
477
+ earth.orbit_period.should == 12
478
+ earth.satellite_count = 2
479
+ earth.satellite_count.should == 2
480
+ earth.orbit_period = 365.26
481
+ earth.orbit_period.should == 365.26
482
+ end
483
+
484
+ describe "#reload_attributes" do
485
+ it 'should call collection.reload if not a new record' do
486
+ planet = Planet.new(:name => 'Omicron Persei VIII')
487
+ planet.stub!(:new_record?).and_return(false)
488
+
489
+ collection = mock('collection')
490
+ collection.should_receive(:reload).with(:fields => [:name]).once
491
+
492
+ planet.stub!(:collection).and_return(collection)
493
+ planet.reload_attributes(:name)
494
+ end
495
+
496
+ it 'should not call collection.reload if no attributes are provided to reload' do
497
+ planet = Planet.new(:name => 'Omicron Persei VIII')
498
+ planet.stub!(:new_record?).and_return(false)
499
+
500
+ collection = mock('collection')
501
+ collection.should_not_receive(:reload)
502
+
503
+ planet.stub!(:collection).and_return(collection)
504
+ planet.reload_attributes
505
+ end
506
+
507
+ it 'should not call collection.reload if the record is new' do
508
+ lambda {
509
+ Planet.new(:name => 'Omicron Persei VIII').reload_attributes(:name)
510
+ }.should_not raise_error
511
+
512
+ planet = Planet.new(:name => 'Omicron Persei VIII')
513
+ planet.should_not_receive(:collection)
514
+ planet.reload_attributes(:name)
515
+ end
516
+ end
517
+
518
+ describe '#reload' do
519
+ it 'should call #reload_attributes with the currently loaded attributes' do
520
+ planet = Planet.new(:name => 'Omicron Persei VIII', :age => 1)
521
+ planet.stub!(:new_record?).and_return(false)
522
+
523
+ planet.should_receive(:reload_attributes).with(:name, :age).once
524
+
525
+ planet.reload
526
+ end
527
+
528
+ it 'should call #reload on the parent and child associations' do
529
+ planet = Planet.new(:name => 'Omicron Persei VIII', :age => 1)
530
+ planet.stub!(:new_record?).and_return(false)
531
+
532
+ child_association = mock('child assoc')
533
+ child_association.should_receive(:reload).once.and_return(true)
534
+
535
+ parent_association = mock('parent assoc')
536
+ parent_association.should_receive(:reload).once.and_return(true)
537
+
538
+ planet.stub!(:child_associations).and_return([child_association])
539
+ planet.stub!(:parent_associations).and_return([parent_association])
540
+ planet.stub!(:reload_attributes).and_return(planet)
541
+
542
+ planet.reload
543
+ end
544
+
545
+ it 'should not do anything if the record is new' do
546
+ planet = Planet.new(:name => 'Omicron Persei VIII', :age => 1)
547
+ planet.should_not_receive(:reload_attributes)
548
+ planet.reload
549
+ end
550
+ end
551
+
552
+ describe 'when retrieving by key' do
553
+ it 'should return the corresponding object' do
554
+ m = mock("planet")
555
+ Planet.should_receive(:get).with(1).and_return(m)
556
+
557
+ Planet.get!(1).should == m
558
+ end
559
+
560
+ it 'should raise an error if not found' do
561
+ Planet.should_receive(:get).and_return(nil)
562
+
563
+ lambda do
564
+ Planet.get!(1)
565
+ end.should raise_error(DataMapper::ObjectNotFoundError)
566
+ end
567
+ end
568
+
569
+ describe "inheritance" do
570
+ before(:all) do
571
+ class ::Media
572
+ include DataMapper::Resource
573
+
574
+ storage_names[:default] = 'media'
575
+ storage_names[:west_coast] = 'm3d1a'
576
+
577
+ property :name, String, :key => true
578
+ end
579
+
580
+ class ::NewsPaper < Media
581
+
582
+ storage_names[:east_coast] = 'mother'
583
+
584
+ property :rating, Integer
585
+ end
586
+ end
587
+
588
+ it 'should inherit storage_names' do
589
+ NewsPaper.storage_name(:default).should == 'media'
590
+ NewsPaper.storage_name(:west_coast).should == 'm3d1a'
591
+ NewsPaper.storage_name(:east_coast).should == 'mother'
592
+ Media.storage_name(:east_coast).should == 'medium'
593
+ end
594
+
595
+ it 'should inherit properties' do
596
+ Media.properties.should have(1).entries
597
+ NewsPaper.properties.should have(2).entries
598
+ end
599
+ end
600
+
601
+ describe "Single-table Inheritance" do
602
+ before(:all) do
603
+ class ::Plant
604
+ include DataMapper::Resource
605
+
606
+ property :id, Integer, :key => true
607
+ property :length, Integer
608
+
609
+ def calculate(int)
610
+ int ** 2
611
+ end
612
+
613
+ def length=(len)
614
+ attribute_set(:length, calculate(len))
615
+ end
616
+ end
617
+
618
+ class ::HousePlant < Plant
619
+ def calculate(int)
620
+ int ** 3
621
+ end
622
+ end
623
+
624
+ class ::PoisonIvy < Plant
625
+ def length=(len)
626
+ attribute_set(:length, len - 1)
627
+ end
628
+ end
629
+ end
630
+
631
+ it "should be able to overwrite getters" do
632
+ @p = Plant.new
633
+ @p.length = 3
634
+ @p.length.should == 9
635
+ end
636
+
637
+ it "should pick overwritten methods" do
638
+ @hp = HousePlant.new
639
+ @hp.length = 3
640
+ @hp.length.should == 27
641
+ end
642
+
643
+ it "should pick overwritten setters" do
644
+ @pi = PoisonIvy.new
645
+ @pi.length = 3
646
+ @pi.length.should == 2
647
+ end
648
+ end
649
+ end