State-Fu

What is it?

State-Fu is:

  • a generalized, extensible framework for state-oriented, event-driven programming in Ruby.
  • a rich DSL for describing workflows, rules engines, behaviours and processes
  • useful both as a Rails plugin, and in standalone ruby programs without any additional dependencies.

It lets you describe:

  • series of discrete states
  • events which allow transitions to occur between states
  • rules (requirements) about when these transitions can occur
  • behaviours which occur when they do

Which adds up to a surprisingly useful way to skin a lot of problems

Other libraries exist for ruby which do some or all of these things. “What’s different about State-Fu?”, you may ask.

Why StateFu is not your grandmother’s state machine

Flippant answer:

Those libraries you’ve played with are toys. They’re made of plastic. State-Fu is forged from a reassuringly dense but unidentifiable metal which comes only from the rarest of meteorites, and it ticks when you hold it up to your ear.1

State-Fu is elegant, powerful and transparent enough that you can use it to drive substantial parts of your application, and actually want to do so.

It is designed as a library for authors, as well as users, of libraries: State-Fu goes to great lengths to impose very few limits on your ability to introspect, manipulate and extend the core features.

It is also delightfully elegant and easy to use for simple things:



  class Document < ActiveRecord::Base
    include StateFu

    def update_rss
      puts "new feed!"
      # ... do something here
    end

    machine( :status ) do
      state :draft do
        event :publish, :to => :published
      end

      state :published do
        on_entry :update_rss
        requires :author  # a database column
      end

      event :delete, :from => :ALL, :to => :deleted do
        execute :destroy
      end

      # save all states once transition is complete.
      # this wants to be last, as it iterates over each state which is
      # already defined.
      states do
        accepted { object.save! }
      end
    end
  end

  my_doc = Document.new

  my_doc.status                          # returns a StateFu::Binding, which lets us access the 'Fu
  my_doc.status.state     => 'draft'     # if this wasn't already a database column or attribute, an
                                         # attribute has been created to keep track of the state
  my_doc.status.name      => :draft      # the name of the current_state (defaults to the first defined)
  my_doc.status.publish!                 # raised =>  StateFu::RequirementError: [:author]
                                         # the author requirement prevented the transition
  my_doc.status.name      => :draft      # see? still a draft.
  my_doc.author = "Susan"                # so let's satisfy it ...
  my_doc.publish!                        # and try again.
  "new feed!"                            # aha - our event hook fires!
  my_doc.status.name      => :published  # and the state has been updated.

Feature Comparison and Detailed Answer

A few of the features which set State-Fu apart for more ambitious work are:

  • a lovely, simple and flexible API gives you plenty of choices about how to describe your problem domain.
  • define any number of workflows on the same object / model; workflows (Machines) can be entirely separate, or interact with each other. Re-use machines across multiple classes, serialize them to a database, or build them on the fly.
  • events can transition from / to any number of states
  • drive application behaviour with a rich set of event hooks
  • define behaviour as methods on your objects, or keep it all in the state machine itself
  • requirements determine at runtime whether a particular state transition can occur, and if not, can tell a user (or developer) what they must do to satisfy the requirements.
  • requirement failure messages can be generated at runtime, making use of whatever application and state-machine context they need
  • transitions can be halted mid-execution, and you can actually determine why, and where from
  • in every event hook, requirement filter, and other method calls, you have complete and consistent access to your classes, its StateFu::Machines, and any Transition context. Use real ruby code anywhere, without breathing through a straw!
  • extend State-Fu (with Lathe#helper) Binding and Transition instances for your Machine to define your a DSL customized for your problem domain, and to keep your class definitions clean. Helper methods have easy access to all the context associated with your object instance, its StateFu::Machines and any Transition in progress.
  • extend a State-Fu Lathe (with Lathe#tool) to keep your Machine definitions DRY
  • store arbitrary meta-data on any component of State-Fu – a simple but extremely powerful tool for integration with almost anything.
  • designed for transparency, introspection and ease of debugging, which means a dynamic, powerful system you can actually use without headaches.
  • flexible and helpful logging out of the box – will use the Rails logger if you’re in a Rails project, or standalone logging to STDOUT or a file. Configurable loglevel and message prefixes help StateFu be a good citizen in a shared application log.
  • magically generate diagrams of state machines / workflows with graphviz
  • “magically” use an ActiveRecord field for state persistence, or just an attribute – or use both, on the same class, for different workflows. If an ActiveRecord field exists for a machine’s field_name (by default, the machine’s name suffixed with ‘_field’; the default machine name is ‘state_fu’, so if you don’t explicitly name a machine it will look for ‘state_fu_field’ in your ActiveRecord columns (if ActiveRecord is included) and use that. Otherwise, an attr_accessor will be used (and created, if necessary).
  • customising the persistence mechanism (eg to use a Rails session, or a text file, or your choice of ORM) is usually as easy as defining a getter and setter method for the persistence field, and a rule about when to use it. If you want to use StateFu with a persistence mechanism which is not yet supported, I’d like to hear about it.
  • fast, lightweight and useful enough to use in any ruby project – works with Rails but does not require it.

State-Fu works with any modern Ruby ( 1.8.6, 1.8.7, and 1.9.1)

1 No disrespect intended to the authors of other similar libraries - some of whom I’ve borrowed an idea or two, and some useful criticism, from. They’re stand-up guys, all of them. It’s the truth though.

I’d like to thank Ryan Allen in particular for his Workflow library, which I previously forked, piled hundreds of lines of code into and renamed Stateful (now deprecated). Some of his ideas (for example the ability to store metadata easily on everything ) have been instrumental in State-Fu’s design.

I’d also like to tip my hat at John Barnette, who’s own (coincidentally named) Stateful set a very high standard with an exceptionally elegant API.

StateFu is not a complete BPM (Business Process Management) platform

It’s worth noting that StateFu is at it’s core a state machine, which strives to be powerful enough to be able to drive many kinds of application behaviour.

It is not, however, a “proper” workflow engine on par with Ruote. In StateFu the basic units with which “workflows” are built are states and events; Ruote takes a higher level view, dealing with processes and participants. As a result, it’s capable of directly implementing these design patterns:

http://openwferu.rubyforge.org/patterns.html

Whereas StateFu cannot, for example, readily model forking / merging of processes (nor does it handles scheduling, process management, etc.

The author of Ruote, the Ruby Workflow Engine, outlines the difference pretty clearly here:

http://jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/

If your application can be described with StateFu, you’ll likely find it simpler to get running and work with; if not, you may find Ruote, or a combination of the two, suits your needs perfectly.

Getting started

You can either clone the repository in the usual fashion (eg to yourapp/vendor/plugins/state-fu), or use StateFu as a gem.

To install as a gem:


gem install davidlee-state-fu -s http://gems.github.com

To require it in your ruby project:


require 'rubygems'
require 'state-fu'

To install the dependencies for running specs:


 sudo gem install rspec rr
 rake             # run the specs
 rake spec:doc    # generate specdocs
 rake doc         # generate rdocs
 rake build       # build the gem locally
 rake install     # install it

Now you can simply include StateFu in any class you wish to make stateful.

The spec/ and features/ folders are currently one of the best source of documentation. The documentation is gradually evolving to catch up with the features, but if you have any questions I’m happy to help you get started.

If you have questions, feature request or ideas, please join the google group or send me a message on GitHub.

A note about ActiveSupport

StateFu will use ActiveSupport if it is already loaded. If not, it will load its own (heavily trimmed) ‘lite’ version.

In most projects this will behave transparently, but it does mean that if you require StateFu before other libraries which require ActiveSupport (e.g. ActiveRecord), you may have to explicitly require 'activesupport' before loading the dependent libraries.

So if you plan to use ActiveSupport in a stand-alone project with StateFu, you should require it before StateFu.

Addditional Resources

Also see the issue tracker

And the build monitor

And the RDoc

Thanks

  • dsturnbull, for helping w/ a patch to stop an error being raised when an activerecord model has no database table
  • lachie, benkimball for pointing out README bugs / typos
  • Ryan Allen for his original Workflow library