A simple DSL to decorate existing methods with state transition guards.
Instead of using a DSL to define events, SimpleStateMachine decorates methods to help you encapsulate state and guard state transitions.
Write a method, arguments are allowed:
def activate_account(activation_code) # call other methods, no need to add these in callbacks ... log.debug "Try to activate account with #{activation_code}" end
Now mark the method as an event and specify how the state should transition when the method is called. In this example, the activate_account method will set the state to :active if the initial state is :pending.
event :activate_account, :pending => :active
That’s it! You can now call the method and the state will automatically change. If the state change is not allowed, a SimpleStateMachine::Error is raised.
When using ActiveRecord / ActiveModel you can add an error to the errors object. This will prevent the state from being changed.
def activate_account(activation_code) if activation_code_invalid?(activation_code) errors.add(:activation_code, 'Invalid') end end
You can rescue exceptions and specify the failure state
def download_data Service.download_data end event :download_data, :pending => :downloaded, Service::ConnectionError => :download_failed
To add a state machine:
-
extend SimpleStateMachine
-
set the initial state
-
turn methods into events
class LampWithHotelSwitch extend SimpleStateMachine def initialize self.state = :off end def push_switch_1 puts 'pushed switch 1 #{state}' end event :push_switch_1, :off => :on, :on => :off # define another event # note that implementation of :push_switch_2 is optional event :push_switch_2, :off => :on, :on => :off end
To add a state machine with ActiveRecord persistence:
-
extend SimpleStateMachine::ActiveRecord,
-
set the initial state in after_initialize,
-
turn methods into events
class User < ActiveRecord::Base extend SimpleStateMachine::ActiveRecord # define a custum state_method (state is default) state_machine_definition.state_method = :ssm_state def after_initialize self.ssm_state ||= 'new' # if you get an ActiveRecord::MissingAttributeError # you'll probably need to do (http://bit.ly/35q23b): # write_attribute(:ssm_state, "new") unless read_attribute(:ssm_state) end def invite self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}") #send_activation_email end event :invite, :new => :invited def confirm_invitation activation_code if self.activation_code != activation_code errors.add 'activation_code', 'is invalid' end end event :confirm_invitation, :invited => :active # :all can be used to catch all from states event :suspend, :all => :suspended end
This generates the following methods
-
{event}_and_save works like save
-
{event}_and_save! works like save!
-
{event}! works the same as {event}_and_save!
-
{state}? whether or not the current state is {state}
This code was just released, we do not claim it to be stable.
-
Fork the project.
-
Make your feature addition or bug fix.
-
Add tests for it. This is important so I don’t break it in a future version unintentionally.
-
Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
-
Send me a pull request. Bonus points for topic branches.
Copyright © 2010 Marek & Petrik. See LICENSE for details.