graph_mediator 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,4 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,26 @@
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
+ .current
16
+ .vimrc
17
+
18
+ ## PROJECT::GENERAL
19
+ coverage
20
+ rdoc
21
+ pkg
22
+ *.gem
23
+
24
+ ## PROJECT::SPECIFIC
25
+ *.log
26
+ thread-test
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Josh Partlow
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,136 @@
1
+ = Graph Mediator
2
+
3
+ GraphMediator is used to help coordinate state between a graph of ActiveRecord
4
+ objects related to a single root node. Its role is assisting in cases where
5
+ you are representing a complex concept as a graph of related objects with
6
+ potentially circular interdependencies. Changing attributes in one object
7
+ might require adding or removing of dependent objects. Adding these objects
8
+ might necessitate a recalculation of memberships in a join table. Any such
9
+ changes might require that cached calculations be redone. Touching any object
10
+ in the graph might require a version bump for the concept of the graph as a
11
+ whole.
12
+
13
+ We want changes to be made once, in a single transaction, with a single overall
14
+ version change. The version change should be guarded by an optimistic lock
15
+ check to avoid conflicts between two processes updates to the same graph.
16
+
17
+ To make interdependent state changes manageable, GraphMediator wraps an
18
+ additional layer of callbacks around the ActiveRecord save cycle to ensure
19
+ that a save occurs within a GraphMediator.mediated_transaction.
20
+
21
+ * :before_mediation
22
+ * * save *
23
+ * :after_mediation
24
+
25
+ The after_mediation callback is itself broken down into three phases:
26
+
27
+ * :reconciliation - in this phase, any methods which bring the overall state
28
+ of the graph into balance should be run to adjust for changes made during the
29
+ save.
30
+ * :cacheing - any calculations which rely on the state of a reconciled graph
31
+ but which do not themselves alter the graph (in that they are reproducible
32
+ from existing state) should be made in the cacheing phase.
33
+ * :bumping - if the class has a +lock_column+ set
34
+ (ActiveRecord::Locking::Optimistic) and has on +updated_at/on+ timestamp then
35
+ the instance will be touched, bumping the +lock_column+ and checking for stale
36
+ data.
37
+
38
+ During a mediated_transaction, the +lock_column+ will only update during the
39
+ +bumping+ phase of the after_mediation callback.
40
+
41
+ But if there is no +update_at/on+ timestamp, then +lock_column+ cannot be
42
+ incremented when dependent objects are updated. This is because there is
43
+ nothing to touch on the root record to trigger the +lock_column+ update.
44
+
45
+ GraphMediator ensures that after_mediation is run only once within the context
46
+ of a mediated transaction. If the block being mediated returns false, the
47
+ after_mediation is skipped; this allows for validations.
48
+
49
+ == Usage
50
+
51
+ # * :pen_number
52
+ # * :dingo_count
53
+ # * :biscuit_count
54
+ # * :feed_rate
55
+ # * :total_biscuit_weight
56
+ # * :lock_version, :default => 0 # required for versioning
57
+ # * :updated_at # required for versioning
58
+ class DingoPen < ActiveRecord::Base
59
+
60
+ has_many :dingos
61
+ has_many :biscuits
62
+
63
+ include GraphMediator
64
+ mediate :purchase_biscuits,
65
+ :dependencies => [Dingo, Biscuit],
66
+ :when_reconciling => [:adjust_biscuit_supply, :feed_dingos],
67
+ :when_cacheing => :calculate_total_biscuit_weight
68
+
69
+ or
70
+
71
+ mediate :purchase_biscuits,
72
+ :dependencies => [Dingo, Biscuit], # ensures a mediated_transaction on Dingo#save or Biscuit#save
73
+ mediate_reconciles :adjust_biscuit_supply, :feed_dingos
74
+ mediate_caches do |instance|
75
+ instance.calculate_total_biscuit_weight
76
+ end
77
+
78
+ ...
79
+
80
+ def purchase_biscuits; ... end
81
+ def adjust_biscuit_supply; ... end
82
+ def feed_dingos; ... end
83
+ def calculate_total_biscuit_weight; ... end
84
+ end
85
+
86
+ See spec/examples for real, dingo-free examples.
87
+
88
+ == Caveats
89
+
90
+ A lock_column and timestamp are not required, but without both columns in your schema
91
+ there will be no versioning.
92
+
93
+ +A lock_column by itself *without* a timestamp will not increment and will not provide
94
+ any optimistic locking in a class including GraphMediator!+
95
+
96
+ Using a lock_column along with a counter_cache in a dependent child will raise a StaleObject
97
+ error during a mediated_transaction if you touch the dependent.
98
+
99
+ The cache_counters do not play well with optimistic locking because they are updated with
100
+ a direct SQL call to the database, so ActiveRecord instance remain unaware of the lock_version
101
+ change and assume it came from another transaction.
102
+
103
+ You should not need to declare lock_version for any children that are declared as a dependency
104
+ of the root node, since updates will also update the root nodes lock_version. So if another
105
+ transaction updates a child, root.lock_version should increment, and the first transaction
106
+ should raise a StaleObject error when it too tries to update the child.
107
+
108
+ If you override super in the model hierarchy you are mediating, you must pass your
109
+ override as a block to super or it will occur outside of mediation:
110
+
111
+ def save
112
+ super do
113
+ my_local_changes
114
+ end
115
+ end
116
+
117
+ You are probably better off hooking to before_save or after_save if they
118
+ suffice.
119
+
120
+ == Threads
121
+
122
+ GraphMediator uses thread local variables to keep track of open mediators.
123
+ It should be thread safe but this needs testing.
124
+
125
+ == Advice
126
+
127
+ Build a simple system first, rather than building a system to use GraphMediator.
128
+
129
+ But if you have a web of observers/callbacks struggling to maintain state,
130
+ repeated, redundant update calls from observed changes in collection members,
131
+ or are running into +lock_column+ issues within your own updates, then
132
+ GraphMediator may help.
133
+
134
+ == Copyright
135
+
136
+ Copyright (c) 2010 Josh Partlow. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'rubygems'
5
+ require 'rake'
6
+ require 'graph_mediator/version'
7
+
8
+ require 'spec/rake/spectask'
9
+ Spec::Rake::SpecTask.new(:spec) do |spec|
10
+ spec.libs << 'lib' << 'spec'
11
+ spec.spec_files = FileList['spec/**/*_spec.rb']
12
+ end
13
+
14
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
15
+ spec.libs << 'lib' << 'spec'
16
+ spec.pattern = 'spec/**/*_spec.rb'
17
+ spec.rcov = true
18
+ end
19
+
20
+ task :default => :spec
21
+
22
+ require 'rake/rdoctask'
23
+ Rake::RDocTask.new do |rdoc|
24
+ version = GraphMediator::VERSION
25
+
26
+ rdoc.rdoc_dir = 'rdoc'
27
+ rdoc.title = "graph_mediator #{version}"
28
+ rdoc.main = 'README.rdoc'
29
+ rdoc.rdoc_files.include('README*')
30
+ rdoc.rdoc_files.include('LICENSE*')
31
+ rdoc.rdoc_files.include('lib/**/*.rb')
32
+ end
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path('../lib/', __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require 'graph_mediator/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{graph_mediator}
8
+ s.version = GraphMediator::VERSION
9
+ s.required_rubygems_version = ">= 1.3.6"
10
+
11
+ s.authors = ["Josh Partlow"]
12
+ s.email = %q{jpartlow@glatisant.org}
13
+ s.summary = %q{Mediates ActiveRecord state changes}
14
+ s.description = %q{Mediates state changes between a set of interdependent ActiveRecord objects.}
15
+ s.homepage = %q{http://github.com/jpartlow/graph_mediator}
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README.rdoc"
19
+ ]
20
+ s.rdoc_options = ["--main=README.rdoc", "--charset=UTF-8"]
21
+ s.require_paths = ["lib"]
22
+ s.rubygems_version = %q{1.3.6}
23
+ s.files = `git ls-files`.split("\n")
24
+ s.test_files = `git ls-files spec/*`.split("\n")
25
+
26
+ s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
27
+ s.add_runtime_dependency(%q<activerecord>, ["= 2.3.5"])
28
+ s.add_runtime_dependency(%q<activesupport>, ["= 2.3.5"])
29
+ s.add_runtime_dependency(%q<aasm>, [">= 2.2.0"])
30
+ end
31
+
@@ -0,0 +1,509 @@
1
+ require 'active_support'
2
+ require 'graph_mediator/mediator'
3
+ require 'graph_mediator/locking'
4
+ require 'graph_mediator/version'
5
+
6
+ # = GraphMediator =
7
+ #
8
+ # GraphMediator is used to coordinate changes between a graph of ActiveRecord objects
9
+ # related to a root node. See README.rdoc for details.
10
+ #
11
+ # GraphMediator::Base::DSL - is the simple class macro language used to set up mediation.
12
+ #
13
+ # == Versioning and Optimistic Locking
14
+ #
15
+ # If you include an integer +lock_version+ column in your class, it will be incremented
16
+ # only once within a mediated_transaction and will serve as the optimistic locking check
17
+ # for the entire graph so long as you have declared all your dependent models for mediation.
18
+ #
19
+ # Outside of a mediated_transaction, +lock_version+ will increment per update as usual.
20
+ #
21
+ # == Convenience Methods for Save Without Mediation
22
+ #
23
+ # There are convenience method to perform a save, save!, toggle,
24
+ # toggle!, update_attribute, update_attributes or update_attributes!
25
+ # call without mediation. They are of the form <method>_without_mediation<punc>
26
+ #
27
+ # For example, save_without_mediation! is equivalent to:
28
+ #
29
+ # instance.disable_mediation!
30
+ # instance.save!
31
+ # instance.enable_mediation!
32
+ #
33
+ # == Overriding
34
+ #
35
+ # GraphMediator overrides ActiveRecord's save_without_transaction to slip in mediation
36
+ # just before the save process is wrapped in a transaction.
37
+ #
38
+ # * save_without_transaction
39
+ # * save_without_transaction_with_mediation
40
+ # * save_without_transaction_without_mediation
41
+ #
42
+ # may all be overridden in your implementation class, but they end up being
43
+ # defined locally by GraphMediator, so you can override with something like
44
+ # alias_method_chain, but will need to be in a subclass to use super.
45
+ #
46
+ # My original intention was to define aliased overrides in MediatorProxy if the target
47
+ # was a method in a superclass (like save), so that the implementation class could
48
+ # make a simple def foo; something; super; end override, but this is prevented by a bug
49
+ # in ruby 1.8 with aliasing of methods that use super in a module.
50
+ # http://redmine.ruby-lang.org/issues/show/734
51
+ #
52
+ module GraphMediator
53
+
54
+ CALLBACKS = [:before_mediation, :mediate_reconciles, :mediate_caches, :mediate_bumps]
55
+ SAVE_METHODS = [:save_without_transactions, :save_without_transactions!]
56
+
57
+ # We want lib/graph_mediator to define GraphMediator constant
58
+ require 'graph_mediator/mediator'
59
+
60
+ class MediatorException < Exception; end
61
+
62
+ # Methods used by GraphMediator to setup.
63
+ class << self
64
+ def included(base)
65
+ base.class_eval do
66
+ extend DSL
67
+ end
68
+ initialize_for_mediation(base)
69
+ end
70
+
71
+ private
72
+
73
+ def initialize_for_mediation(base)
74
+ _include_new_proxy(base)
75
+ base.class_inheritable_accessor :__graph_mediator_enabled, :instance_writer => false
76
+ base.__graph_mediator_enabled = true
77
+ base.__send__(:class_inheritable_array, :graph_mediator_dependencies)
78
+ base.graph_mediator_dependencies = []
79
+ base.__send__(:_register_for_mediation, *(SAVE_METHODS.clone << { :track_changes => true }))
80
+ end
81
+
82
+ # Inserts a new #{base}::MediatorProxy module with Proxy included.
83
+ # All callbacks are defined in here for easy overriding in the Base
84
+ # class.
85
+ def _include_new_proxy(base)
86
+ # XXX How can _include_new_proxy be made cleaner or at least clearer?
87
+ proxy = Module.new do
88
+ # include ActiveSupport::Callbacks
89
+ include Proxy
90
+ mattr_accessor :_graph_mediator_logger
91
+ mattr_accessor :_graph_mediator_log_level
92
+ end
93
+ base.const_set(:MediatorProxy, proxy)
94
+ proxy._graph_mediator_logger = GraphMediator::Configuration.logger || base.logger
95
+ proxy._graph_mediator_log_level = GraphMediator::Configuration.log_level
96
+
97
+ base.send(:include, proxy)
98
+ base.send(:extend, Proxy::ClassMethods)
99
+ base.send(:include, Locking)
100
+
101
+ key = base.to_s.underscore.gsub('/','_').upcase
102
+ hash_key = "GRAPH_MEDIATOR_#{key}_HASH_KEY"
103
+ new_array_key = "GRAPH_MEDIATOR_#{key}_NEW_ARRAY_KEY"
104
+ eigen = base.instance_eval { class << self; self; end }
105
+ eigen.class_eval do
106
+ define_method(:mediator_hash_key) { hash_key }
107
+ define_method(:mediator_new_array_key) { new_array_key }
108
+ end
109
+
110
+ # Relies on ActiveSupport::Callbacks (which is included
111
+ # into ActiveRecord::Base) for callback handling.
112
+ base.define_callbacks *CALLBACKS
113
+ return proxy
114
+ end
115
+
116
+ end
117
+
118
+ module Configuration
119
+ # Enable or disable mediation globally. Default: true
120
+ # TODO this doesn't effect anything yet
121
+ mattr_accessor :enable_mediation
122
+ self.enable_mediation = true
123
+
124
+ # Global logger override for GraphMediator. By default each class
125
+ # including GraphMediator uses the class's ActiveRecord logger. Setting
126
+ # GraphMediator::Configuration.logger overrides this.
127
+ mattr_accessor :logger
128
+
129
+ # Log level may be adjusted just for GraphMediator globally, or for each class including
130
+ # GraphMediator. This should be an ActiveSupport::BufferedLogger log level constant
131
+ # such as ActiveSupport::BufferedLogger::DEBUG
132
+ mattr_accessor :log_level
133
+ self.log_level = ActiveSupport::BufferedLogger::INFO
134
+ end
135
+
136
+ module Util
137
+ # Returns an array of [<method>,<punctuation>] from a given method symbol.
138
+ #
139
+ # parse_method_punctuation(:save) => ['save',nil]
140
+ # parse_method_punctuation(:save!) => ['save','!']
141
+ def parse_method_punctuation(method)
142
+ return method.to_s.sub(/([?!=])$/, ''), $1
143
+ end
144
+ end
145
+
146
+ # All of the working methods for mediation, plus initial call backs.
147
+ module Proxy
148
+ extend Util
149
+
150
+ module ClassMethods
151
+ # Turn on mediation for all instances of this class. (On by default)
152
+ def enable_all_mediation!
153
+ self.__graph_mediator_enabled = true
154
+ end
155
+
156
+ # Turn off mediation for all instances of this class. (Off by default)
157
+ #
158
+ # This will cause new mediators to start up disabled, but existing
159
+ # mediators will finish normally.
160
+ def disable_all_mediation!
161
+ self.__graph_mediator_enabled = false
162
+ end
163
+
164
+ # True if mediation is enabled at the class level.
165
+ def mediation_enabled?
166
+ self.__graph_mediator_enabled
167
+ end
168
+
169
+ # True if we are currently mediating instances of any of the passed ids.
170
+ def currently_mediating?(ids)
171
+ Array(ids).detect do |id|
172
+ mediators[id] || mediators_for_new_records.find { |m| m.mediated_id == id }
173
+ end
174
+ end
175
+
176
+ # Unique key to access a thread local hash of mediators for specific
177
+ # #{base}::MediatorProxy type.
178
+ #
179
+ # (This is overwritten by GraphMediator._include_new_proxy)
180
+ def mediator_hash_key; end
181
+
182
+ # Unique key to access a thread local array of mediators of new records for
183
+ # specific #{base}::MediatorProxy type.
184
+ #
185
+ # (This is overwritten by GraphMediator._include_new_proxy)
186
+ def mediator_new_array_key; end
187
+
188
+ # The hash of Mediator instances active in this Thread for the Proxy's
189
+ # base class.
190
+ #
191
+ # instance.id => Mediator of (instance)
192
+ #
193
+ def mediators
194
+ unless Thread.current[mediator_hash_key]
195
+ Thread.current[mediator_hash_key] = {}
196
+ end
197
+ Thread.current[mediator_hash_key]
198
+ end
199
+
200
+ # An array of Mediator instances mediating new records in this Thread for
201
+ # the Proxy's base class.
202
+ def mediators_for_new_records
203
+ unless Thread.current[mediator_new_array_key]
204
+ Thread.current[mediator_new_array_key] = []
205
+ end
206
+ Thread.current[mediator_new_array_key]
207
+ end
208
+
209
+ end
210
+
211
+ # Wraps the given block in a transaction and begins mediation.
212
+ def mediated_transaction(&block)
213
+ m_debug("#{self}.mediated_transaction called")
214
+ mediator = _get_mediator
215
+ result = mediator.mediate(&block)
216
+ m_debug("#{self}.mediated_transaction completed successfully")
217
+ return result
218
+ ensure
219
+ if mediator && mediator.idle?
220
+ mediators.delete(self.id)
221
+ mediators_for_new_records.delete(mediator)
222
+ end
223
+ end
224
+
225
+ # True if there is currently a mediated transaction begun for
226
+ # this instance.
227
+ def currently_mediating?
228
+ !current_mediator.nil?
229
+ end
230
+
231
+ # Returns the state of the current_mediator or nil.
232
+ def current_mediation_phase
233
+ current_mediator.try(:aasm_current_state)
234
+ end
235
+
236
+ # Returns the hash of changes to the graph being tracked by the current
237
+ # mediator or nil if not currently mediating.
238
+ def mediated_changes
239
+ current_mediator.try(:changes)
240
+ end
241
+
242
+ # Turn off mediation for this instance. If currently mediating, it
243
+ # will finish normally, but new mediators will start disabled.
244
+ def disable_mediation!
245
+ @graph_mediator_mediation_disabled = true
246
+ end
247
+
248
+ # Turn on mediation for this instance (on by default).
249
+ def enable_mediation!
250
+ @graph_mediator_mediation_disabled = false
251
+ end
252
+
253
+ # By default, every instance will be mediated and this will return true.
254
+ # You can turn mediation on or off on an instance by instance basis with
255
+ # calls to disable_mediation! or enable_mediation!.
256
+ #
257
+ # Mediation may also be disabled at the class level, but enabling or
258
+ # disabling an instance supercedes this.
259
+ def mediation_enabled?
260
+ enabled = @graph_mediator_mediation_disabled.nil? ?
261
+ self.class.mediation_enabled? :
262
+ !@graph_mediator_mediation_disabled
263
+ end
264
+
265
+ %w(save save! touch toggle toggle! update_attribute update_attributes update_attributes!).each do |method|
266
+ base, punctuation = parse_method_punctuation(method)
267
+ define_method("#{base}_without_mediation#{punctuation}") do |*args,&block|
268
+ disable_mediation!
269
+ send(method, *args, &block)
270
+ enable_mediation!
271
+ end
272
+ end
273
+
274
+ [:debug, :info, :warn, :error, :fatal].each do |level|
275
+ const = ActiveSupport::BufferedLogger.const_get(level.to_s.upcase)
276
+ define_method("m_#{level}") do |message|
277
+ _graph_mediator_logger.send(level, message) if _graph_mediator_log_level <= const
278
+ end
279
+ end
280
+
281
+ protected
282
+
283
+ def mediators
284
+ self.class.mediators
285
+ end
286
+
287
+ def mediators_for_new_records
288
+ self.class.mediators_for_new_records
289
+ end
290
+
291
+ # Accessor for the mediator associated with this instance's id, or nil if we are
292
+ # not currently mediating.
293
+ def current_mediator
294
+ m_debug("#{self}.current_mediator called")
295
+ mediator = mediators[self.id]
296
+ mediator ||= mediators_for_new_records.find { |m| m.mediated_instance.equal?(self) || m.mediated_id == self.id }
297
+ m_debug("#{self}.current_mediator found #{mediator || 'nothing'}")
298
+ return mediator
299
+ end
300
+
301
+ private
302
+
303
+ # Gets the current mediator or initializes a new one.
304
+ def _get_mediator
305
+ m_debug("#{self}._get_mediator called")
306
+ m_debug("#{self}.get_mediator in a new record") if new_record?
307
+ unless mediator = current_mediator
308
+ mediator = GraphMediator::Mediator.new(self)
309
+ m_debug("#{self}.get_mediator created new mediator")
310
+ new_record? ?
311
+ mediators_for_new_records << mediator :
312
+ mediators[self.id] = mediator
313
+ end
314
+ m_debug("#{self}._get_mediator obtained #{mediator}")
315
+ return mediator
316
+ end
317
+
318
+ end
319
+
320
+ module AliasExtension #:nodoc:
321
+ include Util
322
+
323
+ private
324
+
325
+ # Wraps each method in a mediated_transaction call.
326
+ # The original method is aliased as :method_without_mediation so that it can be
327
+ # overridden separately if needed.
328
+ #
329
+ # * options:
330
+ # * :through => root node accessor that will be the target of the
331
+ # mediated_transaction. By default self is assumed.
332
+ # * :track_changes => if true, the mediator will track changes such
333
+ # that they can be reviewed after_mediation. The after_mediation
334
+ # callbacks occur after dirty has completed and changes are normally lost.
335
+ # False by default. Normally only applied to save and destroy methods.
336
+ def _register_for_mediation(*methods)
337
+ options = methods.extract_options!
338
+ root_node_accessor = options[:through]
339
+ track_changes = options[:track_changes]
340
+ methods.each do |method|
341
+ saveing = method.to_s =~ /save/
342
+ destroying = method.to_s =~ /destroy/
343
+ _alias_method_chain_ensuring_inheritability(method, :mediation) do |aliased_target,punctuation|
344
+ __send__(:define_method, "#{aliased_target}_with_mediation#{punctuation}") do |*args, &block|
345
+ root_node = (root_node_accessor ? send(root_node_accessor) : self)
346
+ unless root_node.nil?
347
+ root_node.mediated_transaction do |mediator|
348
+ mediator.debug("#{root_node} mediating #{aliased_target}#{punctuation} for #{self}")
349
+ mediator.track_changes_for(self) if track_changes && saveing
350
+ result = __send__("#{aliased_target}_without_mediation#{punctuation}", *args, &block)
351
+ mediator.track_changes_for(self) if track_changes && destroying
352
+ mediator.debug("#{root_node} done mediating #{aliased_target}#{punctuation} for #{self}")
353
+ result
354
+ end
355
+ else
356
+ __send__("#{aliased_target}_without_mediation#{punctuation}", *args, &block)
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
362
+
363
+ def _method_defined(method, anywhere = true)
364
+ (instance_methods(anywhere) + private_instance_methods(anywhere)).include?(RUBY_VERSION < '1.9' ? method.to_s : method)
365
+ end
366
+
367
+ # This uses Tammo Freese's patch to alias_method_chain.
368
+ # https://rails.lighthouseapp.com/projects/8994/tickets/285-alias_method_chain-limits-extensibility
369
+ #
370
+ # target, target_with_mediation, target_without_mediation should all be
371
+ # available for decorating (via aliasing) in the base class including the
372
+ # MediatorProxy, as well as in it's subclasses (via aliasing or direct
373
+ # overriding). Overrides made higher up the chain should flow through as
374
+ # well
375
+ #
376
+ # If the target has not been defined yet, there's nothing we can do, and we
377
+ # raise a MediatorException
378
+ def _alias_method_chain_ensuring_inheritability(target, feature, &block)
379
+ raise(MediatorException, "Method #{target} has not been defined yet.") unless _method_defined(target)
380
+
381
+ # Strip out punctuation on predicates or bang methods since
382
+ # e.g. target?_without_feature is not a valid method name.
383
+ aliased_target, punctuation = parse_method_punctuation(target)
384
+ with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
385
+
386
+ method_defined_here = _method_defined(target, false)
387
+ unless method_defined_here
388
+ module_eval do
389
+ define_method(target) do |*args, &block|
390
+ super
391
+ end
392
+ end
393
+ end
394
+
395
+ __send__(:alias_method, without_method, target)
396
+
397
+ if block_given?
398
+ # create with_method
399
+ yield(aliased_target, punctuation)
400
+ end
401
+
402
+ target_method_exists = _method_defined(with_method)
403
+ raise NameError unless target_method_exists
404
+
405
+ module_eval do
406
+ define_method(target) do |*args, &block|
407
+ __send__(with_method, *args, &block)
408
+ end
409
+ end
410
+ end
411
+
412
+ end
413
+
414
+ # DSL for setting up and describing mediation.
415
+ #
416
+ # save and save! are automatically wrapped for mediation when GraphMediator
417
+ # is included into your class. You can mediate other methods with a call to
418
+ # mediate(), and can setup callbacks for reconcilation, cacheing or version
419
+ # bumping.
420
+ #
421
+ # = Callbacks
422
+ #
423
+ # The mediate() method takes options to set callbacks. Or you can set them
424
+ # directly with a method symbol, array of method symbols or a Proc. They may
425
+ # be called multiple times and may be added to in subclasses.
426
+ #
427
+ # * before_mediation - runs before mediation is begun
428
+ # * - mediate and save
429
+ # * mediate_reconciles - after saveing the instance, run any routines to make further
430
+ # adjustments to the structure of the graph or non-cache attributes
431
+ # * mediate_caches - routines for updating cache values
432
+ #
433
+ # Example:
434
+ #
435
+ # mediate_reconciles :bar do |instance|
436
+ # instance.something_else
437
+ # end
438
+ # mediate_reconciles :baz
439
+ #
440
+ # will ensure that [:bar, <block>, :baz] are run in
441
+ # sequence after :foo is done saveing within the context of a mediated
442
+ # transaction.
443
+ #
444
+ module DSL
445
+ include AliasExtension
446
+
447
+ # Establishes callbacks, dependencies and possible methods as entry points
448
+ # for mediation.
449
+ #
450
+ # * :methods => list of methods to mediate (automatically wrap in a
451
+ # mediated_transaction call)
452
+ #
453
+ # ActiveRecord::Base.save is decorated for mediation when GraphMediator
454
+ # is included into your model. If you have additional methods which
455
+ # perform bulk operations on members, you probably want to list them
456
+ # here so that they are mediated as well.
457
+ #
458
+ # You should not list methods used for reconcilation, or cacheing.
459
+ #
460
+ # This macro takes a number of options:
461
+ #
462
+ # * :options => hash of options
463
+ # * :dependencies => list of dependent member classes whose save methods
464
+ # should be decorated for mediation as well.
465
+ # * :when_reconciling => list of methods to execute during the after_mediation
466
+ # reconcilation phase
467
+ # * :when_cacheing => list of methods to execute during the after_mediation
468
+ # cacheing phase
469
+ #
470
+ # mediate :update_children,
471
+ # :dependencies => Child,
472
+ # :when_reconciling => :reconcile,
473
+ # :when_caching => :cache
474
+ #
475
+ # = Dependent Classes
476
+ #
477
+ # Dependent classes have their save methods mediated as well. However, a
478
+ # dependent class must provide an accessor for the root node, so that a
479
+ # mediated_transaction can be begun in the root node when a dependent is
480
+ # changed.
481
+ #
482
+ # = Versioning and Optimistic Locking
483
+ #
484
+ # GraphMediator uses the class's lock_column (default +lock_version+) and
485
+ # +updated_at+ or +updated_on+ for versioning and locks checks during
486
+ # mediation. The lock_column is incremented only once during a mediated_transaction.
487
+ #
488
+ # +Unless both these columns are present in the schema, versioning/locking
489
+ # will not happen.+ A lock_column by itself will not be updated unless
490
+ # there is an updated_at/on timestamp available to touch.
491
+ #
492
+ def mediate(*methods)
493
+ options = methods.extract_options!
494
+ self.graph_mediator_dependencies = Array(options[:dependencies] || [])
495
+
496
+ _register_for_mediation(*methods)
497
+ graph_mediator_dependencies.each do |dependent_class|
498
+ dependent_class.send(:extend, AliasExtension) unless dependent_class.include?(AliasExtension)
499
+ methods = SAVE_METHODS.clone
500
+ methods << :destroy
501
+ methods << { :through => self.class_of_active_record_descendant(self).to_s.demodulize.underscore, :track_changes => true }
502
+ dependent_class.send(:_register_for_mediation, *methods)
503
+ end
504
+ mediate_reconciles(options[:when_reconciling]) if options[:when_reconciling]
505
+ mediate_caches(options[:when_cacheing]) if options[:when_cacheing]
506
+ end
507
+
508
+ end
509
+ end