Stateful

TODO for 1.0:
  - make machine.valid? :from => :to work
  - complete tracing plugin
  - ar/dm integration
  - complete timestamps plugin
  - turn these notes into real docs
  - flesh out Machine#to_dot
  - clean up, minimally document, and :nodoc: classes

Example

class AClass

  # :start is optional, defaults to first state declared    
  statefully(:start => :foo) do

    # can declare a bunch of states like this,
    states :foo, :bar, :baz, :bax

    # or one at a time
    state :angry
    state :angsty

    # note that you don't have to declare states at all if you don't want to:
    # they'll be auto-created the first time they're referenced.

    on :traffic do
      # an event must specify all the places it can be called. it can put
      # them in a 'move' line, like this,
      move :state_or_list => :dest

      # or it can specify that the event 'cycles', staying in the originating state
      stay :state_or_list
    end

    # events that are available on any state can be wildcarded with :ANY,
    # which is the only reserved state name. Everything else is fair game.

    on :ping do
      move :ANY => :pinged
    end
  end
end

# multiple 'statefully' blocks are totally okay.
# you can even add them from outside the class!

AClass.statefully do

  # you can listen for attempted state entry.

  entering :state_or_list do |ctx|
    # do some ruby stuff! Exceptions thrown in here will
    # keep the transition from occuring, i.e., the model's
    # current state will still be 'from'.

    # the context here gives you all sorts of stuff:

    ctx.model  # the target model,
    ctx.event  # the name of the event that was fired,
    ctx.to     # the destination state,
    ctx.from   # the source state, and
    ctx.extras # any additional keys you might've passed in!
  end

  # you can also listen for successful state entry. same context is available.

  entered :state_or_list do |ctx|
    # more ruby stuff! You can throw exceptions in here, of course,
    # but it won't change the model's current state.
  end

  # listening for attempted state exit:

  exiting :state_or_list do |ctx|
    # awesome ruby stuff! throwing exceptions in here vetoes
    # the transition.
  end
end

AClass.statefully do

  # it's possible to listen to events fire, too!

  firing :event_or_list do |ctx|
    # throwing exceptions here will veto the transition
  end

  # or after they've successfully fired

  fired :event_or_list do |ctx|
    # throwing here does squat
  end
end

# The order of listeners/events: any marked with an 'if' can
# be vetoed by throwing an exception.

# (an event is triggered, e.g., model.finish!)
#   - if all matching 'event firing'
#     - if all matching 'state exiting'
#       - if all matching 'state entering'
#         - STATE CHANGE PERSISTED
#         - all matching 'state entered'
#         - all matching 'event fired'

# calls to 'statefully' always return the Stateful::StateMachine instance
# for the class, so reflection is pretty easy.

sm = AClass.statefully
states = sm.states # instances of Stateful::State

sm.valid? :foo => :bar # => false, or whatever

# If the target model class is a descendant of ActiveRecord::Base,
# Statefully will automatically mix in a DB-aware persister.
# otherwise, it's just an attr_accessor called 'current_state'.

# Want to see what's going on?
# include Stateful::Tracing
#
# it'll use the Rails logging bits if they're around, etc, etc.

# Need to audit your state moves?
# include Stateful::Timestamps
#
# it'll set attributes for state_at and event_fired_at if respond_to?

# On any of the state (entering, entered, exiting) or event (firing, fired)
# listeners, not providing a state or event list means you'll hear about 'em all.

AClass.statefully do
  entering { }
  entered { }
  exiting { }
  firing { } 
  fired { } 
end

# want to make a diagram? there's really really crude support for generating
# a graphviz file built in:

File.open("machine.dot", "wb") do |f|
  f.write(AClass.statefully.to_dot)
end

Installing

# FIXME: 1.0 isn't released yet
[sudo] gem install stateful

Installing Development Versions

We periodically update Stateful’s gemspec on Github. Rather than installing the official releases (hosted on RubyForge), you can track the development version. Development versions will have a timestamped version number, like 1.0.0.200807060242.

[sudo] gem sources -a http://gems.github.com   # if you haven't already
[sudo] gem install jbarnette-stateful

License

Copyright 2008 John Barnette

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.