README.rdoc in state_machine-0.3.1 vs README.rdoc in state_machine-0.4.0
- old
+ new
@@ -1,9 +1,9 @@
== state_machine
-+state_machine+ adds support for creating state machines for attributes within
-a model.
++state_machine+ adds support for creating state machines for attributes on any
+Ruby class.
== Resources
API
@@ -21,36 +21,53 @@
* git://github.com/pluginaweek/state_machine.git
== Description
-State machines make it dead-simple to manage the behavior of a model. Too often,
-the status of a record is kept by creating multiple boolean columns in the table
-and deciding how to behave based on the values in those columns. This can become
-cumbersome and difficult to maintain when the complexity of your models starts to
-increase.
+State machines make it dead-simple to manage the behavior of a class. Too often,
+the status of an object is kept by creating multiple boolean attributes and
+deciding how to behave based on the values. This can become cumbersome and
+difficult to maintain when the complexity of your class starts to increase.
+state_machine+ simplifies this design by introducing the various parts of a real
-state machine, including states, events, and transitions. However, the api is
-designed to be similar to ActiveRecord in terms of validations and callbacks,
-making it so simple you don't even need to know what a state machine is :)
+state machine, including states, events, transitions, and callbacks. However,
+the api is designed to be so simple you don't even need to know what a
+state machine is :)
+Some brief, high-level features include:
+* Defining state machines on any Ruby class
+* Multiple state machines on a single class
+* before/after transition hooks with explicit transition requirements
+* ActiveRecord integration
+* DataMapper integration
+* Sequel integration
+* States of any data type
+* State predicates
+* GraphViz visualization creator
+
+Examples of the usage patterns for some of the above features are shown below.
+You can find more detailed documentation in the actual API.
+
== Usage
=== Example
Below is an example of many of the features offered by this plugin, including
* Initial states
* Transition callbacks
* Conditional transitions
- class Vehicle < ActiveRecord::Base
+ class Vehicle
+ attr_accessor :seatbelt_on
+
state_machine :state, :initial => 'parked' do
before_transition :from => %w(parked idling), :do => :put_on_seatbelt
- after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.update_attribute(:seatbelt_on, false)}
- after_transition :on => 'crash', :do => :tow!
- after_transition :on => 'repair', :do => :fix!
+ after_transition :on => 'crash', :do => :tow
+ after_transition :on => 'repair', :do => :fix
+ after_transition :to => 'parked' do |vehicle, transition|
+ vehicle.seatbelt_on = false
+ end
event :park do
transition :to => 'parked', :from => %w(idling first_gear)
end
@@ -81,48 +98,116 @@
event :repair do
transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
end
end
- def tow!
- # do something here
+ def initialize
+ @seatbelt_on = false
end
- def fix!
- # do something here
+ def put_on_seatbelt
+ @seatbelt_on = true
end
def auto_shop_busy?
false
end
+
+ def tow
+ # tow the vehicle
+ end
+
+ def fix
+ # get the vehicle fixed by a mechanic
+ end
end
-Using the above model as an example, you can interact with the state machine
+Using the above class as an example, you can interact with the state machine
like so:
- vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state: "parked">
- vehicle.ignite # => true
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "idling">
- vehicle.shift_up # => true
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "first_gear">
- vehicle.shift_up # => true
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "second_gear">
+ vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
+ vehicle.parked? # => true
+ vehicle.can_ignite? # => true
+ vehicle.next_ignite_transition # => #<StateMachine::Transition:0xb7c34cec ...>
+ vehicle.ignite # => true
+ vehicle.parked? # => false
+ vehicle.idling? # => true
+ vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
+ vehicle.shift_up # => true
+ vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
+ vehicle.shift_up # => true
+ vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
# The bang (!) operator can raise exceptions if the event fails
- vehicle.park! # => PluginAWeek::StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
+ vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
+
+ # Generic state predicates can raise exceptions if the value does not exist
+ vehicle.state?('parked') # => true
+ vehicle.state?('invalid') # => ArgumentError: "parked" is not a known state value
-=== With enumerations
+== Integrations
+In addition to being able to define state machines on all Ruby classes, a set of
+out-of-the-box integrations are available for some of the more popular Ruby
+libraries. These integrations add library-specific behavior, allowing for state
+machines to work more tightly with the conventions defined by those libraries.
+
+The integrations currently available include:
+* ActiveRecord models
+* DataMapper resources
+* Sequel models
+
+A brief overview of these integrations is described below.
+
+=== ActiveRecord
+
+The ActiveRecord integration adds support for database transactions, automatically
+saving the record, named scopes, and observers. For example,
+
+ class Vehicle < ActiveRecord::Base
+ state_machine :initial => 'parked' do
+ before_transition :to => 'idling', :do => :put_on_seatbelt
+ after_transition :to => 'parked' do |vehicle, transition|
+ vehicle.seatbelt = 'off'
+ end
+
+ event :ignite do
+ transition :to => 'idling', :from => 'parked'
+ end
+ end
+
+ def put_on_seatbelt
+ ...
+ end
+ end
+
+ class VehicleObserver < ActiveRecord::Observer
+ # Callback for :ignite event *before* the transition is performed
+ def before_ignite(vehicle, transition)
+ # log message
+ end
+
+ # Generic transition callback *before* the transition is performed
+ def after_transition(vehicle, transition)
+ Audit.log(vehicle, transition)
+ end
+ end
+
+For more information about the various behaviors added for ActiveRecord state
+machines, see StateMachine::Integrations::ActiveRecord.
+
+==== With enumerations
+
Using the acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration] plugin
-states can be transparently stored using record ids in the database like so:
+with an ActiveRecord integration, states can be transparently stored using
+record ids in the database like so:
class VehicleState < ActiveRecord::Base
acts_as_enumeration
create :id => 1, :name => 'parked'
create :id => 2, :name => 'idling'
- create :id => 3, :name => 'first_gear'
...
end
class Vehicle < ActiveRecord::Base
belongs_to :state, :class_name => 'VehicleState'
@@ -131,51 +216,174 @@
...
event :park do
transition :to => 'parked', :from => %w(idling first_gear)
end
-
- event :ignite do
- transition :to => 'stalled', :from => 'stalled'
- transition :to => 'idling', :from => 'parked'
- end
end
...
end
-Notice in the above example, the state machine definition remains *exactly* the
-same. However, when interacting with the records, the actual state will be
-stored using the identifiers defined for the enumeration:
+Notice that the state machine definition remains *exactly* the same. However,
+when interacting with the records, the actual state will be stored using the
+identifiers defined for the enumeration:
vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state_id: 1>
vehicle.ignite # => true
vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 2>
- vehicle.shift_up # => true
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 3>
This allows states to take on more complex functionality other than just being
a string value.
+=== DataMapper
+
+Like the ActiveRecord integration, the DataMapper integration adds support for
+database transactions, automatically saving the record, named scopes, Extlib-like
+callbacks, and observers. For example,
+
+ class Vehicle
+ include DataMapper::Resource
+
+ property :id, Serial
+ property :state, String
+
+ state_machine :initial => 'parked' do
+ before_transition :to => 'idling', :do => :put_on_seatbelt
+ after_transition :to => 'parked' do |transition|
+ self.seatbelt = 'off' # self is the record
+ end
+
+ event :ignite do
+ transition :to => 'idling', :from => 'parked'
+ end
+ end
+
+ def put_on_seatbelt
+ ...
+ end
+ end
+
+ class VehicleObserver
+ include DataMapper::Observer
+
+ observe Vehicle
+
+ # Callback for :ignite event *before* the transition is performed
+ before_transition :on => :ignite do |transition|
+ # log message (self is the record)
+ end
+
+ # Generic transition callback *before* the transition is performed
+ after_transition do |transition, saved|
+ Audit.log(self, transition) if saved # self is the record
+ end
+ end
+
+For more information about the various behaviors added for DataMapper state
+machines, see StateMachine::Integrations::DataMapper.
+
+=== Sequel
+
+Like the ActiveRecord integration, the Sequel integration adds support for
+database transactions, automatically saving the record, named scopes, and
+callbacks. For example,
+
+ class Vehicle < Sequel::Model
+ state_machine :initial => 'parked' do
+ before_transition :to => 'idling', :do => :put_on_seatbelt
+ after_transition :to => 'parked' do |transition|
+ self.seatbelt = 'off' # self is the record
+ end
+
+ event :ignite do
+ transition :to => 'idling', :from => 'parked'
+ end
+ end
+
+ def put_on_seatbelt
+ ...
+ end
+ end
+
+For more information about the various behaviors added for Sequel state
+machines, see StateMachine::Integrations::Sequel.
+
== Tools
+=== Generating graphs
+
+This library comes with built-in support for generating di-graphs based on the
+events, states, and transitions defined for a state machine using GraphViz[http://www.graphviz.org].
+This requires that both the <tt>ruby-graphviz</tt> gem and graphviz library be
+installed on the system.
+
+==== Examples
+
+To generate a graph for a specific file / class:
+
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle
+
+To save files to a specific path:
+
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files
+
+To customize the image format:
+
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg
+
+To generate multiple state machine graphs:
+
+ rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
+
+*Note* that this will generate a different file for every state machine defined
+in the class. The generates files will an output filename of the format #{class_name}_#{attribute}.#{format}.
+
+For examples of actual images generated using this task, see those under the
+test/examples folder.
+
+==== Ruby on Rails Integration
+
+There is a special integration Rake task for generating state machines for
+classes used in a Ruby on Rails application. This task will load the application
+environment, meaning that it's unnecessary to specify the actual file to load.
+
+For example,
+
+ rake state_machine:draw:rails CLASS=Vehicle
+
+==== Merb Integration
+
+Like Ruby on Rails, there is a special integration Rake task for generating
+state machines for classes used in a Merb application. This task will load the
+application environment, meaning that it's unnecessary to specify the actual
+files to load.
+
+For example,
+
+ rake state_machine:draw:merb CLASS=Vehicle
+
+=== Interactive graphs
+
Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html].
This is a great tool for "simulating, visualizing and transforming finite state
automata and Turing Machines". This tool can help in the creation of states and
events for your models. It is cross-platform, written in Java.
== Testing
-Before you can run any tests, the following gem must be installed:
-* plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
+To run the entire test suite (will test ActiveRecord, DataMapper, and Sequel
+integrations if available):
-To run against a specific version of Rails:
+ rake test
- rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
-
== Dependencies
-* Rails 2.1 or later
+By default, there are no dependencies. If using specific integrations, those
+dependencies are listed below.
+
+* ActiveRecord[http://rubyonrails.org] integration: 2.1.0 or later
+* DataMapper[http://datamapper.org] integration: 0.9.0 or later
+* Sequel[http://sequel.rubyforge.org] integration: 2.8.0 or later
== References
* Scott Barron - acts_as_state_machine[http://elitists.textdriven.com/svn/plugins/acts_as_state_machine]
* acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration]