public
Description: Adds support for creating state machines for attributes on any Ruby class
Homepage: http://www.pluginaweek.org
Clone URL: git://github.com/pluginaweek/state_machine.git
name age message
file .gitignore Loading commit data...
file CHANGELOG.rdoc
file LICENSE Wed Jun 25 20:23:38 -0700 2008 Rename MIT-LICENSE to LICENSE [obrie]
file README.rdoc
file Rakefile
file init.rb Sun May 04 15:58:57 -0700 2008 Completely rewritten from scratch Renamed to st... [obrie]
directory lib/
directory test/
README.rdoc

state_machine

state_machine adds support for creating state machines for attributes within a model.

Resources

API

Bugs

Development

Source

  • 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_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 :)

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

      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!
    
        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
    
        event :idle do
          transition :to => 'idling', :from => 'first_gear'
        end
    
        event :shift_up do
          transition :to => 'first_gear', :from => 'idling'
          transition :to => 'second_gear', :from => 'first_gear'
          transition :to => 'third_gear', :from => 'second_gear'
        end
    
        event :shift_down do
          transition :to => 'second_gear', :from => 'third_gear'
          transition :to => 'first_gear', :from => 'second_gear'
        end
    
        event :crash do
          transition :to => 'stalled', :from => %w(first_gear second_gear third_gear), :unless => :auto_shop_busy?
        end
    
        event :repair do
          transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
        end
      end
    
      def tow!
        # do something here
      end
    
      def fix!
        # do something here
      end
    
      def auto_shop_busy?
        false
      end
    

    end

Using the above model 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">

  # The bang (!) operator can raise exceptions if the event fails
  vehicle.park!             # => PluginAWeek::StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"

With enumerations

Using the acts_as_enumeration plugin 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'

    state_machine :state, :initial => 'parked' do
      ...

      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:

  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.

Tools

Jean Bovet - Visual Automata Simulator. 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:

To run against a specific version of Rails:

  rake test RAILS_FRAMEWORK_ROOT=/path/to/rails

Dependencies

  • Rails 2.1 or later

References