datamapper-dm-core 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
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 +41 -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 +30 -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 +136 -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 +229 -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 +267 -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 +75 -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 +493 -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