sam-dm-core 0.9.6

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