SimpleStateMachine¶ ↑
<img src=“https://github.com/mdh/ssm/actions/workflows/build.yml/badge.svg” />
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.
Usage¶ ↑
Define an event and specify how the state should transition. 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.
Methods with arguments¶ ↑
If you want to pass arguments and call other methods before the state transition, define your event as a method.
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.
event :activate_account, :pending => :active
Basic example¶ ↑
class LampSwitch extend SimpleStateMachine def initialize self.state = 'off' end event :push_switch, :off => :on, :on => :off end lamp = LampSwitch.new lamp.state # => 'off' lamp.off? # => true lamp.push_switch # lamp.state # => 'on' lamp.on? # => true lamp.push_switch # lamp.off? # => true
ActiveRecord¶ ↑
For ActiveRecord methods are decorated with state transition guards and persistence. Methods marked as events behave like ActiveRecord save and save!.
Example¶ ↑
To add a state machine to an ActiveRecord class, you will have to:
-
extend SimpleStateMachine::ActiveRecord,
-
set the initial state in after_initialize,
-
turn methods into events
class User < ActiveRecord::Base extend SimpleStateMachine::ActiveRecord after_initialize do self.state ||= 'pending' end def invite self.activation_code = Digest::SHA1.hexdigest("salt #{Time.now.to_f}") end event :invite, :pending => :invited end user = User.new user.pending? # => true user.invite # => true user.invited? # => true user.activation_code # => 'SOMEDIGEST'
For the invite method this generates the following event methods
-
invite (behaves like ActiveRecord save )
-
invite! (behaves like ActiveRecord save!)
If you want to be more verbose you can also use:
-
invite_and_save (alias for invite)
-
invite_and_save! (alias for invite!)
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.
If we add an activate_account method to User
class User < ActiveRecord::Base ... def activate_account(activation_code) if activation_code_invalid?(activation_code) errors.add(:activation_code, 'Invalid') end end event :activate_account, :invited => :confirmed ... end user.confirm_invitation!('INVALID') # => raises ActiveRecord::RecordInvalid, # "Validation failed: Activation code is invalid" user.confirmed? # => false user.confirm_invitation!('VALID') user.confirmed? # => true
Mountable StateMachines¶ ↑
If you like to separate your state machine from your model class, you can do so as following:
class MyStateMachine < SimpleStateMachine::StateMachineDefinition event :invite, :new => :invited event :confirm_invitation, :invited => :active def decorator_class SimpleStateMachine::Decorator::Default end end class User < ActiveRecord::Base extend SimpleStateMachine::Mountable mount_state_machine MyStateMachine after_initialize do self.state ||= 'new' end end
Transitions¶ ↑
Catching all from states¶ ↑
If an event should transition from all other defined states, you can use the :all state:
event :suspend, :all => :suspended
Catching exceptions¶ ↑
You can let the state machine handle exceptions by specifying the failure state for an Error:
def download_data raise Service::ConnectionError, "Uhoh" end event :download_data, Service::ConnectionError => :download_failed download_data # catches Service::ConnectionError state # => "download_failed" state_machine.raised_error # "Uhoh"
Default error state¶ ↑
To automatically catch all exceptions to a default error state use default_error_state:
state_machine_definition.default_error_state = :failed
Transactions¶ ↑
If you want to run events in transactions run them in a transaction block:
user.transaction { user.invite! }
Tools¶ ↑
Generating state diagrams¶ ↑
When using Rails/ActiveRecord you can generate a state diagram of the state machine via the built in rake tasks. For details run:
rake -T ssm
A Googlechart example: tinyurl.com/79xztr6
Installation¶ ↑
Use gem install:
gem install simple_state_machine
Or add it to your Gemfile:
gem 'simple_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.