state_machine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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