
Fast and furious. That’s how development’s been on state_machine over the last month (and is also what happens to sequels when you run out of bad ideas). Yesterday, 0.7 was officially added to the state_machine family of releases. With this release comes better integrations, easier access to more state information, and the all new and improved Done Right™. Check out the changelog for a detailed list of changes.
REST and events – Events are auto-fired when a machine’s action is called (or when validations run) based on the value of #{attribute}_event:
purchase = Purchase.create # => #<Purchase id: 1, state: "pending">
purchase.state_event # => nil
purchase.update_attributes(:state_event => 'authorize')
purchase.state # => "authorized"
purchase.state_event # => nil
purchase.state_event = 'capture'
purchase.valid? # => true
purchase.state # => "captured"
purchase.save # => true
This means that your create/update controller actions don’t change at all. For an example of the full Rails/Merb vertical slice, see the RESTful examples.
State rollbacks – If an action fails when firing an event, the transition is rolled back:
purchase = Purchase.find(1)
purchase.state # => "pending"
purchase.amount = nil
purchase.valid? # => false # validates_presence_of :amount
purchase.authorize # => false
purchase.state # => "pending" # Returned "authorized" pre-0.7
ActiveRecord Observers – The ActiveRecord integration kicked it up a notch (bam!) with new Observer hooks:
class PurchaseObserver < ActiveRecord::Observer
def before_capture_from_pending_to_captured(p, transition); end
def before_capture_from_pending(p, transition); end
def before_capture_to_captured(p, transition); end
def before_capture(p, transition); end
def before_transition_state_from_pending_to_captured(p, transition); end
def before_transition_state_to_captured(p, transition); end
def before_transition_state_from_pending(p, transition); end
def before_transition_state(p, transition); end
def before_transition(p, transition); end
end
You can define the same types of hooks for after_* as well.
DataMapper Defaults – The DataMapper integration no longer uses transactions by default and properties are now auto-defined if they don’t already exist:
class Purchase
include DataMapper::Resource
property :id, Integer, :serial => true
# property :state, String # Automatically done by state_machine
state_machine :use_transactions => true do
...
end
end
Available events / transitions – The full list of available events / transitions for a state machine can be accessed via two new helpers:
purchase = Purchase.find(1) # => #<Purchase id: 1, state: "pending">
purchase.state_events # => [:authorize, :capture]
purchase.state_transitions # => [#<StateMachine::Transition ...>, ...]
purchase.authorize
purchase.state_events # => [:capture]
purchase.state_transitions # => [#<StateMachine::Transition ...>]
This becomes particularly useful when presenting these in a view as a dropdown for users to select from.
Backwards Incompatible! Accessing a specific event’s transition has been renamed from next_#{event}_transition to #{event}_transition:
purchase.authorize_transition # => #<StateMachine::Transition ...>
purchase.next_authorize_transition # => NoMethodError: undefined method...
Event arguments – Arguments to event methods can be accessed from transition callbacks:
before_transition :on => :authorize do |purchase, transition|
amount = transition.args.first
...
end
after_transition callbacks – Backwards incompatible! The result of the action is no longer passed to after_transition callbacks. It can be accessed from the transition instead:
after_transition :on => :authorize do |purchase, transition|
saved = transition.result
...
end
This affects all integrations as well.
Parallel events – For classes that use multiple state machines, you can fire events off in parallel on an object. This means their before/after callbacks get run at the same time and actions only get called once if they’re the same:
purchase = Purchase.find(1)
purchase.state # => "pending"
purchase.shipped_at # => nil
purchase.fire_events(:capture, :deliver_shipment) # => true
purchase.state # => "captured"
purchase.shipped_at # => Sun Apr 05 ...
# Bang method is similar to events
purchase.fire_events!(:capture) # => StateMachine::InvalidTransition: ...
And the rest of the gang…
In addition to the above major changes / additions, there were 6 new minor features and 4 bug fixes in this release.
A special Billy Mays shout-out goes to the folks who helped make this release happen, including:
- Aaron Gibralter
- Paweł Kondzior
- Mikhail Shirkov
Stalk me, but not in a creepy way
As always, be sure to follow the project on GitHub! Happy coding!