vidibus-inheritance 0.3.6 → 0.3.9
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/README.rdoc +3 -2
- data/TODO +4 -5
- data/VERSION +1 -1
- data/lib/vidibus/inheritance.rb +1 -7
- data/lib/vidibus/inheritance/mongoid.rb +239 -190
- data/lib/vidibus/inheritance/validators/ancestor_validator.rb +1 -1
- data/spec/models.rb +70 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/vidibus/inheritance/inheritance_spec.rb +396 -0
- data/spec/vidibus/inheritance/mongoid_spec.rb +104 -397
- data/spec/vidibus/inheritance/validators/ancestor_validator_spec.rb +11 -13
- data/vidibus-inheritance.gemspec +7 -3
- metadata +8 -4
@@ -3,7 +3,7 @@ module Vidibus
|
|
3
3
|
module Validators
|
4
4
|
class AncestorValidator < ActiveModel::EachValidator
|
5
5
|
def validate_each(record, attribute, value)
|
6
|
-
unless value.
|
6
|
+
unless value.class == record.class
|
7
7
|
record.errors[attribute] << "must be a #{record.class}"
|
8
8
|
end
|
9
9
|
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
class Model
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Timestamps
|
4
|
+
include Vidibus::Uuid::Mongoid
|
5
|
+
include Vidibus::Inheritance::Mongoid
|
6
|
+
field :name
|
7
|
+
field :age, :type => Integer
|
8
|
+
embeds_many :children
|
9
|
+
embeds_many :puppets
|
10
|
+
embeds_one :location
|
11
|
+
end
|
12
|
+
|
13
|
+
class Trude < Model
|
14
|
+
before_validation :trudify
|
15
|
+
def trudify; self.name = "Trude"; end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Child
|
19
|
+
include Mongoid::Document
|
20
|
+
field :name
|
21
|
+
field :mutated, :type => Boolean
|
22
|
+
validates :name, :presence => true
|
23
|
+
embedded_in :model, :inverse_of => :children
|
24
|
+
embeds_many :puppets
|
25
|
+
embeds_one :location
|
26
|
+
end
|
27
|
+
|
28
|
+
class Puppet
|
29
|
+
include Mongoid::Document
|
30
|
+
field :name
|
31
|
+
validates :name, :presence => true
|
32
|
+
embedded_in :child, :inverse_of => :puppets
|
33
|
+
embedded_in :model, :inverse_of => :puppets
|
34
|
+
embedded_in :location, :inverse_of => :puppets
|
35
|
+
embeds_one :location
|
36
|
+
end
|
37
|
+
|
38
|
+
class Location
|
39
|
+
include Mongoid::Document
|
40
|
+
field :name
|
41
|
+
field :mutated, :type => Boolean
|
42
|
+
validates :name, :presence => true
|
43
|
+
embedded_in :model, :inverse_of => :location
|
44
|
+
embedded_in :child, :inverse_of => :location
|
45
|
+
embedded_in :puppet, :inverse_of => :location
|
46
|
+
embeds_many :puppets
|
47
|
+
end
|
48
|
+
|
49
|
+
class Manager
|
50
|
+
include Mongoid::Document
|
51
|
+
include Vidibus::Uuid::Mongoid
|
52
|
+
include Vidibus::Inheritance::Mongoid
|
53
|
+
field :name
|
54
|
+
validates :name, :presence => true
|
55
|
+
end
|
56
|
+
|
57
|
+
class Clerk
|
58
|
+
include Mongoid::Document
|
59
|
+
field :name
|
60
|
+
validates :name, :presence => true
|
61
|
+
end
|
62
|
+
|
63
|
+
class ValidatedModel
|
64
|
+
include ActiveModel::Validations
|
65
|
+
attr_accessor :ancestor
|
66
|
+
validates :ancestor, :ancestor => true
|
67
|
+
end
|
68
|
+
|
69
|
+
class ValidatedModelSubclass < ValidatedModel
|
70
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,10 +4,11 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
|
4
4
|
require "rubygems"
|
5
5
|
require "active_support/core_ext"
|
6
6
|
require "spec"
|
7
|
+
require "rr"
|
7
8
|
require "mongoid"
|
8
9
|
require "vidibus-uuid"
|
9
10
|
require "vidibus-inheritance"
|
10
|
-
require "
|
11
|
+
require "models"
|
11
12
|
|
12
13
|
Mongoid.configure do |config|
|
13
14
|
name = "vidibus-inheritance_test"
|
@@ -0,0 +1,396 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Inheritance" do
|
4
|
+
let(:ancestor) { Model.create }
|
5
|
+
let(:inheritor) { Model.new }
|
6
|
+
let(:anna) { Model.create!(:name => "Anna", :age => 35) }
|
7
|
+
|
8
|
+
it "should happen when creating objects" do
|
9
|
+
ancestor # trigger object creation before mocking
|
10
|
+
mock.instance_of(Model).inherit_attributes
|
11
|
+
Model.create!(:ancestor => ancestor)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should happen when ancestor did change" do
|
15
|
+
inheritor = Model.create!
|
16
|
+
inheritor.ancestor = ancestor
|
17
|
+
stub(inheritor).inherit_attributes
|
18
|
+
inheritor.save
|
19
|
+
inheritor.should have_received.inherit_attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not happen when ancestor did not change" do
|
23
|
+
inheritor = Model.create!(:ancestor => ancestor)
|
24
|
+
dont_allow(inheritor).inherit
|
25
|
+
inheritor.save
|
26
|
+
# Does not work with RR:
|
27
|
+
# inheritor.should_not have_received.inherit
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should apply ancestor's attributes to inheritor" do
|
31
|
+
inheritor.update_attributes(:ancestor => anna)
|
32
|
+
inheritor.name.should eql("Anna")
|
33
|
+
inheritor.age.should eql(35)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not inherit acquired attributes" do
|
37
|
+
inheritor.update_attributes(:ancestor => ancestor)
|
38
|
+
Model::ACQUIRED_ATTRIBUTES.should include("uuid")
|
39
|
+
inheritor.uuid.should_not eql(ancestor.uuid)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should apply ancestor's attributes to inheritor but keep previously mutated attributes" do
|
43
|
+
inheritor.update_attributes(:name => "Jenny")
|
44
|
+
inheritor.update_attributes(:ancestor => anna)
|
45
|
+
inheritor.name.should eql("Jenny")
|
46
|
+
inheritor.age.should eql(35)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should apply ancestor's attributes to inheritor but keep recently mutated attributes" do
|
50
|
+
inheritor.update_attributes(:ancestor => anna, :name => "Jenny")
|
51
|
+
inheritor.name.should eql("Jenny")
|
52
|
+
inheritor.age.should eql(35)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should allow switching the ancestor" do
|
56
|
+
inheritor.inherit_from!(anna)
|
57
|
+
another_ancestor = Model.create!(:name => "Leah", :age => 30)
|
58
|
+
inheritor.inherit_from!(another_ancestor)
|
59
|
+
inheritor.ancestor.should eql(another_ancestor)
|
60
|
+
inheritor.name.should eql("Leah")
|
61
|
+
inheritor.age.should eql(30)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should apply changes on ancestor to inheritor" do
|
65
|
+
inheritor.inherit_from!(anna)
|
66
|
+
inheritor.name.should eql("Anna")
|
67
|
+
anna.update_attributes(:name => "Leah")
|
68
|
+
inheritor.reload
|
69
|
+
inheritor.name.should eql("Leah")
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should preserve changes on inheritor" do
|
73
|
+
inheritor = Model.create(:ancestor => anna)
|
74
|
+
inheritor.update_attributes(:name => "Sara")
|
75
|
+
inheritor.mutated_attributes.should eql(["name"])
|
76
|
+
anna.update_attributes(:name => "Leah")
|
77
|
+
inheritor.reload
|
78
|
+
inheritor.name.should eql("Sara")
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not update inheritor if acquired attributes were changed on ancestor" do
|
82
|
+
inheritor.inherit_from!(ancestor)
|
83
|
+
Model::ACQUIRED_ATTRIBUTES.should include("updated_at")
|
84
|
+
dont_allow(ancestor.inheritors.first).inherit!
|
85
|
+
ancestor.update_attributes(:updated_at => Time.now)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should not update inheritor if no inheritable attributes were changed on ancestor" do
|
89
|
+
inheritor.inherit_from!(anna)
|
90
|
+
dont_allow(anna.inheritors.first).inherit!
|
91
|
+
anna.update_attributes(:name => "Anna")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should be applied before validation" do
|
95
|
+
ancestor = Manager.create!(:name => "John")
|
96
|
+
invalid = Manager.new
|
97
|
+
invalid.should_not be_valid
|
98
|
+
valid = Manager.new(:ancestor => ancestor)
|
99
|
+
valid.should be_valid
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should destroy inheritor when destroying ancestor" do
|
103
|
+
inheritor.inherit_from!(ancestor)
|
104
|
+
ancestor.destroy
|
105
|
+
expect { inheritor.reload }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
106
|
+
end
|
107
|
+
|
108
|
+
context "with embedded collections" do
|
109
|
+
before do
|
110
|
+
inheritor.inherit_from!(ancestor)
|
111
|
+
ancestor.children.create(:name => "Han")
|
112
|
+
ancestor.save
|
113
|
+
inheritor.reload
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should inherit subobjects on existing relationship" do
|
117
|
+
inheritor.children.should have(1).child
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should inherit subobjects when relationship gets established" do
|
121
|
+
inheritor = Model.new
|
122
|
+
inheritor.inherit_from!(ancestor)
|
123
|
+
inheritor.children.should have(1).child
|
124
|
+
inheritor.reload.children.should have(1).child
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should add subobjects" do
|
128
|
+
ancestor.children << Child.new(:name => "Leah")
|
129
|
+
ancestor.save
|
130
|
+
ancestor.children.should have(2).children
|
131
|
+
inheritor.reload
|
132
|
+
inheritor.children.should have(2).children
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should add subobjects with saving" do
|
136
|
+
ancestor.children << Child.new(:name => "Leah")
|
137
|
+
ancestor.save
|
138
|
+
ancestor.children.should have(2).children
|
139
|
+
inheritor.save
|
140
|
+
inheritor.reload
|
141
|
+
inheritor.children.should have(2).children
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should not add existing subobjects twice" do
|
145
|
+
inheritor.inherit_from!(ancestor)
|
146
|
+
inheritor.children.should have(1).child
|
147
|
+
inheritor.reload.children.should have(1).child
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should remove subobjects" do
|
151
|
+
inheritor.children.should have(1).child
|
152
|
+
ancestor.children.first.destroy
|
153
|
+
ancestor.save
|
154
|
+
inheritor.reload
|
155
|
+
ancestor.children.should have(0).children
|
156
|
+
inheritor.children.should have(0).children
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should remove a single suboject without removing others on inheritor" do
|
160
|
+
inheritor.children.create(:name => "Leah")
|
161
|
+
inheritor.children.should have(2).children
|
162
|
+
ancestor.children.first.destroy
|
163
|
+
ancestor.save
|
164
|
+
ancestor.children.should have(0).children
|
165
|
+
inheritor.reload
|
166
|
+
inheritor.children.should have(1).child
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should update subobjects" do
|
170
|
+
ancestor.children.first.name = "Luke"
|
171
|
+
ancestor.save
|
172
|
+
inheritor.reload
|
173
|
+
inheritor.children.first.name.should eql("Luke")
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should call #update_inherited_attributes for updating subobjects, if available" do
|
177
|
+
Child.send(:define_method, :update_inherited_attributes) do
|
178
|
+
self.update_attributes(:name => "Callback")
|
179
|
+
end
|
180
|
+
Child.send(:protected, :update_inherited_attributes)
|
181
|
+
ancestor.children.first.name = "Luke"
|
182
|
+
ancestor.save
|
183
|
+
Child.send(:remove_method, :update_inherited_attributes)
|
184
|
+
inheritor.reload
|
185
|
+
inheritor.children.first.name.should eql("Callback")
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should exclude acquired attributes of subobjects" do
|
189
|
+
ancestor.children.first.mutated = true
|
190
|
+
ancestor.save
|
191
|
+
ancestor.children.first.mutated.should be_true
|
192
|
+
inheritor.reload
|
193
|
+
inheritor.children.first.mutated.should be_false
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should inherit embedded documents of subobjects" do
|
197
|
+
ancestor.children.first.puppets.create(:name => "Goofy")
|
198
|
+
ancestor.save
|
199
|
+
inheritor.reload
|
200
|
+
inheritor.children.first.puppets.should have(1).puppet
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context "with embedded items" do
|
205
|
+
before do
|
206
|
+
inheritor.inherit_from!(ancestor)
|
207
|
+
ancestor.create_location(:name => "Home")
|
208
|
+
ancestor.save
|
209
|
+
inheritor.reload
|
210
|
+
end
|
211
|
+
|
212
|
+
it "should inherit subobject on existing relationship" do
|
213
|
+
inheritor.location.should_not be_nil
|
214
|
+
end
|
215
|
+
|
216
|
+
it "should inherit subobjects when relationship gets established" do
|
217
|
+
inheritor = Model.new
|
218
|
+
inheritor.inherit_from!(ancestor)
|
219
|
+
inheritor.location.should_not be_nil
|
220
|
+
end
|
221
|
+
|
222
|
+
it "should update subobject" do
|
223
|
+
ancestor.location.name = "Studio"
|
224
|
+
ancestor.save
|
225
|
+
inheritor.reload
|
226
|
+
ancestor.location.name.should eql("Studio")
|
227
|
+
inheritor.location.name.should eql("Studio")
|
228
|
+
end
|
229
|
+
|
230
|
+
it "should remove subobject" do
|
231
|
+
ancestor.location.destroy
|
232
|
+
ancestor.save
|
233
|
+
inheritor.reload
|
234
|
+
ancestor.location.should be_nil
|
235
|
+
inheritor.location.should be_nil
|
236
|
+
end
|
237
|
+
|
238
|
+
it "should exclude acquired attributes of subobject" do
|
239
|
+
ancestor.location.mutated = true
|
240
|
+
ancestor.save
|
241
|
+
ancestor.location.mutated.should be_true
|
242
|
+
inheritor.reload
|
243
|
+
inheritor.location.mutated.should be_false
|
244
|
+
end
|
245
|
+
|
246
|
+
it "should inherit embedded documents of subobject" do
|
247
|
+
ancestor.location.puppets.create(:name => "Goofy")
|
248
|
+
ancestor.save
|
249
|
+
inheritor.reload
|
250
|
+
inheritor.location.puppets.should have(1).puppet
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context "across several generations" do
|
255
|
+
let(:grand_ancestor) { Model.create!(:name => "Anna", :age => 97) }
|
256
|
+
|
257
|
+
before do
|
258
|
+
ancestor.inherit_from!(grand_ancestor)
|
259
|
+
inheritor.inherit_from!(ancestor)
|
260
|
+
end
|
261
|
+
|
262
|
+
it "should apply changes on grand ancestor to inheritor" do
|
263
|
+
inheritor.name.should eql("Anna")
|
264
|
+
grand_ancestor.update_attributes(:name => "Leah")
|
265
|
+
inheritor.reload
|
266
|
+
inheritor.name.should eql("Leah")
|
267
|
+
end
|
268
|
+
|
269
|
+
it "should not apply changes on grand ancestor to inheritor if predecessor has mutations" do
|
270
|
+
ancestor.update_attributes(:name => "Jenny")
|
271
|
+
grand_ancestor.update_attributes(:name => "Leah")
|
272
|
+
inheritor.reload
|
273
|
+
inheritor.name.should eql("Jenny")
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should allow resetting mutated attributes" do
|
277
|
+
ancestor.update_attributes(:name => "Sara")
|
278
|
+
ancestor.name.should eql("Sara")
|
279
|
+
inheritor.reload
|
280
|
+
inheritor.name.should eql("Sara")
|
281
|
+
ancestor.inherit!(:reset => :name)
|
282
|
+
ancestor.name.should eql("Anna")
|
283
|
+
inheritor.reload
|
284
|
+
inheritor.name.should eql("Anna")
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should destroy all inheritors when destroying ancestor" do
|
288
|
+
grand_ancestor.destroy
|
289
|
+
expect { ancestor.reload }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
290
|
+
expect { inheritor.reload }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
291
|
+
end
|
292
|
+
|
293
|
+
context "with embedded collections" do
|
294
|
+
before do
|
295
|
+
grand_ancestor.children.create(:name => "Han")
|
296
|
+
grand_ancestor.save
|
297
|
+
ancestor.reload
|
298
|
+
inheritor.reload
|
299
|
+
end
|
300
|
+
|
301
|
+
it "should inherit subobjects" do
|
302
|
+
inheritor.children.should have(1).child
|
303
|
+
end
|
304
|
+
|
305
|
+
it "should add subobjects" do
|
306
|
+
grand_ancestor.children << Child.new(:name => "Leah")
|
307
|
+
grand_ancestor.save
|
308
|
+
inheritor.reload
|
309
|
+
inheritor.children.should have(2).children
|
310
|
+
end
|
311
|
+
|
312
|
+
it "should not add existing subobjects twice" do
|
313
|
+
ancestor.inherit_from!(grand_ancestor)
|
314
|
+
inheritor.reload
|
315
|
+
inheritor.children.should have(1).child
|
316
|
+
end
|
317
|
+
|
318
|
+
it "should remove subobjects" do
|
319
|
+
inheritor.children.should have(1).child
|
320
|
+
grand_ancestor.children.first.destroy
|
321
|
+
grand_ancestor.save
|
322
|
+
grand_ancestor.children.should have(0).children
|
323
|
+
inheritor.reload
|
324
|
+
inheritor.children.should have(0).children
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should remove a single suboject without removing others on inheritor" do
|
328
|
+
inheritor.children.create(:name => "Leah")
|
329
|
+
inheritor.children.should have(2).children
|
330
|
+
grand_ancestor.children.first.destroy
|
331
|
+
grand_ancestor.save
|
332
|
+
grand_ancestor.children.should have(0).children
|
333
|
+
inheritor.reload
|
334
|
+
inheritor.children.should have(1).child
|
335
|
+
end
|
336
|
+
|
337
|
+
it "should update subobjects" do
|
338
|
+
grand_ancestor.children.first.name = "Luke"
|
339
|
+
grand_ancestor.save
|
340
|
+
grand_ancestor.children.first.name.should eql("Luke")
|
341
|
+
inheritor.reload
|
342
|
+
inheritor.children.first.name.should eql("Luke")
|
343
|
+
end
|
344
|
+
|
345
|
+
it "should inherit embedded documents of subobjects" do
|
346
|
+
grand_ancestor.children.first.puppets.create(:name => "Goofy")
|
347
|
+
grand_ancestor.save
|
348
|
+
inheritor.reload
|
349
|
+
inheritor.children.first.puppets.should have(1).puppet
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
context "with embedded items" do
|
354
|
+
before do
|
355
|
+
grand_ancestor.create_location(:name => "Home")
|
356
|
+
grand_ancestor.save
|
357
|
+
ancestor.reload
|
358
|
+
inheritor.reload
|
359
|
+
end
|
360
|
+
|
361
|
+
it "should inherit subobject on existing relationship" do
|
362
|
+
inheritor.location.should_not be_nil
|
363
|
+
end
|
364
|
+
|
365
|
+
it "should inherit subobject when relationship gets established" do
|
366
|
+
ancestor = Model.new
|
367
|
+
ancestor.inherit_from!(grand_ancestor)
|
368
|
+
inheritor = Model.new
|
369
|
+
inheritor.inherit_from!(ancestor)
|
370
|
+
inheritor.location.should_not be_nil
|
371
|
+
end
|
372
|
+
|
373
|
+
it "should update subobject" do
|
374
|
+
grand_ancestor.location.name = "Studio"
|
375
|
+
grand_ancestor.save
|
376
|
+
inheritor.reload
|
377
|
+
inheritor.location.name.should eql("Studio")
|
378
|
+
end
|
379
|
+
|
380
|
+
it "should remove subobject" do
|
381
|
+
grand_ancestor.location.destroy
|
382
|
+
grand_ancestor.save
|
383
|
+
inheritor.reload
|
384
|
+
grand_ancestor.location.should be_nil
|
385
|
+
inheritor.location.should be_nil
|
386
|
+
end
|
387
|
+
|
388
|
+
it "should inherit embedded documents of subobject" do
|
389
|
+
grand_ancestor.location.puppets.create(:name => "Goofy")
|
390
|
+
grand_ancestor.save
|
391
|
+
inheritor.reload
|
392
|
+
inheritor.location.puppets.should have(1).puppet
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|