graph_mediator 0.2.1

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