No release in over 3 years
Low commit activity in last 3 years
A simple DSL to decorate existing methods with state transition guards.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

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 © 2010 Marek & Petrik. See LICENSE for details.