Stately
A minimal, elegant state machine for your ruby objects.
Making a stately start
Stately is a state machine for ruby objects, with an elegant, easy-to-read DSL. Here's an example showing off what Stately can do:
class Order
stately :start => :processing do
state :completed do
prevent_from :refunded
before_transition :from => :processing, :do => :calculate_total
after_transition :do => :email_receipt
validate :validates_credit_card
end
state :invalid do
prevent_from :completed, :refunded
end
state :refunded do
allow_from :completed
after_transition :do => :email_receipt
end
end
endStately tries hard not to surprise you. When you transition to a new state, you're responsible for taking whatever actions that means using before_transition and after_transition. Stately also has no dependencies on things like DataMapper or ActiveModel, so it will never surprise you with an implicit save after transitioning states.
When to use stately
Often, you'll find yourself writing an object that can have multiple states. Tracking these states can usually be done either:
- By hand (i.e. adding a string column in the db and storing the current state there).
- Via a state machine of some kind. state_machine is a popular one that I've used quite a bit, which has a lot of advanced features (most of which I've never used).
Stately exists in a middle space between the two options. The goal of stately is to make the most common case, where you just need to track state and react appropriately when switching those states, easy.
Design goals
-
Minimalist. Stately tries to solve the most common use case: tracking the current state and handling transitions between states.
-
No magic. In other words, if you're using, say, ActiveRecord, stately won't hook in to activerecord callbacks. This requires you to be more explicit and perhaps more verbose, but I think it helps with readability and reduces surprises. See the Examples section below for what this looks like when in an ActiveRecord environment.
-
Syntax that is as self-documenting as possible. Someone not familiar with Stately should be able to understand what happens when an object's state is changed just by reading the DSL.
Getting started
Either install locally:
gem install statelyor add it to your Gemfile:
gem statelyBe sure to run bundle install afterwards.
The first step is to add the following to your object:
stately :start => :initial_state, :attr => :my_state_attr do
# ...
endThis sets up Stately to look for an attribute named my_state_attr, and initially set it to initial_state. If you omit :attr => :my_state_attr, Stately will automatically look for an attribute named state.
Defining a state
States make up the core of Stately and define two things: the name of the state (i.e. "completed"), and a verb as the name of the method to call to begin a transition into that state (i.e. "complete"). Stately has support for some common state/verb combinations, but you can always use your own:
class Order
stately :start => :processing do
state :my_state, :action => transition_to_my_state
end
end
order = Order.new
order.transition_to_my_stateTransitions
A "transition" is the process of moving from one state to another. You can define legal transitions using allow_from and prevent_from:
state :completed do
allow_from :processing
prevent_from :refunded
endIn the above example, if you try to transition to completed (by calling complete on the object) from refunded, you'll see a Stately::InvalidTransition is raised. By default, all transitions are allowed.
Validations
While transitioning from one state to another, you can define validations to be run. If any validation returns false, the transition is halted.
state :completed do
validate :validates_amount
validate :validates_credit_card
endEach validation is also called in order, so first validates_amount will be called, and if it doesn't return false, then validates_credit_card will be called and checked.
Callbacks
Callbacks can be defined to run either before or after a transition occurs. A before_transition is run after validations are checked, but before the state_attr has been written to with the new state. An after_transition is called after the state_attr has been written to.
If you're using Stately with some kind of persistence layer, sych as activerecord, you'll probably want an after_transition that calls save or the equivalent.
class Order
stately :start => :processing do
# ...
state :completed do
before_transition :from => :processing, :do => :before_completed
before_transition :from => :invalid, :do => :cleanup_invalid
after_transition :do => :after_completed
end
end
private
def after_completed
save
end
endA callback can include an optional from state name, which is only called when transitioning from the named state. Omitting it means the callback is always called.
Additionally, each callback is executed in the order in which it's defined.
Example: using Stately with ActiveRecord
Let's say you are modeling a Bicycle object for your rental shop and you're using ActiveRecord. A Bicycle has two states: available and rented. Using stately, you could define this as the following:
class Bicycle < ActiveRecord::Base
stately :start => :available do
state :rented, :action => :rent do
after_transition :do => :save
end
end
endWhen Bicycle is first instantiated, its state column is set to the string available. If you want to rent the Bicycle, you'd call bicycle.rent, which would update the state column to be the string rented and then call the ActiveRecord method save.
As you can see, Stately is slightly more verbose than other state machine gems, but with the upside of being more self-documenting. Additionally, it doesn't hook into ActiveRecord's callback chains, and instead requires you to explicitely call save.
Requirements
Stately requires Ruby 1.8.7 or newer. If you'd like to contribute to Stately, you'll need Rspec 2.0+.
License
Stately is Copyright © 2013 Ryan Twomey. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.