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.
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
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.
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"
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"
If an event should transition from all states you can use :all
event :suspend, :all => :suspended
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' end def invite self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}") 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 (behaves like ActiveRecord save )
-
invite! (behaves like ActiveRecord save!)
-
confirm_invitation (behaves like ActiveRecord save )
-
confirm_invitation! (behaves like ActiveRecord save!)
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 (alias for invite)
-
invite_and_save! (alias for invite!)
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
If your using rails you get rake tasks for generating a graphviz google chart of the state machine.
-
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.