class MicroMachine
  InvalidEvent = Class.new(NoMethodError)

  attr :transitions_for
  attr :state

  def initialize initial_state
    @state = initial_state
    @transitions_for = Hash.new
    @callbacks = Hash.new { |hash, key| hash[key] = [] }
  end

  def on key, &block
    @callbacks[key] << block
  end

  def trigger event
    if trigger?(event)
      @state = transitions_for[event][@state]
      callbacks = @callbacks[@state] + @callbacks[:any]
      callbacks.each { |callback| callback.call }
      true
    end
  end

  def trigger?(event)
    transitions_for[event][state]
  rescue NoMethodError
    raise InvalidEvent
  end
end

class MicroMachine
  InvalidEvent = Class.new(NoMethodError)

  attr :transitions_for
  attr :state

  def initialize initial_state
    @state = initial_state
    @transitions_for = Hash.new
    @callbacks = Hash.new { |hash, key| hash[key] = [] }
  end

  def on key, &block
    @callbacks[key] << block
  end

  def trigger event
    if trigger?(event)
      @state = transitions_for[event][@state]
      callbacks = @callbacks[@state] + @callbacks[:any]
      callbacks.each { |callback| callback.call }
      true
    end
  end

  def trigger?(event)
    transitions_for[event][state]
  rescue NoMethodError
    raise InvalidEvent
  end

  def ==(some_state)
    state == some_state
  end
end

Allow comparing a Machine to a String
Allow comparing a Machine to a String

This lets us redefine model methods as FSMs, and still be able to compare:

    class SomeModel < ActiveRecord::Base
      def activate!
        state.trigger(:activate)
        save!
      end

      def state
        @_state ||= MicroMachine.new(self[:state]).tap do |fsm|
          fsm.transitions_for[:activate] = { "pending" => "active" }
          fsm.on(:any) { self[:state] = state.state }
        end
      end
    end

    model = SomeModel.new
    model.activate!
    model.state == "active"
