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