sam-dm-core 0.9.6

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 (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