Bstard - A New State(sman) Machine

A small state machine library with a simple interface. Designed around the concept of aggregation rather than inheritance enabling its use wherever you need it without the need to derive from it or mix it in.

Gem Version Build Status

Installation

Add this line to your application's Gemfile:

gem 'bstard'

And then execute:

$ bundle

Or install it yourself as:

$ gem install bstard

Usage

Use Bstard.define to describe your state machine

  machine = Bstard.define do |fsm|
    fsm.initial :new
    fsm.event :submit, :new => :submitted
    fsm.event :delete, :submitted => :deleted
  end

This defines a state machine that:

  • has new, submitted and deleted states
  • responds to submit and delete events
  • has an initial state of new
  • transitions from new to submitted when triggered by the submit event
  • transitions from submitted to deleted when triggered by the delete event

Pretty simple huh?

Initial State

Set the initial state of the machine with initial. If no initial state is set a default state :uninitialized is used. As a safeguard initial will ignore nil or empty string values.

Querying the State

The current state of the machine is accessed via the current_state method

  machine.current_state
  #=> :new

Helper methods are automatically generated for each state consisting of the state name suffixed with a question mark. This method returns true if the current state matches the queried state or false otherwise.

  machine.new?
  #=> true
  machine.
  #=> false
  machine.deleted?
  #=> false

Querying Transitions

Helper methods are dynamically generated to enable querying whether an event can transition the current state. These methods are named using the event name prefixed with 'can_' and suffixed with a question mark.

  machine.current_state
  #=> :new
  machine.can_submit?
  #=> true
  machine.can_delete?
  #=> false

Defining Events

Events are defined by supplying an event name followed by one or more state transition declarations. Each state transition declaration is just a hash key value pair. The key represents the starting state and the value the ending state.

  machine = Bstard.define do |fsm|
    fsm.initial :new
    fsm.event :save, :new => :draft
    fsm.event :approve, :draft => :approved
    fsm.event :publish, :approved => :published
    fsm.event :delete, :draft => :deleted, :approved => :deleted
  end

I find the spear => Hash syntax style works best for me in describing the intent of the definition.

Triggering Events

Trigger events by sending a message using the event name suffixed with an exclamation mark.

  machine.submit!
  #=> :submitted
  machine.delete!
  #=> :deleted

Callbacks

Callbacks can be configured for particular events and state changes, or for any event or state change. Event callbacks are configured with on

  machine = Bstard.define do |fsm|
    # ...
    fsm.on :submit do |event, state, next_state|
      # code that needs to be run when :submit is triggered
    end
  end

State change callbacks are configured with when

  machine = Bstard.define do |fsm|
    # ...
    fsm.when :submitted do |event, previous_state, state|
      # code that needs to run when state changes to :submitted
    end
  end

Use the symbol :any for event or state change callbacks that should run when any event is triggered or after any state change.

  machine = Bstard.define do |fsm|
    # ...
    fsm.on :any do |event, state, next_state|
      # code that will run when any event is triggered
    end
    fsm.when :any do |event, previous_state, state|
      # code that will run when after any state change
    end
  end

e.g. Use initial and when :any to persist state

class MyModel < ActiveRecord::Base
  def make_draft
    # do stuff
    state.save!
    save
  end

private
  def state
    @state ||= Bstard.define do |fsm|
      fsm.initial status
      fsm.event :save, :new => :draft
      fsm.event :approve, :draft => :approved
      fsm.when :any do |event, prev_state, new_state|
        status = new_state
      end
    end
  end
end

Exceptions

An InvalidTransition error will be raised if an event is triggered on a state that cannot transition on that event.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run bundle exec rake install. To run the tests, run bundle exec rake test.

Contributing

  1. Fork it ( https://github.com/tonyheadford/bstard/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request