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