vidibus-inheritance 0.3.6 → 0.3.9

Sign up to get free protection for your applications and to get access to all the features.
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 for Rails 3 with 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.
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 a inheritance relationship, add ancestor to a model of same class:
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
- Use delayed_job
2
-
3
- Use uuid for children to ensure portability across the network?
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.6
1
+ 0.3.9
@@ -1,8 +1,2 @@
1
1
  require "inheritance/mongoid"
2
- require "inheritance/validators"
3
-
4
- module Vidibus
5
- module Inheritance
6
- class SomeError < StandardError; end
7
- end
8
- end
2
+ require "inheritance/validators"
@@ -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
- before_validation :preprocess
20
- after_save :postprocess
21
-
22
- # Callback of Mongoid when deleting a collection item on a parent object.
23
- def remove(*args)
24
- super(*args)
25
- end
26
-
27
- # Setter for ancestor.
28
- def ancestor=(obj)
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
- # Returns ancestor object by uuid.
34
- def ancestor
35
- @ancestor ||= begin
36
- self.class.where(:uuid => ancestor_uuid).first if ancestor_uuid
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
- # Performs inheritance and saves instance with force.
41
- # Accepts :reset option to overwrite mutated attributes.
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
- def inherit!(options = {})
50
- self.inherit_attributes(options)
51
- self.save!
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
- # Performs inheritance from given object.
55
- # Accepts :reset option to overwrite mutated attributes.
56
- def inherit_from!(obj, options = {})
57
- self.ancestor = obj
58
- self.inherit!(options)
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
- # Returns list of objects that inherit from this ancestor.
62
- def inheritors
63
- self.class.where(:ancestor_uuid => uuid).to_a
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
- # Returns embedded documents.
67
- def inheritable_documents
68
- self.class.inheritable_documents(self)
69
- end
70
-
71
- # Returns true if attributes have been mutated.
72
- def mutated?
73
- @is_mutated ||= mutated || mutated_attributes.any?
74
- end
75
-
76
- protected
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
- # Performs inheritance of attributes while excluding acquired and mutated ones.
79
- # Accepts :reset option to overwrite mutated attributes.
80
- def inherit_attributes(options = {})
81
- self._inherited = true
82
- exceptions = ACQUIRED_ATTRIBUTES
83
- reset = options[:reset]
84
- if !reset
85
- exceptions += mutated_attributes
86
- elsif reset != true
87
- reset_attributes = reset.is_a?(Array) ? reset.map { |a| a.to_s } : [reset.to_s]
88
- exceptions += mutated_attributes - reset_attributes
89
- end
90
- exceptions += ancestor.inheritable_documents.keys
91
- self.attributes = self.inherited_attributes = ancestor.attributes.except(*exceptions)
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
- # Performs inheritance of documents.
95
- def inherit_documents
96
- return unless ancestor
97
- return unless list = ancestor.inheritable_documents
98
- for association, inheritable in list
99
-
100
- # embeds_many
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
- # embeds_one
126
- else
127
- if inheritable
128
- attrs = inheritable_document_attributes(inheritable)
129
- if doc = self.send("#{association}")
130
- update_inheritable_document(doc, attrs)
131
- else
132
- self.send("create_#{association}", attrs)
133
- end
134
- elsif existing = self.send("#{association}")
135
- existing.destroy
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
- # Performs actions after saving.
148
- def postprocess
149
- inherit_documents if embed?
150
- update_inheritors
151
- end
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
- # Returns true if this documents has any inheritable documents.
159
- def embed?
160
- inheritable_documents.any?
161
- end
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
- # Stores changed attributes as #mutated_attributes unless they have been inherited recently.
164
- def track_mutations
165
- changed_items = new_record? ? attributes.keys : changes.keys
166
- changed_items -= ACQUIRED_ATTRIBUTES
167
- if inherited_attributes
168
- for key, value in inherited_attributes
169
- changed_items.delete(key) if value == attributes[key]
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
- _collection.update(_selector, { "$set" => query })
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
- # Applies changes to inheritors.
214
- def update_inheritors
215
- return unless inheritors.any?
216
- inheritors.each(&:inherit!)
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
- # Returns embedded documents of given document.
222
- def inheritable_documents(doc, options = {})
223
- keys = options[:keys]
224
- collection = keys ? [] : {}
225
- for name, association in doc.associations
226
- next unless association.embedded?
227
- if keys
228
- collection << name
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