SimpleStateMachine
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.
It supports exception rescuing, google chart visualization and mountable state_machines.
Basic example
class LampSwitch
extend SimpleStateMachine
def initialize
self.state = 'off'
end
def push_switch
puts "pushed switch"
end
event :push_switch, :off => :on,
:on => :off
end
lamp = LampSwitch.new
lamp.state # => 'off'
lamp.off? # => true
lamp.push_switch # => 'pushed switch'
lamp.state # => 'on'
lamp.on? # => true
lamp.push_switch # => 'pushed switch'
lamp.off? # => true
Basic usage
Define your event as a method, arguments are allowed:
def activate_account(activation_code)
# call other methods, no need to add these in callbacks
..
end
Now mark the method as an event and specify how the state should transition when the method is called. If we want the state to change from :pending to :active we write:
event :activate_account, :pending => :active
That’s it! You can now call activate_account and the state will automatically change. If the state change is not allowed, a SimpleStateMachine::IllegalStateTransitionError is raised.
Using ActiveRecord / ActiveModel validations
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
activate_account!("INVALID_CODE") # => ActiveRecord::RecordInvalid, "Validation failed: Activation code is invalid"
Catching exceptions
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
download_data # catches Service::ConnectionError
state # => "download_failed"
Catching all from states
If an event should transition from all states you can use :all
event :suspend, :all => :suspended
ActiveRecord Example
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 def after_initialize self.ssm_state ||= 'pending' # if you get an ActiveRecord::MissingAttributeError # you'll probably need to do (http://bit.ly/35q23b): # write_attribute(:ssm_state, "pending") 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, :pending => :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 end
This generates the following event methods
-
invite
-
invite!
-
confirm_invitation!
And the following methods to query the state:
-
pending?
-
invited?
-
active?
If you want to be more verbose you can also use:
-
invite_and_save (is equal to invite and behaves like ActiveRecord save)
-
invite_and_save! (is equal to invite! and behaves like save!)
Mountable Example
You can define your state machine in a seperate class:
class MyStateMachine < SimpleStateMachine::StateMachineDefinition
def initialize(subject)
self.lazy_decorator = lambda { SimpleStateMachine::Decorator.new(subject) }
add_transition(:invite, :new, :invited)
add_transition(:confirm_invitation, :invited, :active)
end
end
class User < ActiveRecord::Base
extend SimpleStateMachine::Mountable
self.state_machine_definition = MyStateMachine.new self
def after_initialize
self.ssm_state ||= 'new'
end
end
Generating google chart visualizations
If your using rails you get rake tasks for generating a graphiz google chart of the state machine.
Note on Patches/Pull Requests
-
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
Copyright © 2010 Marek & Petrik. See LICENSE for details.