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 +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +93 -0
- data/Rakefile +49 -0
- data/TODO +5 -0
- data/VERSION +1 -0
- data/lib/vidibus/inheritance/mongoid.rb +239 -0
- data/lib/vidibus/inheritance/validators/ancestor_validator.rb +13 -0
- data/lib/vidibus/inheritance/validators.rb +4 -0
- data/lib/vidibus/inheritance.rb +8 -0
- data/lib/vidibus-inheritance.rb +3 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/vidibus/inheritance/mongoid_spec.rb +600 -0
- data/spec/vidibus/inheritance/validators/ancestor_validator_spec.rb +28 -0
- data/vidibus-inheritance.gemspec +78 -0
- metadata +176 -0
data/.document
ADDED
data/.gitignore
ADDED
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
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
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|