state_machine: One machine to rule them all?

by aaron

After 2 1/2 years of blood, sweat, tears, and the occasional Jägermeister, I’m finally officially announcing a project I’ve been quietly working on: state_machine. Well, quietly until some people started mentioning it ;) This is a project which has undergone many rounds of rewrites, but which has finally met its goal, in my opinion, to become the easiest, sexiest, yet most powerful state machine library for the Ruby language.

LOTR reference? Really? What’s next, The Matrix?

Rather than sit here and beat about the bush, here’s a rundown of state_machine’s features (in no particular order):

  • Define multiple state machines on any Ruby class
  • Namespaced state machines
  • before/after transition hooks with explicit transition requirements
  • Integration with ActiveRecord / DataMapper / Sequel
  • State-driven instance / class behavior
  • State values of any data type
  • Dynamically-generated state values
  • Inheritance support
  • Simple instance/class method overrides
  • Internationalization
  • GraphViz visualization creator

Now if that list doesn’t get you up on the dance floor, we need to have a talk.

Never send a human to do a state machine’s job.

What’s a blog post about code without… well, code? Rather than bore you with words, here’s an example using state_machine with a Purchase DataMapper resource, written just for this blog :)

class Purchase
  include DataMapper::Resource
 
  property :id, Serial
  property :amount, Float
  property :state, String
  property :shipped_at, DateTime
  property :receipt, String
 
  state_machine :initial => :pending do
    before_transition :log_state_change
    after_transition  :pending => :authorized, :authorized => :captured do
      # Update RSS feed...
    end
 
    event :authorize do
      transition :pending => :authorized
    end
 
    event :reauthorize do
      transition :authorized => same, :if => lambda {|p| p.amount_diff < 15}
    end
 
    event :capture do
      transition [:pending, :authorized] => :captured
    end
 
    event :void do
      transition all - [:captured, :voided] => :voided
    end
 
    state :pending do
      def transaction_id
        nil
      end
    end
 
    state :authorized, :captured, :voided do
      validates_present :receipt
 
      def transaction_id
        id.to_s
      end
    end
  end
 
  state_machine :shipped_at, :initial => :pending, :namespace => 'shipment' do
    event :deliver do
      transition :pending => :delivered
    end
 
    state :pending, :value => nil
    state :delivered, :if => lambda {|value| value}, :value => lambda {Time.now}
  end
 
  def amount_diff
    amount - original_values[:amount]
  end
 
  def log_state_change(transition)
    event, from, to = transition.event, transition.from_name, transition.to_name
    DataMapper.logger.info("#{id}: #{from} => #{to} on #{event}")
  end
end

And now some fun actually interacting with the state machine:

purchase = Purchase.new         # => #<Purchase id=nil amount=nil state="pending"
                                #               shipping_status=nil receipt=nil>
purchase.amount = 5.0
purchase.state                  # => "pending"
purchase.state_name             # => :pending
purchase.pending?               # => true
purchase.can_authorize?         # => true
purchase.transaction_id         # => nil
purchase.save                   # => true
 
purchase.authorize              # => false
purchase.errors.full_messages   # => ["Receipt must not be blank"]
purchase.reload                 # => #<Purchase id=1 amount=5.0 state="pending"
                                #               shipped_at=nil receipt=nil>
purchase.receipt = 'abc123'
purchase.authorize              # => true
purchase.authorized?            # => true
purchase.captured?              # => false
purchase.transaction_id         # => "1"
 
purchase.authorize!             # => StateMachine::InvalidTransition: ...
 
purchase.shipped_at             # => nil
purchase.shipped_at_name        # => :pending
 
purchase.can_deliver_shipment?  # => true
purchase.deliver_shipment       # => true
purchase.shipped_at             # => #<DateTime: 106051652881/43200,-1/6,2299161>
purchase.shipped_at_name        # => :delivered
 
purchase.shipment_pending?      # => false
purchase.shipment_delivered?    # => true
 
Purchase.with_state(:authorized).count        # => 1
Purchase.without_shipped_at(:pending).count   # => 1
 
Purchase.state_machines.each {|name, machine| machine.draw}

Boom shakalak! That covers the basics, but notice that little nugget at the end? That draws a visual representation of the graph for those PowerPoint presentations you’ll be giving. Here they are (click for the full image):

purchase_shipment_status

What are we going to do tonight, Brain?

So that’s a taste of state_machine. Take a gander over to GitHub and read the docs. There’s lots more there.

Before I close out this post, I want to recognize a few people who asked the hard questions, contributed back, or provided inspiration for this library:

  • Scott Barron
  • S. Brent Faulkner
  • Pete Forde
  • Aaron Gibralter
  • Avdi Grimm
  • Michael Grosser
  • Tse-Ching Ho
  • Nate Murray
  • Sean O’Brien
  • Mike Perham
  • Brian Terlson
  • Dennis Theisen
  • Richard Titze
  • …and anyone else I missed!

While you’re here, don’t forget to follow the project on GitHub and help make state_machine a market leader!

Comments

49 Responses to “state_machine: One machine to rule them all?”

  1. Another state machine for Ruby - what_i_am.upto(2009) on March 9th, 2009 6:23 am

    [...] 2009, under Uncategorized Aaron Pfeifer from PluginAWeek has released a pretty decent state machine today. It’s got plenty of neat features like before- and after-transition hooks, state machine [...]

  2. Brian on March 9th, 2009 11:47 am

    Awesome work! As someone who reported a bug and offered suggestions over two months ago I feel partially responsible for this ;) Anyway, definitely the best state machine library there is — I am happy to finally be able to use something other than AASM.

  3. aaron on March 9th, 2009 9:13 pm

    @Brian – Recognition list updated! First round is on me if you ever find yourself in Boston ;)

  4. Markus on March 10th, 2009 6:56 am

    Cool! The library looks awesome. I’m a big fan of state machine plugin (aka AASM), and always feel the need for a bit more functionality and options. I’ll try this right away.

    One thing you might consider interesting is to add a “!” to event methods, I liked this feature in AASM, it’s a bit more clear of what are the event doing (I think).

    Cheers.

  5. aaron on March 10th, 2009 8:38 am

    @Markus – This is actually available in state_machine, though with different semantics. In this case, the “bang” event will raise an exception if it can’t transition for any reason. There’s an example of that in the code in this post when we call:

    purchase.authorize!  # => StateMachine::InvalidTransition: ...
  6. Markus on March 10th, 2009 10:47 am

    @aaron: I hadn’t read the whole example, sorry. It’s exactly the functionality I was talking about.

  7. David Backeus on March 11th, 2009 6:13 am

    Wow. Finally a state machine done right. Thank you!

  8. Dr Nic on March 12th, 2009 2:14 am
  9. aaron on March 12th, 2009 10:56 pm

    @Dr Nic – Good stuff! Thanks for the heads up. Now, if only I had a Mac… :)

  10. Chris on March 13th, 2009 6:40 am

    I would be interested to see how you would actually use the Purcahse class within an application, for example a rails application. At some point in the code (the controller) do you not have a long list of if-else or case statements? :

    if purchase.can_deliver_shipment?

    elsif purchase.shipment_delivered?

    end

  11. Chris on March 13th, 2009 6:40 am

    Is there a decent ruby library that shows FSMs like this being used effectively?

  12. aaron on March 13th, 2009 9:06 am

    @Chris – I think it’s easiest to look at this from the public API perspective. Imagine if you’re building a payment gateway like PayPal, you might end up seeing something like:

      class PurchasesController < ApplicationControler
        ...
        def capture
          if @purchase.capture
            head :ok
          else
            render :xml => @purchase.errors, :status => :unprocessible_entity
          end
        end
      end

    That’s a very simple example… As far as open libraries that use state_machine, I know Spree uses it as well as (I believe) the guys over at Shopify. For an exhaustive list of projects using an alternative like aasm, you could search Github.

    Hope this helps!

  13. Nat on March 14th, 2009 2:14 pm

    Hi!

    I wonder, if there’s a way to trigger event or transition on my activerecord attributes (actually has_many association) change?

  14. aaron on March 14th, 2009 5:08 pm

    @Nat – It’s an interesting question … Unfortunately, I think you’d end up running into a few issues with that approach once you start thinking about multiple state machines, loopbacks, and making it work across both Ruby classes and ORM integrations like ActiveRecord. It’s possible that it’s a solvable problem, but it makes for a more complex integration with the class containing the state machine.

    The other issue this raises is that it would slightly change the semantics of the before_transition / after_transition hooks. In your use case, before_transition callbacks would end up getting invoked after the attribute’s value has already transitioned. On the other hand, the current implementation of events has before_transition callbacks getting invoked before the attribute’s value has been transitioned.

    Nonetheless, I’m always open to suggestions and patches :) Made for a great mental exercise thinking through the implications!

  15. Paweł Kondzior on March 15th, 2009 6:12 am

    How to extend State ? To be able for example pass some arguments for events and be able to use this values in before_transition and after_transition. Let say we have event:

    event(:resolve) do
    transition …
    end

    @ticket = Ticket.find(:first)
    @ticket.resolve(‘Ticket resolved thank you’)

    Right now the only way to do this will be override resolve method in model, it would be awesome to have more direct way to do this

  16. aaron on March 15th, 2009 10:07 am

    @Pawel – That’s another great question and I’m glad you brought it up. The ability to pass in arguments was originally in state_machine until I found that the complexity it introduced wasn’t worth it. The biggest problem (at the time) turned out to be how to properly handle a before/after transition callback for multiple events when those events took different arguments.

    Given that, I think that the recommended way for accepting additional arguments (without passing them to transitions) is still by overriding the method like so:

    class Ticket
      ...
      def resolve(message, *args)
        this.message = message
        super(*args)
      end
    end

    Now, I’m not necessarily saying that there is no use case for being able to access the entire arguments list from a transition callback. If there is, this is how I think it would work:

    class Ticket
      state_machine do
        before_transition :on => :resolve do |ticket, transition|
          message = *transition.args
          ...
        end
      end
     
      def resolve(message, *args)
        this.message = message
        super
      end
    end

    I would love to hear this discussed in a lighthouse ticket and see some example use cases.

    Update: After some offline discussion, this was implemented.

  17. Ennuyer.net » Blog Archive » 2009-03-15- Today’s Ruby/Rails Reading on March 15th, 2009 5:22 pm

    [...] state_machine: One machine to rule them all? | PluginAWeek [...]

  18. Paweł Kondzior on March 15th, 2009 6:06 pm

    I’m impressed :) Big thanks!

  19. Brian on March 15th, 2009 8:43 pm

    This looks great, just missing one thing: a gem. Maybe I’ll fork it and create one.

  20. aaron on March 15th, 2009 8:47 pm

    @Brian – You should be able to install the latest version via

    gem install state_machine
    
  21. rebotfc on March 15th, 2009 9:39 pm

    For those who are using state_machine with rails, just wondering how it is being implemented whilst retaining a RESTful application.

    I am currently using a #action_event=() virtual attribute on my stateful models which accepts a string of event names on UPDATE, which then triggers the respective event.

    This seems to work well however my edit view gets a little unwieldy with multiple case/when statements determining which forms to show.

    Unfortunately this also means dirtying the controller with an @action_event set by params.

    I would love to hear if there was a better/cleaner way to implement this.

  22. aaron on March 15th, 2009 10:27 pm

    @rebotfc – For rendering, you might want to try something like the following if you need to render based on state:

    render :partial => "purchase/#{@purchase.state_name}", :object => @purchase

    You could also do something similar in the controller with states. If you wanted to render differently based on the event being taken, then I think you can still do it this way, but instead based on some parameter (like action_event as you described) instead of the record’s state.

    For doing object updates, I tend to break the pure RESTful standard and expose events as controller actions. If I were to follow the standard RESTful actions, then what you’ve described is probably a decent way of doing it… something like:

    class PurchasesController < ApplicationController
      def update
        @purchase.attributes = params[:purchase]
        if @purchase.run_action # Internally validates the action_event attribute and invokes it
          ...
        else
          ...
        end
      end
    end

    Those are some ideas… :)

  23. almost effortless » Weekly Digest, 3-15-09 on March 15th, 2009 10:34 pm

    [...] state_machine After 2 1/2 years… I’m finally officially announcing a project I’ve been quietly working on: state_machine… This is a project which has undergone many rounds of rewrites, but which has finally met its goal, in my opinion, to become the easiest, sexiest, yet most powerful state machine library for the Ruby language. [...]

  24. rebotfc on March 15th, 2009 10:35 pm

    Aaron, thanks those ideas definitely help.

    state_machine rocks!

  25. Brian on March 16th, 2009 9:22 pm

    Thanks, I am a dumb-ass, I didn’t see them gem icon on github and for some reason I didn’t bother to check rubyforge. I have a need for more than one “state” for some models and was thinking about doing something like this myself, but I’m glad you beat me to it. Thanks.

  26. Pete Forde on March 17th, 2009 2:50 am

    Hey Aaron!

    Congratulations on the official release, and thanks for the mention. Oddly, I actually just met Brent Faulkner at a bachelor party a few months ago! Small world.

    Hope you make it up for http://futureruby.com/ as we can safely say it’ll be a good time.

  27. John Wells on March 17th, 2009 4:38 pm

    I’m really digging state_machine. However, I need the ability to define dynamic states. I’m using state_machine within ActiveRecord, and for reasons I won’t dive deeply into, I need to create state names based on an objects properties.

    So, for example, you’d have something like:

    state_machine :initial => :created do 
      event :approve do
        transition :created => lambda {|model|model.manager.name}
        transition :lambda {|model|model.manager.name} => lambda {|model|model.director.name}
        # etc, etc
      end
    end

    This forms a dynamic approval hierarchy based on a specific user (as opposed to roles, which I already have working).

    I saw some notes in the changelog from 2008 that seems to indicate this is supported, but my initial attempts are not working. Is this still supported, and if so, is it documented anywhere?

    Thanks! Awesome work all around!

  28. John Wells on March 17th, 2009 4:40 pm

    Btw, a Google Group would probably be very nice ;-). I see this plugin getting a *lot* of use…

  29. aaron on March 17th, 2009 10:02 pm

    @John – Thanks for the comment. First thing… there’s already a Google Group :) Check out PluginAWeek: Talk and PluginAWeek: Core.

    Now for your use case.. I’m not convinced yet that a state machine should be allowed to be defined with state names that are unknown until runtime. I think being able to do so would break some important contracts of the state machine.

    On the other hand, there is a case for dynamic state values. This is something that’s demonstrated with the shipped_at field in this post’s example. Although there is currently no way to access the object when dynamically generating the value, I imagine adding it like so:

    state_machine :initial => :created do
      event :approve do
        transition :created => :manager_approved, :manager_approved => :director_approved
      end
     
      state :manager_approved, :value => lambda {|record| record.manager.name}
      state :director_approved, :value => lambda {|record| record.director.name}
    end

    In this case, the state name is “manager_approved”, but the value that’s actually stored when in that state is the name of the manager.

    Does this make sense and/or solve your problem?

  30. John Wells on March 17th, 2009 10:19 pm

    aaron, thanks for your quick reply.

    I think that will probably work, if I understand it correctly. The way our system currently works, a user’s “worklist” follows the convention of being workitems in states that match (literally…a string match) one of the user’s roles within the system (i.e., manager or director), or the user’s login name. This is necessary because sometimes a workflow follows cleanly through a set of roles, but other times it needs to flow directly through a chain of certain people, if that makes sense. It may seem odd, but it works within the context of what we’re doing.

    If I understand you correctly, you’re saying that I can set a value that ends up being the value persisted in the database table (in the case of ActiveRecord). If this is so, it should work perfectly. I’m a little thrown by your comment “Although there is currently no way to access the object when dynamically generating the value, I imagine adding it like so”, I am sure to find out if it works tomorrow. ;-)

    Thanks!
    John

  31. aaron on March 18th, 2009 8:41 am

    @John – Sorry, that was a bit cryptic :) What I meant by that is that my example isn’t currently possible… it was more of a “if this is how you could do it, this is how it would look”. I’m looking into how it could be implemented and there are already a few gotchas, but keep an eye on the git repository.. I’m cautiously optimistic :)

  32. John Wells on March 18th, 2009 9:10 am

    aaron,

    Gotcha. Ok, I’ll find a work around in the meantime and be on the watch for changes. Thanks!

    John

  33. Alex on March 19th, 2009 2:41 pm

    Hello, small question: event of creation of an object with :initial=>:newly_created state — is it a transition?

    I tried:
    before_transition all => :newly_created , :do => :alarm
    after_transition all => :newly_created , :do => :alarm
    None was triggered.

    Does it mean that initial state has no callbacks? (Workaround would be calling :alarm in object initialize.)

    Thanks.

  34. aaron on March 19th, 2009 10:41 pm

    @Alex – Not currently… Callbacks only get invoked within the context of an event. I would recommend a workaround either as you stated or with an after_create. It’s a good question and one that should be properly documented. I’ve created a ticket to track this.

  35. Jamie on March 20th, 2009 12:31 pm

    I’ve been using state_maching to develop an session API for a chat application and it’s got pretty complicated. state_machine made the code so much cleaner and easier to work with, fantastic bit of code.

  36. Brian on March 22nd, 2009 4:29 pm

    One thing I can’t seem to figure out is how to easily enumerate the list of state names for a given state machine to use in a drop-down list.

  37. aaron on March 22nd, 2009 4:32 pm

    @Brian – I’m a step ahead of you :) Wait for the 0.7.0 release (soon) and you’ll see examples on how it can be achieved with the new features that have been added recently (or will soon be).

  38. John Wells on March 23rd, 2009 1:58 pm

    @Brian, I think the following might work. If you are, for example, using state_machine to create a state_machine in an AR Model (let’s say Car), you can do:

    Car.state_machines[:state].states

    to get at the states, and then do things like:

    Car..state_machines[:state].states.by_priority.each do |s|
    puts s.name
    end

    Explore the API to see other examples.

    Although, @aaron will know much more about this than me…but it seems that you don’t have to wait on 0.7.0 to get at them.

    HTH,
    John

  39. Scott on March 25th, 2009 3:41 pm

    I started trying to do a project using AASM and was maintaining my own patches to fix the callback ordering and behavior. I found state_machine, rewrote my state machines to use it instead of AASM in an hour, and haven’t had to fight with any of the callback problems since. AWESOME WORK!!!

  40. aaron on April 5th, 2009 4:36 pm

    @John – You’re exactly right… I misinterpreted the question as a list of states that are allowed to be transitioned to from the current state. Thanks for helping out!

    @Scott – Great feedback! Glad to hear it’s helped :)

  41. Phillip Med on May 6th, 2009 3:08 pm

    is there a group out there for support and for installation instructions? Thanks

  42. Phillip Med on May 6th, 2009 3:24 pm

    Actually i read above..and found it…….

    I keep getting this:

    undefined method `state_machine’ for #

  43. aaron on May 6th, 2009 8:13 pm

    @Phillip You can post messages on the PluginAWeek-Talk Google Group. It looks like your error message didn’t quite make it through the filters. My guess based on what made it through is that the library wasn’t loaded properly (assuming you’re loading it via a gem). If you post something up on the Google Group, myself or others would be more than happy to help out there. Best of luck! :)

  44. Ennuyer.net » Blog Archive » 2009-05-09- Today’s Ruby/Rails Reading on May 9th, 2009 5:55 am

    [...] state_machine: One machine to rule them all? | PluginAWeek [...]

  45. Ennuyer.net » Blog Archive » I am way behind on my rails link blogging. Link dump and reboot. on May 9th, 2009 6:48 am

    [...] state_machine: One machine to rule them all? | PluginAWeek [...]

  46. cies on September 18th, 2009 9:25 am

    this code is soooo pretty — it nearly made me cry..

    thanks! im gonna use it with dm tomorrow (ok, i lie: monday).

  47. fisons on September 24th, 2009 4:05 am

    I watch you! (@github)

  48. Aaron Cohen on February 19th, 2010 6:53 pm

    If a transition fails due to a validation on the state being transitioned to, how can one determine which validation caused the failure?

    I have a very simple case (everything non-essential has been trimmed) -

    state_machine :status, :initial => :unpublished do
    event :publish do
    transition :unpublished => :published
    end
    state :published do
    validate :start_at_in_future
    end
    end

    def start_at_in_future
    errors.add(:start_at, ‘must be in the future’) unless start_at > Time.zone.now
    end

    should “validate that the start_at is in the future to perform a publish transition” do
    @deal.start_at = Time.zone.now – 1.second
    assert @deal.can_publish? # wish this would return false, but understand why it doesn’t
    assert !@deal.publish
    assert !@deal.published?
    pp @deal.errors.inspect # no errors; how to tell the web GUI what went wrong?
    end

    Thanks for any advice/recommendation you have!!

  49. aaron on March 14th, 2010 5:25 pm

    Aaron Cohen – Please ask questions like this on the mailing list if you still need help. Thanks :)