vidibus-inheritance 0.3.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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Andre Pankratz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,93 @@
1
+ = vidibus-inheritance
2
+
3
+ This gem is part of the open source SOA framework Vidibus: http://www.vidibus.org
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.
6
+
7
+
8
+ == Installation
9
+
10
+ Add the dependency to the Gemfile of your application:
11
+
12
+ gem "vidibus-inheritance"
13
+
14
+ Then call bundle install on your console.
15
+
16
+
17
+ == Usage
18
+
19
+ Include the Vidibus::Uuid::Inheritance module in your Mongoid model:
20
+
21
+ class Model
22
+ include Mongoid::Document
23
+ include Vidibus::Uuid::Mongoid
24
+ include Vidibus::Inheritance::Mongoid
25
+ field :name
26
+ end
27
+
28
+ To establish a inheritance relationship, add ancestor to a model of same class:
29
+
30
+ ancestor = Model.create(:name => "Anna")
31
+
32
+ # To establish a relation, call #inherit_from!
33
+ inheritor = Model.new
34
+ inheritor.inherit_from!(ancestor)
35
+
36
+ # ...or set :ancestor attribute
37
+ inheritor = Model.create(:ancestor => ancestor)
38
+
39
+
40
+ === Mongoid configuration
41
+
42
+ When inheriting, the attribute :_reference_id will be set on embedded documents of inherited objects. So make sure this field is available or Mongoid is configured to allow dynamic fields. Add to config/mongoid.yml:
43
+
44
+ allow_dynamic_fields: true
45
+
46
+
47
+ === Acquired attributes
48
+
49
+ All attributes will be inherited, except these ACQUIRED_ATTRIBUTES:
50
+
51
+ _id
52
+ uuid
53
+ ancestor_uuid
54
+ mutated_attributes
55
+ mutated
56
+ created_at
57
+ updated_at
58
+ version
59
+ versions
60
+
61
+
62
+ === Manage mutations of embedded documents
63
+
64
+ All custom changes on inherited objects will be stored in #mutated_attributes. On embedded documents of inherited objects, however, mutations of attributes will not be tracked. But you may flag a document as mutated when applying custom values:
65
+
66
+ class Job
67
+ include Mongoid::Document
68
+ field :salary
69
+ field :mutated, :type => Boolean
70
+ embedded_in :model, :inverse_of => :jobs
71
+
72
+ def set_custom_salary(amount)
73
+ self.salary = amount
74
+ self.mutated = true
75
+ end
76
+ end
77
+
78
+ To control how inherited data will be updated, you may define a callback method and check #mutated:
79
+
80
+ def update_inherited_attributes(attrs)
81
+ attrs.delete("salary") if mutated? # preserve custom salary
82
+ update_attributes(attrs)
83
+ end
84
+
85
+
86
+ == Copyright
87
+
88
+ Copyright (c) 2010 Andre Pankratz. See LICENSE for details.
89
+
90
+
91
+ == Thank you!
92
+
93
+ The development of this gem was sponsored by Käuferportal: http://www.kaeuferportal.de
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "vidibus-inheritance"
8
+ gem.summary = %Q{Provides inheritance for models.}
9
+ gem.description = %Q{This gem allows inheritance of objects for Rails 3 with Mongoid. It will update all attributes and embedded documents of inheritors when ancestor gets changed.}
10
+ gem.email = "andre@vidibus.com"
11
+ gem.homepage = "http://github.com/vidibus/vidibus-inheritance"
12
+ gem.authors = ["Andre Pankratz"]
13
+ gem.add_development_dependency "rspec", ">= 1.2.9"
14
+ gem.add_development_dependency "relevance-rcov"
15
+ gem.add_development_dependency "mongoid", "= 2.0.0.beta.15"
16
+ gem.add_development_dependency "rr"
17
+ gem.add_dependency "vidibus-core_extensions"
18
+ gem.add_dependency "vidibus-uuid"
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'spec/rake/spectask'
27
+ Spec::Rake::SpecTask.new(:spec) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.spec_files = FileList['spec/**/*_spec.rb']
30
+ end
31
+
32
+ Spec::Rake::SpecTask.new(:rcov) do |t|
33
+ t.spec_files = FileList['spec/vidibus/**/*_spec.rb']
34
+ t.rcov = true
35
+ t.rcov_opts = ['--exclude', '^spec,/gems/']
36
+ end
37
+
38
+ task :spec => :check_dependencies
39
+ task :default => :spec
40
+
41
+ require "rake/rdoctask"
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?("VERSION") ? File.read("VERSION") : ""
44
+ rdoc.rdoc_dir = "rdoc"
45
+ rdoc.title = "vidibus-uuid #{version}"
46
+ rdoc.rdoc_files.include("README*")
47
+ rdoc.rdoc_files.include("lib/**/*.rb")
48
+ rdoc.options << "--charset=utf-8"
49
+ end
data/TODO ADDED
@@ -0,0 +1,5 @@
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
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.6
@@ -0,0 +1,239 @@
1
+ module Vidibus
2
+ module Inheritance
3
+ module Mongoid
4
+ extend ActiveSupport::Concern
5
+
6
+ ACQUIRED_ATTRIBUTES = %w[_id uuid ancestor_uuid mutated_attributes mutated created_at updated_at version versions]
7
+
8
+ included do
9
+ attr_accessor :inherited_attributes, :_inherited
10
+ attr_protected :mutated_attributes, :mutated
11
+
12
+ field :ancestor_uuid
13
+ field :mutated_attributes, :type => Array, :default => []
14
+ field :mutated, :type => Boolean
15
+
16
+ validates :ancestor_uuid, :uuid => { :allow_blank => true }
17
+ validates :ancestor, :ancestor => true, :if => :ancestor_uuid?
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
31
+ end
32
+
33
+ # Returns ancestor object by uuid.
34
+ def ancestor
35
+ @ancestor ||= begin
36
+ self.class.where(:uuid => ancestor_uuid).first if ancestor_uuid
37
+ end
38
+ 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
48
+ #
49
+ def inherit!(options = {})
50
+ self.inherit_attributes(options)
51
+ self.save!
52
+ 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)
59
+ end
60
+
61
+ # Returns list of objects that inherit from this ancestor.
62
+ def inheritors
63
+ self.class.where(:ancestor_uuid => uuid).to_a
64
+ end
65
+
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
77
+
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)
92
+ end
93
+
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)
123
+ end
124
+
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
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # Performs actions before saving.
142
+ def preprocess
143
+ track_mutations
144
+ inherit_attributes if inherit?
145
+ end
146
+
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
157
+
158
+ # Returns true if this documents has any inheritable documents.
159
+ def embed?
160
+ inheritable_documents.any?
161
+ end
162
+
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
201
+ 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
211
+ end
212
+
213
+ # Applies changes to inheritors.
214
+ def update_inheritors
215
+ return unless inheritors.any?
216
+ inheritors.each(&:inherit!)
217
+ end
218
+
219
+ class << self
220
+
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
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,13 @@
1
+ module Vidibus
2
+ module Inheritance
3
+ module Validators
4
+ class AncestorValidator < ActiveModel::EachValidator
5
+ def validate_each(record, attribute, value)
6
+ unless value.is_a?(record.class)
7
+ record.errors[attribute] << "must be a #{record.class}"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ require "inheritance/validators/ancestor_validator"
2
+
3
+ # Add AncestorValidator
4
+ ActiveModel::Validations.send(:include, Vidibus::Inheritance::Validators)
@@ -0,0 +1,8 @@
1
+ require "inheritance/mongoid"
2
+ require "inheritance/validators"
3
+
4
+ module Vidibus
5
+ module Inheritance
6
+ class SomeError < StandardError; end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib", "vidibus"))
2
+ require "vidibus-core_extensions"
3
+ require "inheritance"
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
@@ -0,0 +1,32 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
3
+
4
+ require "rubygems"
5
+ require "active_support/core_ext"
6
+ require "spec"
7
+ require "mongoid"
8
+ require "vidibus-uuid"
9
+ require "vidibus-inheritance"
10
+ require "rr"
11
+
12
+ Mongoid.configure do |config|
13
+ name = "vidibus-inheritance_test"
14
+ host = "localhost"
15
+ config.master = Mongo::Connection.new.db(name)
16
+ end
17
+
18
+ Spec::Runner.configure do |config|
19
+ config.mock_with RR::Adapters::Rspec
20
+ config.before(:each) do
21
+ Mongoid.master.collections.select { |c| c.name != "system.indexes" }.each(&:drop)
22
+ end
23
+ end
24
+
25
+ # Helper for stubbing time. Define String to be set as Time.now.
26
+ # example: stub_time!('01.01.2010 14:00')
27
+ # example: stub_time!(2.days.ago)
28
+ def stub_time!(string = nil)
29
+ now = string ? Time.parse(string.to_s) : Time.now
30
+ stub(Time).now { now }
31
+ now
32
+ end