state_machine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +101 -0
- data/MIT-LICENSE +20 -0
- data/README +97 -0
- data/Rakefile +79 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +92 -0
- data/lib/state_machine/event.rb +127 -0
- data/lib/state_machine/machine.rb +141 -0
- data/lib/state_machine/transition.rb +75 -0
- data/test/app_root/app/models/auto_shop.rb +34 -0
- data/test/app_root/app/models/car.rb +19 -0
- data/test/app_root/app/models/highway.rb +3 -0
- data/test/app_root/app/models/motorcycle.rb +3 -0
- data/test/app_root/app/models/switch.rb +12 -0
- data/test/app_root/app/models/toggle_switch.rb +2 -0
- data/test/app_root/app/models/vehicle.rb +71 -0
- data/test/app_root/db/migrate/001_create_switches.rb +12 -0
- data/test/app_root/db/migrate/002_create_auto_shops.rb +13 -0
- data/test/app_root/db/migrate/003_create_highways.rb +11 -0
- data/test/app_root/db/migrate/004_create_vehicles.rb +16 -0
- data/test/factory.rb +61 -0
- data/test/functional/state_machine_test.rb +443 -0
- data/test/test_helper.rb +13 -0
- data/test/unit/event_test.rb +234 -0
- data/test/unit/machine_test.rb +152 -0
- data/test/unit/state_machine_test.rb +102 -0
- data/test/unit/transition_test.rb +298 -0
- metadata +91 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
*SVN*
|
2
|
+
|
3
|
+
*0.1.0* (May 5th, 2008)
|
4
|
+
|
5
|
+
* Completely rewritten from scratch
|
6
|
+
|
7
|
+
* Renamed to state_machine
|
8
|
+
|
9
|
+
* Removed database dependencies
|
10
|
+
|
11
|
+
* Removed models in favor of an attribute-agnostic design
|
12
|
+
|
13
|
+
* Use ActiveSupport::Callbacks instead of eval_call
|
14
|
+
|
15
|
+
* Remove dry_transaction_rollbacks dependencies
|
16
|
+
|
17
|
+
* Added functional tests
|
18
|
+
|
19
|
+
* Updated documentation
|
20
|
+
|
21
|
+
*0.0.1* (September 26th, 2007)
|
22
|
+
|
23
|
+
* Add dependency on custom_callbacks
|
24
|
+
|
25
|
+
* Move test fixtures out of the test application root directory
|
26
|
+
|
27
|
+
* Improve documentation
|
28
|
+
|
29
|
+
* Remove the StateExtension module in favor of adding singleton methods to the stateful class
|
30
|
+
|
31
|
+
* Convert dos newlines to unix newlines
|
32
|
+
|
33
|
+
* Fix error message when a given event can't be found in the database
|
34
|
+
|
35
|
+
* Add before_#{action} and #{action} callbacks when an event is performed
|
36
|
+
|
37
|
+
* All state and event callbacks can now explicitly return false in order to cancel the action
|
38
|
+
|
39
|
+
* Refactor ActiveState callback creation
|
40
|
+
|
41
|
+
* Refactor unit tests so that they use mock classes instead of themselves
|
42
|
+
|
43
|
+
* Allow force_reload option to be set in the state association
|
44
|
+
|
45
|
+
* Don't save the entire model when updating the state_id
|
46
|
+
|
47
|
+
* Raise exception if a class tries to define a state more than once
|
48
|
+
|
49
|
+
* Add tests for PluginAWeek::Has::States::ActiveState
|
50
|
+
|
51
|
+
* Refactor active state/active event creation
|
52
|
+
|
53
|
+
* Fix owner_type not being set correctly in active states/events of subclasses
|
54
|
+
|
55
|
+
* Allow subclasses to override the initial state
|
56
|
+
|
57
|
+
* Fix problem with migrations using default null when column cannot be null
|
58
|
+
|
59
|
+
* Moved deadline support into a separate plugin (has_state_deadlines).
|
60
|
+
|
61
|
+
* Added many more unit tests.
|
62
|
+
|
63
|
+
* Simplified many of the interfaces for maintainability.
|
64
|
+
|
65
|
+
* Added support for turning off recording state changes.
|
66
|
+
|
67
|
+
* Removed the short_description and long_description columns, in favor of an optional human_name column.
|
68
|
+
|
69
|
+
* Fixed not overriding the correct equality methods in the StateTransition class.
|
70
|
+
|
71
|
+
* Added to_sym to State and Event.
|
72
|
+
|
73
|
+
* State#name and Event#name now return the string version of the name instead of the symbol version.
|
74
|
+
|
75
|
+
* Added State#human_name and Event#human_name to automatically figure out what the human name is if it isn't specified in the table.
|
76
|
+
|
77
|
+
* Updated manual rollbacks to use the new Rails edge api (ActiveRecord::Rollback exception).
|
78
|
+
|
79
|
+
* Moved StateExtension class into a separate file in order to help keep the has_state files clean.
|
80
|
+
|
81
|
+
* Renamed InvalidState and InvalidEvent exceptions to StateNotFound and EventNotFound in order to follow the ActiveRecord convention (i.e. RecordNotFound).
|
82
|
+
|
83
|
+
* Added StateNotActive and EventNotActive exceptions to help differentiate between states which don't exist and states which weren't defined in the class.
|
84
|
+
|
85
|
+
* Added support for defining callbacks like so:
|
86
|
+
|
87
|
+
def before_exit_parked
|
88
|
+
end
|
89
|
+
|
90
|
+
def after_enter_idling
|
91
|
+
end
|
92
|
+
|
93
|
+
* Added support for defining callbacks using class methods:
|
94
|
+
|
95
|
+
before_exit_parked :fasten_seatbelt
|
96
|
+
|
97
|
+
* Added event callbacks after the transition has occurred (e.g. after_park)
|
98
|
+
|
99
|
+
* State callbacks no longer receive any of the arguments that were provided in the event action
|
100
|
+
|
101
|
+
* Updated license to include our names.
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006 Scott Barron, 2006-2008 Aaron Pfefier
|
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
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
== state_machine
|
2
|
+
|
3
|
+
+state_machine+ support for creating state machines for attributes within a model.
|
4
|
+
|
5
|
+
== Resources
|
6
|
+
|
7
|
+
Wiki
|
8
|
+
|
9
|
+
* http://wiki.pluginaweek.org/State_machine
|
10
|
+
|
11
|
+
API
|
12
|
+
|
13
|
+
* http://api.pluginaweek.org/state_machine
|
14
|
+
|
15
|
+
Development
|
16
|
+
|
17
|
+
* http://dev.pluginaweek.org/browser/trunk/state_machine
|
18
|
+
|
19
|
+
Source
|
20
|
+
|
21
|
+
* http://svn.pluginaweek.org/trunk/state_machine
|
22
|
+
|
23
|
+
== Description
|
24
|
+
|
25
|
+
State machines make it dead-simple to manage the behavior of a model. Too often,
|
26
|
+
the status of a record is kept by creating multiple boolean columns in the table
|
27
|
+
and deciding how to behave based on the values in those columns. This can become
|
28
|
+
cumbersome and difficult to maintain when the complexity of your models starts to
|
29
|
+
increase.
|
30
|
+
|
31
|
+
+state_machine+ simplifies this design by introducing the various parts of a state
|
32
|
+
machine, including states, events, and transitions. However, its api is designed
|
33
|
+
to be similar to ActiveRecord in terms of validations and callbacks, making it
|
34
|
+
so simple you don't even need to know what a state machine is :)
|
35
|
+
|
36
|
+
== Usage
|
37
|
+
|
38
|
+
=== Example
|
39
|
+
|
40
|
+
class Vehicle < ActiveRecord::Base
|
41
|
+
state_machine :state, :initial => 'idling' do
|
42
|
+
before_exit 'parked', :put_on_seatbelt
|
43
|
+
after_enter 'parked', Proc.new {|vehicle| vehicle.update_attribute(:seatbelt_on, false)}
|
44
|
+
|
45
|
+
event :park do
|
46
|
+
transition :to => 'parked', :from => %w(idling first_gear)
|
47
|
+
end
|
48
|
+
|
49
|
+
event :ignite do
|
50
|
+
transition :to => 'stalled', :from => 'stalled'
|
51
|
+
transition :to => 'idling', :from => 'parked'
|
52
|
+
end
|
53
|
+
|
54
|
+
event :idle do
|
55
|
+
transition :to => 'idling', :from => 'first_gear'
|
56
|
+
end
|
57
|
+
|
58
|
+
event :shift_up do
|
59
|
+
transition :to => 'first_gear', :from => 'idling'
|
60
|
+
transition :to => 'second_gear', :from => 'first_gear'
|
61
|
+
transition :to => 'third_gear', :from => 'second_gear'
|
62
|
+
end
|
63
|
+
|
64
|
+
event :shift_down do
|
65
|
+
transition :to => 'second_gear', :from => 'third_gear'
|
66
|
+
transition :to => 'first_gear', :from => 'second_gear'
|
67
|
+
end
|
68
|
+
|
69
|
+
event :crash, :after => :tow! do
|
70
|
+
transition :to => 'stalled', :from => %w(first_gear second_gear third_gear), :unless => :auto_shop_busy?
|
71
|
+
end
|
72
|
+
|
73
|
+
event :repair, :after => :fix! do
|
74
|
+
transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
== Tools
|
80
|
+
|
81
|
+
Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html].
|
82
|
+
This is a great tool for "simulating, visualizing and transforming finite state
|
83
|
+
automata and Turing Machines". This tool can help in the creation of states and
|
84
|
+
events for your models. It is cross-platform, written in Java.
|
85
|
+
|
86
|
+
== Dependencies
|
87
|
+
|
88
|
+
None.
|
89
|
+
|
90
|
+
== Testing
|
91
|
+
|
92
|
+
Before you can run any tests, the following gem must be installed:
|
93
|
+
* plugin_test_helper[http://wiki.pluginaweek.org/Plugin_test_helper]
|
94
|
+
|
95
|
+
== References
|
96
|
+
|
97
|
+
* Scott Barron - acts_as_state_machine[http://elitists.textdriven.com/svn/plugins/acts_as_state_machine]
|
data/Rakefile
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
PKG_NAME = 'state_machine'
|
7
|
+
PKG_VERSION = '0.1.0'
|
8
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
9
|
+
RUBY_FORGE_PROJECT = 'pluginaweek'
|
10
|
+
|
11
|
+
desc 'Default: run unit tests.'
|
12
|
+
task :default => :test
|
13
|
+
|
14
|
+
desc 'Test the state_machine plugin.'
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.libs << 'lib'
|
17
|
+
t.pattern = 'test/**/*_test.rb'
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Generate documentation for the state_machine plugin.'
|
22
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
23
|
+
rdoc.rdoc_dir = 'rdoc'
|
24
|
+
rdoc.title = 'StateMachine'
|
25
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
26
|
+
rdoc.rdoc_files.include('README')
|
27
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
28
|
+
end
|
29
|
+
|
30
|
+
spec = Gem::Specification.new do |s|
|
31
|
+
s.name = PKG_NAME
|
32
|
+
s.version = PKG_VERSION
|
33
|
+
s.platform = Gem::Platform::RUBY
|
34
|
+
s.summary = 'Adds support for creating state machines for attributes within a model'
|
35
|
+
|
36
|
+
s.files = FileList['{lib,test}/**/*'].to_a + %w(CHANGELOG init.rb MIT-LICENSE Rakefile README)
|
37
|
+
s.require_path = 'lib'
|
38
|
+
s.autorequire = 'state_machine'
|
39
|
+
s.has_rdoc = true
|
40
|
+
s.test_files = Dir['test/**/*_test.rb']
|
41
|
+
|
42
|
+
s.author = 'Aaron Pfeifer'
|
43
|
+
s.email = 'aaron@pluginaweek.org'
|
44
|
+
s.homepage = 'http://www.pluginaweek.org'
|
45
|
+
end
|
46
|
+
|
47
|
+
Rake::GemPackageTask.new(spec) do |p|
|
48
|
+
p.gem_spec = spec
|
49
|
+
p.need_tar = true
|
50
|
+
p.need_zip = true
|
51
|
+
end
|
52
|
+
|
53
|
+
desc 'Publish the beta gem'
|
54
|
+
task :pgem => [:package] do
|
55
|
+
Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'Publish the API documentation'
|
59
|
+
task :pdoc => [:rdoc] do
|
60
|
+
Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{PKG_NAME}", 'rdoc').upload
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Publish the API docs and gem'
|
64
|
+
task :publish => [:pdoc, :release]
|
65
|
+
|
66
|
+
desc 'Publish the release files to RubyForge.'
|
67
|
+
task :release => [:gem, :package] do
|
68
|
+
require 'rubyforge'
|
69
|
+
|
70
|
+
ruby_forge = RubyForge.new
|
71
|
+
ruby_forge.login
|
72
|
+
|
73
|
+
%w( gem tgz zip ).each do |ext|
|
74
|
+
file = "pkg/#{PKG_FILE_NAME}.#{ext}"
|
75
|
+
puts "Releasing #{File.basename(file)}..."
|
76
|
+
|
77
|
+
ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
|
78
|
+
end
|
79
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'state_machine'
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'state_machine/machine'
|
2
|
+
|
3
|
+
module PluginAWeek #:nodoc:
|
4
|
+
# A state machine is a model of behavior composed of states, transitions,
|
5
|
+
# and events. This helper adds support for defining this type of
|
6
|
+
# functionality within your ActiveRecord models.
|
7
|
+
module StateMachine
|
8
|
+
def self.included(base) #:nodoc:
|
9
|
+
base.class_eval do
|
10
|
+
extend PluginAWeek::StateMachine::MacroMethods
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module MacroMethods
|
15
|
+
# Creates a state machine for the given attribute.
|
16
|
+
#
|
17
|
+
# Configuration options:
|
18
|
+
# * +initial+ - The initial value of the attribute. This can either be the actual value or a Proc for dynamic initial states.
|
19
|
+
#
|
20
|
+
# == Example
|
21
|
+
#
|
22
|
+
# With a static state:
|
23
|
+
#
|
24
|
+
# class Switch < ActiveRecord::Base
|
25
|
+
# state_machine :state, :initial => 'off' do
|
26
|
+
# ...
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# With a dynamic state:
|
31
|
+
#
|
32
|
+
# class Switch < ActiveRecord::Base
|
33
|
+
# state_machine :state, :initial => Proc.new {|switch| (8..22).include?(Time.now.hour) ? 'on' : 'off'} do
|
34
|
+
# ...
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
def state_machine(attribute, options = {}, &block)
|
38
|
+
unless included_modules.include?(PluginAWeek::StateMachine::InstanceMethods)
|
39
|
+
write_inheritable_attribute :state_machines, {}
|
40
|
+
class_inheritable_reader :state_machines
|
41
|
+
|
42
|
+
after_create :run_initial_state_machine_actions
|
43
|
+
|
44
|
+
include PluginAWeek::StateMachine::InstanceMethods
|
45
|
+
end
|
46
|
+
|
47
|
+
# This will create a new machine for subclasses as well so that the owner_class and
|
48
|
+
# initial state can be overridden
|
49
|
+
attribute = attribute.to_s
|
50
|
+
options[:initial] = state_machines[attribute].initial_state_without_processing if !options.include?(:initial) && state_machines[attribute]
|
51
|
+
machine = state_machines[attribute] = PluginAWeek::StateMachine::Machine.new(self, attribute, options)
|
52
|
+
machine.instance_eval(&block) if block
|
53
|
+
machine
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module InstanceMethods
|
58
|
+
def self.included(base) #:nodoc:
|
59
|
+
base.class_eval do
|
60
|
+
alias_method_chain :initialize, :state_machine
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Defines the initial values for state machine attributes
|
65
|
+
def initialize_with_state_machine(attributes = nil)
|
66
|
+
initialize_without_state_machine(attributes)
|
67
|
+
|
68
|
+
attribute_keys = (attributes || {}).keys.map!(&:to_s)
|
69
|
+
|
70
|
+
self.class.state_machines.each do |attribute, machine|
|
71
|
+
unless attribute_keys.include?(attribute)
|
72
|
+
send("#{attribute}=", machine.initial_state(self))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
yield self if block_given?
|
77
|
+
end
|
78
|
+
|
79
|
+
# Records the transition for the record going into its initial state
|
80
|
+
def run_initial_state_machine_actions
|
81
|
+
self.class.state_machines.each do |attribute, machine|
|
82
|
+
callback = "after_enter_#{attribute}_#{self[attribute]}"
|
83
|
+
run_callbacks(callback) if self.class.respond_to?(callback)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ActiveRecord::Base.class_eval do
|
91
|
+
include PluginAWeek::StateMachine
|
92
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'state_machine/transition'
|
2
|
+
|
3
|
+
module PluginAWeek #:nodoc:
|
4
|
+
module StateMachine
|
5
|
+
# An event defines an action that transitions an attribute from one state to
|
6
|
+
# another
|
7
|
+
class Event
|
8
|
+
# The state machine for which this event is defined
|
9
|
+
attr_reader :machine
|
10
|
+
|
11
|
+
# The name of the action that fires the event
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
delegate :owner_class,
|
15
|
+
:to => :machine
|
16
|
+
|
17
|
+
# Creates a new event with the given name
|
18
|
+
def initialize(machine, name, options = {})
|
19
|
+
options.assert_valid_keys(:before, :after)
|
20
|
+
|
21
|
+
@machine = machine
|
22
|
+
@name = name
|
23
|
+
@options = options.stringify_keys
|
24
|
+
|
25
|
+
add_transition_action
|
26
|
+
add_transition_callbacks
|
27
|
+
add_event_callbacks
|
28
|
+
end
|
29
|
+
|
30
|
+
# Creates a new transition to the specified state.
|
31
|
+
#
|
32
|
+
# Configuration options:
|
33
|
+
# * +to+ - The state that being transitioned to
|
34
|
+
# * +from+ - A state or array of states that can be transitioned from
|
35
|
+
# * +if+ - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
|
36
|
+
# * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
|
37
|
+
#
|
38
|
+
# == Examples
|
39
|
+
#
|
40
|
+
# transition :to => 'parked', :from => 'first_gear'
|
41
|
+
# transition :to => 'parked', :from => %w(first_gear reverse)
|
42
|
+
# transition :to => 'parked', :from => 'first_gear', :if => :moving?
|
43
|
+
# transition :to => 'parked', :from => 'first_gear', :unless => :stopped?
|
44
|
+
def transition(options = {})
|
45
|
+
options.symbolize_keys!
|
46
|
+
options.assert_valid_keys(:to, :from, :if, :unless)
|
47
|
+
raise ArgumentError, ':to state must be specified' unless options.include?(:to)
|
48
|
+
|
49
|
+
to_state = options.delete(:to)
|
50
|
+
from_states = Array(options.delete(:from))
|
51
|
+
from_states.collect do |from_state|
|
52
|
+
# Create the actual transition that will update records when run
|
53
|
+
transition = Transition.new(self, from_state, to_state)
|
54
|
+
|
55
|
+
# The callback that will be invoked when the event is run. If the callback
|
56
|
+
# fails, then the next available callback for the event will run until
|
57
|
+
# one is successful.
|
58
|
+
callback = Proc.new do |record, *args|
|
59
|
+
transition.can_perform_on?(record) &&
|
60
|
+
invoke_event_callbacks(:before, record, *args) != false &&
|
61
|
+
transition.perform(record, *args) &&
|
62
|
+
invoke_event_callbacks(:after, record, *args) != false
|
63
|
+
end
|
64
|
+
|
65
|
+
# Add the callback to the model
|
66
|
+
owner_class.send("transition_on_#{name}", callback, options)
|
67
|
+
|
68
|
+
transition
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Attempts to transition to one of the next possible states for the given record
|
73
|
+
def fire!(record, *args)
|
74
|
+
success = false
|
75
|
+
record.class.transaction {success = invoke_transition_callbacks(record, *args) == true || raise(ActiveRecord::Rollback)}
|
76
|
+
success
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
# Add action for transitioning the record
|
81
|
+
def add_transition_action
|
82
|
+
owner_class.class_eval <<-end_eval
|
83
|
+
def #{name}!(*args)
|
84
|
+
#{owner_class}.state_machines['#{machine.attribute}'].events['#{name}'].fire!(self, *args)
|
85
|
+
end
|
86
|
+
end_eval
|
87
|
+
end
|
88
|
+
|
89
|
+
# Defines callbacks for invoking transitions when this event is performed
|
90
|
+
def add_transition_callbacks
|
91
|
+
owner_class.define_callbacks("transition_on_#{name}")
|
92
|
+
end
|
93
|
+
|
94
|
+
# Adds the before/after callbacks for when the event is performed
|
95
|
+
def add_event_callbacks
|
96
|
+
%w(before after).each do |type|
|
97
|
+
callback_name = "#{type}_#{name}"
|
98
|
+
owner_class.define_callbacks(callback_name)
|
99
|
+
|
100
|
+
# Add each defined callback
|
101
|
+
Array(@options[type]).each {|callback| owner_class.send(callback_name, callback)}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Invokes a particulary type of callbacks for the event
|
106
|
+
def invoke_event_callbacks(type, record, *args)
|
107
|
+
args = [record] + args
|
108
|
+
|
109
|
+
record.class.send("#{type}_#{name}_callback_chain").each do |callback|
|
110
|
+
result = callback.call(*args)
|
111
|
+
break result if result == false
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Invokes the callbacks for each transition in order to find one that
|
116
|
+
# completes successfully
|
117
|
+
def invoke_transition_callbacks(record, *args)
|
118
|
+
args = [record] + args
|
119
|
+
|
120
|
+
record.class.send("transition_on_#{name}_callback_chain").each do |callback|
|
121
|
+
result = callback.call(*args)
|
122
|
+
break result if result == true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|