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.
- data/.autotest +26 -0
- data/CONTRIBUTING +51 -0
- data/FAQ +92 -0
- data/History.txt +145 -0
- data/MIT-LICENSE +22 -0
- data/Manifest.txt +125 -0
- data/QUICKLINKS +12 -0
- data/README.txt +143 -0
- data/Rakefile +30 -0
- data/SPECS +63 -0
- data/TODO +1 -0
- data/lib/dm-core.rb +224 -0
- data/lib/dm-core/adapters.rb +4 -0
- data/lib/dm-core/adapters/abstract_adapter.rb +202 -0
- data/lib/dm-core/adapters/data_objects_adapter.rb +707 -0
- data/lib/dm-core/adapters/mysql_adapter.rb +136 -0
- data/lib/dm-core/adapters/postgres_adapter.rb +188 -0
- data/lib/dm-core/adapters/sqlite3_adapter.rb +105 -0
- data/lib/dm-core/associations.rb +199 -0
- data/lib/dm-core/associations/many_to_many.rb +147 -0
- data/lib/dm-core/associations/many_to_one.rb +107 -0
- data/lib/dm-core/associations/one_to_many.rb +309 -0
- data/lib/dm-core/associations/one_to_one.rb +61 -0
- data/lib/dm-core/associations/relationship.rb +218 -0
- data/lib/dm-core/associations/relationship_chain.rb +81 -0
- data/lib/dm-core/auto_migrations.rb +113 -0
- data/lib/dm-core/collection.rb +638 -0
- data/lib/dm-core/dependency_queue.rb +31 -0
- data/lib/dm-core/hook.rb +11 -0
- data/lib/dm-core/identity_map.rb +45 -0
- data/lib/dm-core/is.rb +16 -0
- data/lib/dm-core/logger.rb +232 -0
- data/lib/dm-core/migrations/destructive_migrations.rb +17 -0
- data/lib/dm-core/migrator.rb +29 -0
- data/lib/dm-core/model.rb +471 -0
- data/lib/dm-core/naming_conventions.rb +84 -0
- data/lib/dm-core/property.rb +673 -0
- data/lib/dm-core/property_set.rb +162 -0
- data/lib/dm-core/query.rb +625 -0
- data/lib/dm-core/repository.rb +159 -0
- data/lib/dm-core/resource.rb +637 -0
- data/lib/dm-core/scope.rb +58 -0
- data/lib/dm-core/support.rb +7 -0
- data/lib/dm-core/support/array.rb +13 -0
- data/lib/dm-core/support/assertions.rb +8 -0
- data/lib/dm-core/support/errors.rb +23 -0
- data/lib/dm-core/support/kernel.rb +7 -0
- data/lib/dm-core/support/symbol.rb +41 -0
- data/lib/dm-core/transaction.rb +267 -0
- data/lib/dm-core/type.rb +160 -0
- data/lib/dm-core/type_map.rb +80 -0
- data/lib/dm-core/types.rb +19 -0
- data/lib/dm-core/types/boolean.rb +7 -0
- data/lib/dm-core/types/discriminator.rb +34 -0
- data/lib/dm-core/types/object.rb +24 -0
- data/lib/dm-core/types/paranoid_boolean.rb +34 -0
- data/lib/dm-core/types/paranoid_datetime.rb +33 -0
- data/lib/dm-core/types/serial.rb +9 -0
- data/lib/dm-core/types/text.rb +10 -0
- data/lib/dm-core/version.rb +3 -0
- data/script/all +5 -0
- data/script/performance.rb +203 -0
- data/script/profile.rb +87 -0
- data/spec/integration/association_spec.rb +1371 -0
- data/spec/integration/association_through_spec.rb +203 -0
- data/spec/integration/associations/many_to_many_spec.rb +449 -0
- data/spec/integration/associations/many_to_one_spec.rb +163 -0
- data/spec/integration/associations/one_to_many_spec.rb +151 -0
- data/spec/integration/auto_migrations_spec.rb +398 -0
- data/spec/integration/collection_spec.rb +1069 -0
- data/spec/integration/data_objects_adapter_spec.rb +32 -0
- data/spec/integration/dependency_queue_spec.rb +58 -0
- data/spec/integration/model_spec.rb +127 -0
- data/spec/integration/mysql_adapter_spec.rb +85 -0
- data/spec/integration/postgres_adapter_spec.rb +731 -0
- data/spec/integration/property_spec.rb +233 -0
- data/spec/integration/query_spec.rb +506 -0
- data/spec/integration/repository_spec.rb +57 -0
- data/spec/integration/resource_spec.rb +475 -0
- data/spec/integration/sqlite3_adapter_spec.rb +352 -0
- data/spec/integration/sti_spec.rb +208 -0
- data/spec/integration/strategic_eager_loading_spec.rb +138 -0
- data/spec/integration/transaction_spec.rb +75 -0
- data/spec/integration/type_spec.rb +271 -0
- data/spec/lib/logging_helper.rb +18 -0
- data/spec/lib/mock_adapter.rb +27 -0
- data/spec/lib/model_loader.rb +91 -0
- data/spec/lib/publicize_methods.rb +28 -0
- data/spec/models/vehicles.rb +34 -0
- data/spec/models/zoo.rb +47 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +86 -0
- data/spec/unit/adapters/abstract_adapter_spec.rb +133 -0
- data/spec/unit/adapters/adapter_shared_spec.rb +15 -0
- data/spec/unit/adapters/data_objects_adapter_spec.rb +628 -0
- data/spec/unit/adapters/postgres_adapter_spec.rb +133 -0
- data/spec/unit/associations/many_to_many_spec.rb +17 -0
- data/spec/unit/associations/many_to_one_spec.rb +152 -0
- data/spec/unit/associations/one_to_many_spec.rb +393 -0
- data/spec/unit/associations/one_to_one_spec.rb +7 -0
- data/spec/unit/associations/relationship_spec.rb +71 -0
- data/spec/unit/associations_spec.rb +242 -0
- data/spec/unit/auto_migrations_spec.rb +111 -0
- data/spec/unit/collection_spec.rb +182 -0
- data/spec/unit/data_mapper_spec.rb +35 -0
- data/spec/unit/identity_map_spec.rb +126 -0
- data/spec/unit/is_spec.rb +80 -0
- data/spec/unit/migrator_spec.rb +33 -0
- data/spec/unit/model_spec.rb +339 -0
- data/spec/unit/naming_conventions_spec.rb +36 -0
- data/spec/unit/property_set_spec.rb +83 -0
- data/spec/unit/property_spec.rb +753 -0
- data/spec/unit/query_spec.rb +530 -0
- data/spec/unit/repository_spec.rb +93 -0
- data/spec/unit/resource_spec.rb +626 -0
- data/spec/unit/scope_spec.rb +142 -0
- data/spec/unit/transaction_spec.rb +493 -0
- data/spec/unit/type_map_spec.rb +114 -0
- data/spec/unit/type_spec.rb +119 -0
- data/tasks/ci.rb +68 -0
- data/tasks/dm.rb +63 -0
- data/tasks/doc.rb +20 -0
- data/tasks/gemspec.rb +23 -0
- data/tasks/hoe.rb +46 -0
- data/tasks/install.rb +20 -0
- 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
|