state_machine
https://github.com/pluginaweek/state_machine
Ruby
Adds support for creating state machines for attributes on any Ruby class
StateMachine::Machine#state
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.Edit
git clone [email protected]:pluginaweek/state_machine.git
cd state_machine
open lib/state_machine/machine.rb
Contribute
# Make a new branchgit checkout -b -your-name--update-docs-StateMachine--Machine-state-for-pr
# Commit to gitgit add lib/state_machine/machine.rbgit commit -m "better docs for StateMachine::Machine#state"
# Open pull requestgem install hub # on a mac you can `brew install hub`
hub fork
git push <your name> -your-name--update-docs-StateMachine--Machine-state-for-pr
hub pull-request
# Celebrate!