vidibus-inheritance 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
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