state_machine 0.1.0
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/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
|