state_machine

https://github.com/pluginaweek/state_machine

Ruby

Adds support for creating state machines for attributes on any Ruby class

StateMachine::Machine#other_states

Customizes the definition of one or more states in the machine.

Configuration options:
* <tt>:value</tt> - The actual value to store when an object transitions
  to the state.  Default is the name (stringified).
* <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
  then setting this to true will cache the evaluated result
* <tt>:if</tt> - Determines whether an object's value matches the state
  (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
  By default, the configured value is matched.
* <tt>:human_name</tt> - The human-readable version of this state's name.
  By default, this is either defined by the integration or stringifies the
  name and converts underscores to spaces.

== Customizing the stored value

Whenever a state is automatically discovered in the state machine, its
default value is assumed to be the stringified version of the name.  For
example,

  class Vehicle
    state_machine :initial => :parked do
      event :ignite do
        transition :parked => :idling
      end
    end
  end

In the above state machine, there are two states automatically discovered:
:parked and :idling.  These states, by default, will store their stringified
equivalents when an object moves into that state (e.g. "parked" / "idling").

For legacy systems or when tying state machines into existing frameworks,
it's oftentimes necessary to need to store a different value for a state
than the default.  In order to continue taking advantage of an expressive
state machine and helper methods, every defined state can be re-configured
with a custom stored value.  For example,

  class Vehicle
    state_machine :initial => :parked do
      event :ignite do
        transition :parked => :idling
      end
      
      state :idling, :value => 'IDLING'
      state :parked, :value => 'PARKED
    end
  end

This is also useful if being used in association with a database and,
instead of storing the state name in a column, you want to store the
state's foreign key:

  class VehicleState < ActiveRecord::Base
  end
  
  class Vehicle < ActiveRecord::Base
    state_machine :attribute => :state_id, :initial => :parked do
      event :ignite do
        transition :parked => :idling
      end
      
      states.each do |state|
        self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
      end
    end
  end

In the above example, each known state is configured to store it's
associated database id in the +state_id+ attribute.  Also, notice that a
lambda block is used to define the state's value.  This is required in
situations (like testing) where the model is loaded without any existing
data (i.e. no VehicleState records available).

One caveat to the above example is to keep performance in mind.  To avoid
constant db hits for looking up the VehicleState ids, the value is cached
by specifying the <tt>:cache</tt> option.  Alternatively, a custom
caching strategy can be used like so:

  class VehicleState < ActiveRecord::Base
    cattr_accessor :cache_store
    self.cache_store = ActiveSupport::Cache::MemoryStore.new
    
    def self.find_by_name(name)
      cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
    end
  end

=== Dynamic values

In addition to customizing states with other value types, lambda blocks
can also be specified to allow for a state's value to be determined
dynamically at runtime.  For example,

  class Vehicle
    state_machine :purchased_at, :initial => :available do
      event :purchase do
        transition all => :purchased
      end
      
      event :restock do
        transition all => :available
      end
      
      state :available, :value => nil
      state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
    end
  end

In the above definition, the <tt>:purchased</tt> state is customized with
both a dynamic value *and* a value matcher.

When an object transitions to the purchased state, the value's lambda
block will be called.  This will get the current time and store it in the
object's +purchased_at+ attribute.

*Note* that the custom matcher is very important here.  Since there's no
way for the state machine to figure out an object's state when it's set to
a runtime value, it must be explicitly defined.  If the <tt>:if</tt> option
were not configured for the state, then an ArgumentError exception would
be raised at runtime, indicating that the state machine could not figure
out what the current state of the object was.

== Behaviors

Behaviors define a series of methods to mixin with objects when the current
state matches the given one(s).  This allows instance methods to behave
a specific way depending on what the value of the object's state is.

For example,

  class Vehicle
    attr_accessor :driver
    attr_accessor :passenger
    
    state_machine :initial => :parked do
      event :ignite do
        transition :parked => :idling
      end
      
      state :parked do
        def speed
          0
        end
        
        def rotate_driver
          driver = self.driver
          self.driver = passenger
          self.passenger = driver
          true
        end
      end
      
      state :idling, :first_gear do
        def speed
          20
        end
        
        def rotate_driver
          self.state = 'parked'
          rotate_driver
        end
      end
      
      other_states :backing_up
    end
  end

In the above example, there are two dynamic behaviors defined for the
class:
* +speed+
* +rotate_driver+

Each of these behaviors are instance methods on the Vehicle class.  However,
which method actually gets invoked is based on the current state of the
object.  Using the above class as the example:

  vehicle = Vehicle.new
  vehicle.driver = 'John'
  vehicle.passenger = 'Jane'
  
  # Behaviors in the "parked" state
  vehicle.state             # => "parked"
  vehicle.speed             # => 0
  vehicle.rotate_driver     # => true
  vehicle.driver            # => "Jane"
  vehicle.passenger         # => "John"
  
  vehicle.ignite            # => true
  
  # Behaviors in the "idling" state
  vehicle.state             # => "idling"
  vehicle.speed             # => 20
  vehicle.rotate_driver     # => true
  vehicle.driver            # => "John"
  vehicle.passenger         # => "Jane"

As can be seen, both the +speed+ and +rotate_driver+ instance method
implementations changed how they behave based on what the current state
of the vehicle was.

=== Invalid behaviors

If a specific behavior has not been defined for a state, then a
NoMethodError exception will be raised, indicating that that method would
not normally exist for an object with that state.

Using the example from before:

  vehicle = Vehicle.new
  vehicle.state = 'backing_up'
  vehicle.speed               # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"

=== Using matchers

The +all+ / +any+ matchers can be used to easily define behaviors for a
group of states.  Note, however, that you cannot use these matchers to
set configurations for states.  Behaviors using these matchers can be
defined at any point in the state machine and will always get applied to
the proper states.

For example:

  state_machine :initial => :parked do
    ...
    
    state all - [:parked, :idling, :stalled] do
      validates_presence_of :speed
      
      def speed
        gear * 10
      end
    end
  end

== State-aware class methods

In addition to defining scopes for instance methods that are state-aware,
the same can be done for certain types of class methods.

Some libraries have support for class-level methods that only run certain
behaviors based on a conditions hash passed in.  For example:

  class Vehicle < ActiveRecord::Base
    state_machine do
      ...
      state :first_gear, :second_gear, :third_gear do
        validates_presence_of   :speed
        validates_inclusion_of  :speed, :in => 0..25, :if => :in_school_zone?
      end
    end
  end

In the above ActiveRecord model, two validations have been defined which
will *only* run when the Vehicle object is in one of the three states:
+first_gear+, +second_gear+, or +third_gear.  Notice, also, that if/unless
conditions can continue to be used.

This functionality is not library-specific and can work for any class-level
method that is defined like so:

  def validates_presence_of(attribute, options = {})
    ...
  end

The minimum requirement is that the last argument in the method be an
options hash which contains at least <tt>:if</tt> condition support.

Source | Google | Stack overflow

Edit

git clone [email protected]:pluginaweek/state_machine.git

cd state_machine

open lib/state_machine/machine.rb

Contribute

# Make a new branch

git checkout -b -your-name--update-docs-StateMachine--Machine-other_states-for-pr


# Commit to git

git add lib/state_machine/machine.rbgit commit -m "better docs for StateMachine::Machine#other_states"


# Open pull request

gem install hub # on a mac you can `brew install hub`

hub fork

git push <your name> -your-name--update-docs-StateMachine--Machine-other_states-for-pr

hub pull-request


# Celebrate!