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
data/README.rdoc
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
This gem is part of the open source SOA framework Vidibus: http://www.vidibus.org
|
4
4
|
|
5
|
-
It allows inheritance of objects
|
5
|
+
It allows inheritance of objects and is depends on Rails 3 and Mongoid. It will update all attributes and embedded documents of inheritors when ancestor gets changed. Custom attributes (mutations) of inheritors will not be overridden, unless a :reset option is set.
|
6
6
|
|
7
7
|
|
8
8
|
== Installation
|
@@ -25,7 +25,7 @@ Include the Vidibus::Uuid::Inheritance module in your Mongoid model:
|
|
25
25
|
field :name
|
26
26
|
end
|
27
27
|
|
28
|
-
To establish
|
28
|
+
To establish an inheritance relationship, add ancestor to a model of same class:
|
29
29
|
|
30
30
|
ancestor = Model.create(:name => "Anna")
|
31
31
|
|
@@ -49,6 +49,7 @@ When inheriting, the attribute :_reference_id will be set on embedded documents
|
|
49
49
|
All attributes will be inherited, except these ACQUIRED_ATTRIBUTES:
|
50
50
|
|
51
51
|
_id
|
52
|
+
_type
|
52
53
|
uuid
|
53
54
|
ancestor_uuid
|
54
55
|
mutated_attributes
|
data/TODO
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
Get ACQUIRED_ATTRIBUTES as class variable from each document, if available
|
1
|
+
* Removed items will be re-added when inheritance is performed again. Introduce paranoid behaviour for embedded collection items? Or add a list of deleted associations like _destroyed_children?
|
2
|
+
* Use delayed_job for inheritance across a huge pedigree.
|
3
|
+
* Maybe use UUID for children to ensure portability across the network?
|
4
|
+
* Get ACQUIRED_ATTRIBUTES as class variable from each document, if available.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.3.
|
1
|
+
0.3.9
|
data/lib/vidibus/inheritance.rb
CHANGED
@@ -3,7 +3,7 @@ module Vidibus
|
|
3
3
|
module Mongoid
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
ACQUIRED_ATTRIBUTES = %w[_id uuid ancestor_uuid mutated_attributes mutated created_at updated_at version versions]
|
6
|
+
ACQUIRED_ATTRIBUTES = %w[_id _type uuid ancestor_uuid mutated_attributes mutated created_at updated_at version versions]
|
7
7
|
|
8
8
|
included do
|
9
9
|
attr_accessor :inherited_attributes, :_inherited
|
@@ -16,223 +16,272 @@ module Vidibus
|
|
16
16
|
validates :ancestor_uuid, :uuid => { :allow_blank => true }
|
17
17
|
validates :ancestor, :ancestor => true, :if => :ancestor_uuid?
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
self.ancestor_uuid = obj.uuid
|
30
|
-
@ancestor = obj
|
19
|
+
set_callback :validate, :before, :inherit_attributes, :if => :inherit?
|
20
|
+
set_callback :save, :before, :track_mutations
|
21
|
+
set_callback :save, :after, :postprocess
|
22
|
+
set_callback :destroy, :after, :destroy_inheritors
|
23
|
+
|
24
|
+
# Returns true if attributes have been mutated.
|
25
|
+
# This method must be included directly so that it will not be
|
26
|
+
# overwritten by Mongoid's accessor #mutated?
|
27
|
+
def mutated?
|
28
|
+
@is_mutated ||= mutated || mutated_attributes.any?
|
31
29
|
end
|
30
|
+
end
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
module ClassMethods
|
33
|
+
|
34
|
+
# Returns embedded documents of given document.
|
35
|
+
# Provide :keys option if you want to return a list of document types instead
|
36
|
+
# of a hash of documents grouped by their type.
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
#
|
40
|
+
# inheritable_documents(model)
|
41
|
+
# # => { :children => [ <embeds_many collection> ], :location => <embeds_one object> }
|
42
|
+
#
|
43
|
+
# inheritable_documents(model, :keys => true)
|
44
|
+
# # => [:children, :location ]
|
45
|
+
#
|
46
|
+
def inheritable_documents(object, options = {})
|
47
|
+
keys = options[:keys]
|
48
|
+
collection = keys ? [] : {}
|
49
|
+
for name, association in object.associations
|
50
|
+
next unless association.embedded?
|
51
|
+
if keys
|
52
|
+
collection << name
|
53
|
+
else
|
54
|
+
collection[name] = object.send(name)
|
55
|
+
end
|
37
56
|
end
|
57
|
+
collection
|
38
58
|
end
|
39
|
-
|
40
|
-
#
|
41
|
-
# Accepts
|
42
|
-
#
|
43
|
-
# Usage:
|
44
|
-
#
|
45
|
-
# inherit!(:reset => true) => # Overwrites all mutated attributes
|
46
|
-
# inherit!(:reset => :name) => # Overwrites name only
|
47
|
-
# inherit!(:reset => [:name, :age]) => # Overwrites name and age
|
59
|
+
|
60
|
+
# Returns all objects that have no ancestor.
|
61
|
+
# Accepts Mongoid criteria to reduce and sort matching set.
|
62
|
+
# Criteria API: http://mongoid.org/docs/querying/
|
48
63
|
#
|
49
|
-
|
50
|
-
|
51
|
-
|
64
|
+
# Examples:
|
65
|
+
#
|
66
|
+
# Model.root # => All models without ancestor
|
67
|
+
# Model.root.where(:name => "Laura") # => Only models named Laura
|
68
|
+
# Model.root.asc(:created_at) # => All models, ordered by date of creation
|
69
|
+
#
|
70
|
+
def roots
|
71
|
+
where(:ancestor_uuid => nil)
|
52
72
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
73
|
+
end
|
74
|
+
|
75
|
+
# Setter for ancestor.
|
76
|
+
def ancestor=(obj)
|
77
|
+
self.ancestor_uuid = obj.uuid
|
78
|
+
@ancestor = obj
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns ancestor object by uuid.
|
82
|
+
def ancestor
|
83
|
+
@ancestor ||= begin
|
84
|
+
self.class.where(:uuid => ancestor_uuid).first if ancestor_uuid
|
59
85
|
end
|
86
|
+
end
|
60
87
|
|
61
|
-
|
62
|
-
|
63
|
-
|
88
|
+
# Returns a list of all ancestors ordered by inheritance distance.
|
89
|
+
def ancestors
|
90
|
+
@ancestors ||= [].tap do |bloodline|
|
91
|
+
obj = self
|
92
|
+
while true do
|
93
|
+
break unless obj = obj.ancestor
|
94
|
+
bloodline << obj
|
95
|
+
end
|
64
96
|
end
|
97
|
+
end
|
65
98
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
99
|
+
# Performs inheritance and saves instance with force.
|
100
|
+
# Accepts :reset option to overwrite mutated attributes.
|
101
|
+
#
|
102
|
+
# Examples:
|
103
|
+
#
|
104
|
+
# inherit!(:reset => true) => # Overwrites all mutated attributes
|
105
|
+
# inherit!(:reset => :name) => # Overwrites name only
|
106
|
+
# inherit!(:reset => [:name, :age]) => # Overwrites name and age
|
107
|
+
#
|
108
|
+
def inherit!(options = {})
|
109
|
+
inherit_attributes(options)
|
110
|
+
self.save!
|
111
|
+
end
|
112
|
+
|
113
|
+
# Performs inheritance from given object.
|
114
|
+
# It sets the ancestor and then calls #inherit! with given options.
|
115
|
+
def inherit_from!(obj, options = {})
|
116
|
+
self.ancestor = obj
|
117
|
+
self.inherit!(options)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns inheritors of this ancestor.
|
121
|
+
def inheritors
|
122
|
+
self.class.where(:ancestor_uuid => uuid)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns embedded documents.
|
126
|
+
# See ClassMethods.inheritable_documents for options.
|
127
|
+
def inheritable_documents(options = {})
|
128
|
+
self.class.inheritable_documents(self, options)
|
129
|
+
end
|
77
130
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
131
|
+
# Creates a sibling with identical inheritable attributes.
|
132
|
+
# Applies inheritance on new object.
|
133
|
+
def clone!
|
134
|
+
exceptions = ACQUIRED_ATTRIBUTES - ["ancestor_uuid"]
|
135
|
+
attrs = attributes.except(*exceptions)
|
136
|
+
self.class.create!(attrs)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
# Performs inheritance of attributes while excluding acquired and mutated ones.
|
142
|
+
# Accepts :reset option to overwrite mutated attributes.
|
143
|
+
def inherit_attributes(options = {})
|
144
|
+
track_mutations
|
145
|
+
self._inherited = true
|
146
|
+
exceptions = ACQUIRED_ATTRIBUTES
|
147
|
+
reset = options[:reset]
|
148
|
+
if !reset
|
149
|
+
exceptions += mutated_attributes
|
150
|
+
elsif reset != true
|
151
|
+
reset_attributes = reset.is_a?(Array) ? reset.map { |a| a.to_s } : [reset.to_s]
|
152
|
+
exceptions += mutated_attributes - reset_attributes
|
92
153
|
end
|
154
|
+
exceptions += ancestor.inheritable_documents.keys
|
155
|
+
self.attributes = self.inherited_attributes = ancestor.attributes.except(*exceptions)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Performs inheritance of documents.
|
159
|
+
def inherit_documents
|
160
|
+
return unless ancestor
|
161
|
+
return unless list = ancestor.inheritable_documents
|
162
|
+
for association, inheritable in list
|
93
163
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if inheritable.is_a?(Array)
|
102
|
-
collection = new_record? ? self.send(association) : self.reload.send(association)
|
103
|
-
existing_ids = collection.map do |a|
|
104
|
-
begin
|
105
|
-
a._reference_id
|
106
|
-
rescue
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
for obj in inheritable
|
111
|
-
attrs = inheritable_document_attributes(obj)
|
112
|
-
if existing_ids.include?(obj._id)
|
113
|
-
doc = collection.where(:_reference_id => obj._id).first
|
114
|
-
update_inheritable_document(doc, attrs)
|
115
|
-
else
|
116
|
-
doc = collection.create!(attrs)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
obsolete = existing_ids - inheritable.map { |i| i._id }
|
121
|
-
if obsolete.any?
|
122
|
-
collection.destroy_all(:_reference_id => obsolete)
|
164
|
+
# embeds_many
|
165
|
+
if inheritable.is_a?(Array)
|
166
|
+
collection = new_record? ? self.send(association) : self.reload.send(association)
|
167
|
+
existing_ids = collection.map do |a|
|
168
|
+
begin
|
169
|
+
a._reference_id
|
170
|
+
rescue
|
123
171
|
end
|
172
|
+
end
|
124
173
|
|
125
|
-
|
126
|
-
|
127
|
-
if
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
174
|
+
for obj in inheritable
|
175
|
+
attrs = inheritable_document_attributes(obj)
|
176
|
+
if existing_ids.include?(obj._id)
|
177
|
+
doc = collection.where(:_reference_id => obj._id).first
|
178
|
+
update_inheritable_document(doc, attrs)
|
179
|
+
else
|
180
|
+
doc = collection.create!(attrs)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
obsolete = (existing_ids - inheritable.map { |i| i._id }).compact
|
184
|
+
if obsolete.any?
|
185
|
+
collection.delete_all(:conditions => { :_reference_id.in => obsolete })
|
186
|
+
end
|
187
|
+
|
188
|
+
# embeds_one
|
189
|
+
else
|
190
|
+
if inheritable
|
191
|
+
attrs = inheritable_document_attributes(inheritable)
|
192
|
+
if doc = self.send("#{association}")
|
193
|
+
update_inheritable_document(doc, attrs)
|
194
|
+
else
|
195
|
+
self.send("create_#{association}", attrs)
|
136
196
|
end
|
197
|
+
elsif existing = self.send("#{association}")
|
198
|
+
existing.destroy
|
137
199
|
end
|
138
200
|
end
|
139
201
|
end
|
140
|
-
|
141
|
-
# Performs actions before saving.
|
142
|
-
def preprocess
|
143
|
-
track_mutations
|
144
|
-
inherit_attributes if inherit?
|
145
|
-
end
|
202
|
+
end
|
146
203
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
# Returns true if inheritance should be applied on inheritor.
|
154
|
-
def inherit?
|
155
|
-
!_inherited and ancestor and (new_record? or ancestor_uuid_changed?)
|
156
|
-
end
|
204
|
+
# Performs actions after saving.
|
205
|
+
def postprocess
|
206
|
+
inherit_documents if inheritor? and embed?
|
207
|
+
update_inheritors
|
208
|
+
end
|
157
209
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
210
|
+
# Returns true if object is an inheritor.
|
211
|
+
def inheritor?
|
212
|
+
ancestor
|
213
|
+
end
|
214
|
+
|
215
|
+
# Returns true if inheritance should be applied on inheritor.
|
216
|
+
def inherit?
|
217
|
+
!_inherited and ancestor and (new_record? or ancestor_uuid_changed?)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns true if this documents has any inheritable documents.
|
221
|
+
def embed?
|
222
|
+
inheritable_documents.any?
|
223
|
+
end
|
162
224
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
end
|
171
|
-
self.inherited_attributes = nil
|
172
|
-
end
|
173
|
-
self.mutated_attributes += changed_items
|
174
|
-
self.mutated_attributes.uniq!
|
175
|
-
end
|
176
|
-
|
177
|
-
# Updates an given document with given attributes.
|
178
|
-
def update_inheritable_document(doc, attrs)
|
179
|
-
update_inheritable_document_attributes(doc, attrs)
|
180
|
-
update_inheritable_document_children(doc, attrs)
|
181
|
-
end
|
182
|
-
|
183
|
-
# Updates an given document with given attributes.
|
184
|
-
# This will perform #update_inherited_attributes on document, if this callback method is available.
|
185
|
-
def update_inheritable_document_attributes(doc, attrs)
|
186
|
-
if doc.respond_to?(:update_inherited_attributes)
|
187
|
-
doc.send(:update_inherited_attributes, attrs)
|
188
|
-
else
|
189
|
-
doc.update_attributes(attrs)
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
# Updates children of given embedded document.
|
194
|
-
# Because update_attributes won't modify the hash of children, a custom database update is needed.
|
195
|
-
def update_inheritable_document_children(doc, attrs)
|
196
|
-
inheritable_documents = self.class.inheritable_documents(doc, :keys => true)
|
197
|
-
idocs = attrs.only(*inheritable_documents)
|
198
|
-
query = {}
|
199
|
-
for k,v in idocs
|
200
|
-
query["#{doc._position}.#{k}"] = v
|
225
|
+
# Stores changed attributes as #mutated_attributes unless they have been inherited recently.
|
226
|
+
def track_mutations
|
227
|
+
changed_items = new_record? ? attributes.keys : changes.keys
|
228
|
+
changed_items -= ACQUIRED_ATTRIBUTES
|
229
|
+
if inherited_attributes
|
230
|
+
for key, value in inherited_attributes
|
231
|
+
changed_items.delete(key) if value == attributes[key]
|
201
232
|
end
|
202
|
-
|
203
|
-
end
|
204
|
-
|
205
|
-
# Returns list of inheritable attributes of a given document.
|
206
|
-
# The list will include the _id as reference.
|
207
|
-
def inheritable_document_attributes(doc)
|
208
|
-
attrs = doc.attributes.except(*ACQUIRED_ATTRIBUTES)
|
209
|
-
attrs[:_reference_id] = doc._id
|
210
|
-
attrs
|
233
|
+
self.inherited_attributes = nil
|
211
234
|
end
|
235
|
+
self.mutated_attributes += changed_items
|
236
|
+
self.mutated_attributes.uniq!
|
237
|
+
end
|
238
|
+
|
239
|
+
# Updates an given document with given attributes.
|
240
|
+
def update_inheritable_document(doc, attrs)
|
241
|
+
update_inheritable_document_attributes(doc, attrs)
|
242
|
+
update_inheritable_document_children(doc, attrs)
|
243
|
+
end
|
212
244
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
245
|
+
# Updates an given document with given attributes.
|
246
|
+
# This will perform #update_inherited_attributes on document, if this callback method is available.
|
247
|
+
def update_inheritable_document_attributes(doc, attrs)
|
248
|
+
if doc.respond_to?(:update_inherited_attributes)
|
249
|
+
doc.send(:update_inherited_attributes, attrs)
|
250
|
+
else
|
251
|
+
doc.update_attributes(attrs)
|
217
252
|
end
|
218
|
-
|
219
|
-
class << self
|
253
|
+
end
|
220
254
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
else
|
230
|
-
collection[name] = doc.send(name)
|
231
|
-
end
|
232
|
-
end
|
233
|
-
collection
|
234
|
-
end
|
255
|
+
# Updates children of given embedded document.
|
256
|
+
# Because update_attributes won't modify the hash of children, a custom database update is needed.
|
257
|
+
def update_inheritable_document_children(doc, attrs)
|
258
|
+
inheritable_documents = self.class.inheritable_documents(doc, :keys => true)
|
259
|
+
idocs = attrs.only(*inheritable_documents)
|
260
|
+
query = {}
|
261
|
+
for k,v in idocs
|
262
|
+
query["#{doc._position}.#{k}"] = v
|
235
263
|
end
|
264
|
+
_collection.update(_selector, { "$set" => query })
|
265
|
+
end
|
266
|
+
|
267
|
+
# Returns list of inheritable attributes of a given document.
|
268
|
+
# The list will include the _id as reference.
|
269
|
+
def inheritable_document_attributes(doc)
|
270
|
+
attrs = doc.attributes.except(*ACQUIRED_ATTRIBUTES)
|
271
|
+
attrs[:_reference_id] = doc._id
|
272
|
+
attrs
|
273
|
+
end
|
274
|
+
|
275
|
+
# Applies changes to inheritors.
|
276
|
+
def update_inheritors
|
277
|
+
return unless inheritors.any?
|
278
|
+
inheritors.each(&:inherit!)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Destroys inheritors.
|
282
|
+
def destroy_inheritors
|
283
|
+
return unless inheritors.any?
|
284
|
+
inheritors.each(&:destroy)
|
236
285
|
end
|
237
286
|
end
|
238
287
|
end
|