state_machine 1.0: Jump for joy!

by aaron

Today I’m proud to announce that state_machine 1.0 has been officially released. This release marks the end of a long journey to build a library that brings the simplicity and power of state machines to Ruby and its major ORM frameworks. In case you missed it, we’re celebrating the release tonight at the Asgard Irish Pub & Restaurant in Cambridge, MA.

Individually, we are one drop. Together, we are an ocean.

In past announcements, I’ve always ended with a special thank you to everyone who contributed to the release. Today, however, I feel it’s important to highlight this project’s group of contributors first, not by name but as a whole. An enormous number of individuals (over 100!) have, directly or indirectly, contributed toward this effort. Moreover, many open-source projects have inspired several of the features now found in state_machine.

I can’t possibly express the gratitude we all have for everyone who has been involved.

A retrospective: Taking a look back

While much of what I’ve contributed to the community has come in the form of code, I thought it might be useful to take a few moments to reflect on what I’ve learned, as the core contributor of this project and a member of the Ruby community. If any of the following seems like a regurgitation of the same things other folks have spoken about before me, consider this at least a testimony to those experiences.

Fragmentation can lead to unity

Like most open-source projects, state_machine was initially built out of a need for specific functionality that was not offered at the time nor could be easily extended onto existing libraries. When it became a part of the PluginAWeek project, it was just a budding seed of the library it is today. At that time, there was a significant fragmentation of features both between the various Ruby state machine projects and within their own forks. They all had similar goals, but marched to a different beat.

To better understand the fragmentation of these libraries meant reading through Rubyforge / Github projects, project forks, and community forums to find out what features were important and how they were used. This research proved invaluable as it provided the information needed to unite of all of these fragmented ideas and assemble them under a single vision of how they could work together.

Success means sharing a single vision with an open mind

As I began to realize the potential for the project, I had a single, simple vision: to build a library that brought an array of complex state machine features into a simple, united interface and that could be officially endorsed by the major Ruby ORMs.

The former part of that vision is something that every project with even a little popularity will encounter with difficulty. As state_machine began to grow, so did the number of use cases and features that its users wanted to have. However, many of these suggestions were made with only a partial view of the world. My job was to filter through those and find out how they fit into the single vision I had for state_machine.

I discovered during this process that it was critical to address every problem or feature that a user reported. If I could not suggest the right way to address the issue, then there was a flaw in the library. That could sometimes mean spending hours, days, or weeks thinking about an issue before coming up with the right solution. Sometimes the solution was simply an explanation of why things work the way they do… other times it meant building a new feature that directly or indirectly solved the issue.

While I understand that being the single gatekeeper to this project slowed its progress significantly, I also attribute much of its success to the fact that every single piece of functionality that was added was rigorously scrutinized and made sense with the whole vision in mind.

Setting standards sets expectations

I mentioned earlier that one of the goals for this project was to build it in such a way that it could be endorsed by the major Ruby ORMs. The primary reason for this was so that the community could be provided with a standard interface for building state machines which was consistent across ORMs, similar to how the i18n or activemodel libraries have been embraced.

To that end, I set four critical standards for this project:

  1. Every feature must be implemented to the highest degree of quality, both from a functional and design perspective
  2. Every change must be backed by well-written unit tests to ensure functional quality
  3. A significant amount of documentation must be provided for every feature, whether part of the public or private API
  4. Support must be provided, at the very least, in the earlier stages of the project

Everyone’s encountered their fair share of libraries that have failed to provide at least one of the above standards. It can be a frustrating, and unnecessary, experience. You can choose to control the expectations for your project if you set simple standards like this.

Not all ORMs are created equal

It was about two and a half years ago when I made the decision to rewrite state_machine with the ability to be used within a simple Ruby class or within an ORM library like ActiveRecord or DataMapper. Since then, integration support has been added for ActiveModel, Mongoid, MongoMapper, and Sequel. The upshot of having all of these integrations is twofold: new integrations can reveal flaws in the design of your project (such as a lack of interface hooks) and they can open up a bigger audience of users. The downside is that not only are these projects moving at a rapid rate of development (which means constantly keeping up with all of them), but they vary significantly in ease of integration.

Reading through the state_machine integration for each of these ORMs can reveal a great deal about their design for extensibility. First, I have to commend the Rails folks for building ActiveModel and helping the community move to a standardized interface. This has been a huge success (as I’m sure it has with others) in helping to simplify integrations. Second, John Nunemaker and the MongoMapper developers deserve an extra mention for building the easiest library to extend. ActiveRecord and Mongoid were not much harder, but they have room to improve. (addendum: I accidentally confused the Mongoid and MongoMapper integrations the first time. I’ve since updated to indicate that MongoMapper has been the easiest)

With praise, however comes criticism (in the constructive way, of course). Of all the integrations, I found Sequel to be the most frustrating library to integrate with. It lacks many of the hooks found in other libraries and will likely be the most difficult to maintain moving forward. (addendum: Please don’t take this criticism the wrong way… I purposefully didn’t go into details here. We’ll have to work together to improve things… that’s how these things go :))

Moving targets need to be managed

Every project moves at its own pace and, at least in the post-GitHub world, that pace can be at a breakneck speed. As ORMs make significant changes to their public APIs, updates often need to be made to the associated state_machine integration as a result. Supporting multiple versions of a library can be an minor inconvenience; supporting multiple versions of multiple libraries can be a major problem.

The only way to keep up with moving targets like this is to provide a framework for managing it. In addition, it should ideally be done in a way that keeps it simple for developers and allows them to continue to get new features even for older ORM integrations. By designing each version to act like an integration itself, this allowed for new features to be added and version-specific issues to be fixed with ease even as the ORMs continued rapid development.

Open-source projects require good management… and lots of time

Open-sourcing a small library you whipped up over the weekend or while building a feature at work is easy. Managing that project as it begins to get followers and takes on a grander vision can be difficult. More than anything, I’ve discovered that the hardest part is finding the time to work on it. Granted this is the beauty of open-source, and GitHub in particular, where the masses can help build new features and fix bugs… and all I have to hit is the one-click merge button. However, it’s not always that simple.

As I mentioned earlier, a project like state_machine can require a much more hands-on management style where every change is assessed for consistency, compatibility, quality, and more. After you take into account acting as the support contact, being a core developer, and more, the time adds up.

Despite the difficulties associated with managing a project like this, it’s been completely worth it. When developing software is a hobby, it should be fun… and it is.

Thank you

I’m sure there are other things we could talk about, but I feel like I’ve yapped for long enough. As a Ruby / Rails engineer, I’ve been fortunate enough to have been able to be in a job where so much of the software we take advantage of every day was built and open-sourced by our fellow engineers. I can think of no better way to pay those gestures forward than by making my own contribution back to the community.

Oh, and before I forget… hitting 1.0 wasn’t the only milestone we hit this week. As of Monday, state_machine crossed the 1,000 followers mark over at GitHub thanks to Patrick Klingemann and the 999 people before him. Awesome.

As always, be sure to follow the project on GitHub! Happy state machining!

state_machine 1.0: Release party Thurs, May 12

by aaron

What’s better than seeing a project you’ve put your blood, sweat, and tears into for 5 years finally reach its goal? Drinking while doing it. About 2 months ago, I hailed Duke Nukem to announce plans for reaching the state_machine 1.0 milestone. Today we’re getting the ball rolling and putting those plans into action.

I’m not drunk, I just have a slight chemical imbalance.

Join me Thursday, May 12 at the Asgard Irish Pub & Restaurant in Cambridge, MA at 7:30pm or later to celebrate the release of state_machine 1.0. Come by, say hello, and let me buy you a drink (or two)! …and if you’ve used state_machine, I’d love to hear your war stories!

If you see one or more guys wearing a “Viximo” t-shirt, you know you’ve found the right place. I’ll be wearing one. I look forward to seeing you there!

P.S. This is your last chance to get a change in (bug fix, documentation or otherwise) for state_machine 1.0. If you’ve been procrastinating, better act fast!

state_machine 0.10.0: Let’s rock!

by aaron

As excited as I am to receive my pre-ordered copy of Duke Nukem Forever and quench my 14-year thirst, I’m even more excited to announce the official release of state_machine 0.10.0. After staying underground for more than 7 months, it’s time to come out like Punxsutawney Phil and give some good news! Check out all of the goodies you’ll discover in this release!

It’s time to kick ass and chew bubble gum… and I’m all outta gum.

As was the case with the last major release (0.9.0), the theme of this one continues to be the stabilization of public APIs and integrated ORMs in support of a 1.0 release. With that in mind, this release comes packed with a few major features and upgrades including:

  • Mongoid 2.0.0+ (rc7 or higher) support
  • Upgraded support for MongoMapper 0.9.0+ (including i18n and observer hooks)
  • Path analysis via {state}_paths
  • after_failure callbacks
  • Access to transition context during state_machine exceptions
  • Framework for simplifying the integration of legacy ORM versions
  • Nested attribute assignment support in ORM integrations
  • Optional guard bypass when accessing available events / transitions / paths
  • …and 5 bug fixes along with lots of refactoring

Here’s a very brief look at some of the new features:

Mongoid support – As expected, state_machine definitions are just like any other ORM:

class Vehicle
  include Mongoid::Document
 
  state_machine :initial => :parked do
    before_transition :parked => any - :parked, :do => :put_on_seatbelt
 
    event :park { transition [:idling, :first_gear] => :parked }
    event :shift_up { transition :idling => :first_gear }
 
    ...
  end
end
 
Vehicle.with_states(:parked, :idling) # => [#<Vehicle _id: ... >, ...]

Path analysis – Previously, you could only find the *next* set of transitions via {state}_transitions. Now you can see the whole set of transition paths for an object:

# Building on the example above...
vehicle = Vehicle.new
vehicle.state = "idling"
vehicle.state_paths
# => [
#  [#<StateMachine::Transition event=:park>],
#  [#<StateMachine::Transition event=:shift_up ...>,  #<StateMachine::Transition event=:park ...>]
# ]
 
vehicle.state_paths.to_states
# => [:first_gear, :parked]
 
vehicle.state_paths.events
# => [:shift_up, :park]
 
# Find all paths that start and end on certain states
vehicle.state_paths(:from => :first_gear, :to => :parked)
# => [
#  [#<StateMachine::Transition event=:shift_up ...>]
# ]

after_failure callbacks – Allows you to hook into transitions that fail to complete successfully. Note this replaces the :include_failures option on after_transition hooks.

class Vehicle
  state_machine do
    after_failure do |vehicle, transition|
      logger.error "Vehicle #{vehicle} failed to transition on #{transition.event}"
    end
 
    after_failure :on => :ignite, :do => :log_ignition_failure
    ...
  end
end

See the full list of changes here.

Hail to the contributors, baby!

As always, this release could not have been accomplished without support or inspiration from its community. In particular, these folks deserve a drink on me:

  • VChad Boyd
  • Morgan Christiansson
  • Karim Helal
  • Jon Klein
  • Benjamin ter Kuile
  • Dmitriy Landberg
  • Alexey Palazhchenko
  • Kaleem Ullah

If I missed anyone, please let me know!

We meet again, Doctor Jones!

It was almost a year ago when I asked for your help in making state_machine 1.0 a reality. Although I expected the library to be in feature lockdown, I decided that this interim 0.10.0 release was necessary for it to reach a stable state. Without your help, state_machine would never have become what it is today.

Like last time, I humbly ask for your help to get to 1.0. Report bugs, spread the word, and help me reach this goal by May 2011.

As always, don’t forget to follow the project! The bigger we can make this community, the more successful the project be!

Addendum: Please see state_machine 0.10.1 to avoid issues with multiple machines on a single class.

state_machine 0.9.0: Locked and loaded

by aaron

I‘ve decided the only way to go viral is to get a cat to use state_machine. I’m just having a hard time getting past that whole “cats can’t program” thing… so blogging it is! Last week, state_machine 0.9.0 was tagged after a month’s worth of active development. This is a major milestone that I’m really excited to share with all of you.

If you can dodge a bullet, you can dodge a ball. Wait, what?

If you’ve been following the project for a while, you’ll know that I only bump major versions when there are significant changes in either the public API or the feature set. Fortunately, this release comes loaded with a bunch of new features (and some fixes) that I hope you’ll all enjoy:

  • MongoMapper 0.5.5+ integration support
  • ActiveModel 3.0.0+ integration support (extracted from ActiveRecord)
  • DataMapper 0.10.3+ support
  • around_transition callbacks (supplements before/after callbacks)
  • Rails 3 railtie integration
  • Consolidated rake tasks
  • Unified API for event / attribute transitions
  • …and 7 bug fixes

To me, the most interesting part of this release has been building support for around_transition callbacks. Here’s a basic example of its usage:

class Vehicle < ActiveRecord::Base
  state_machine :initial => :parked do
    around_transition :on => :ignite, :do => :benchmark
    event :ignite do
      transition :parked => :first_gear
    end
  end
 
  def benchmark
    puts "...starting benchmark"
    time = Benchmark.measure { yield }
    puts "...completed in: #{time.real}"
  end
end
 
v = Vehicle.new
v.ignite
# ...starting benchmark
# ...completed in: 0.0642118453979492
v.state # => "idling"
 
v = Vehicle.new(:state_event => 'ignite')
v.valid?
# ...starting benchmark
v.state # => "idling"
sleep 1
v.save
# ...completed in: 1.00799298286438

See the full list of changes here.

I can only show you the door… unfortunately, it’s locked.

Besides addition of the above features, 0.9.0 is also a significant milestone as it marks the beginning of feature lockdown on state_machine. All future changes will be primarily for maintenance means (fixing bugs, compatibility with releases of integrated ORMs, etc.). This is the final push to get to an official 1.0.0 release.

However, I need your help to get to 1.0.0. If you encounter bugs in any part of the code or see any issues with the documentation, now is the time to report them.

Otherwise, enjoy this release :)

state_machine 0.8.1: The Ocho

by aaron

If you had asked me 6 months ago when the next release of state_machine was going to be, Pi Day 2010 would probably not have been my first guess. If it had been, I probably would have asked you to admit me into a hospital so I could be treated for delirium. Then again, what better way to celebrate such an important holiday?

Be a thinker, not a stinker. Or at least take a shower. Seriously.

Without further ado, I am excited to announce the release of state_machine 0.8.1. This is the most stable, feature-rich release yet. Given that I never officially announced the 0.8.0 release 6 months ago, below is a brief overview of everything that comes with the Ocho package:

  • ActiveRecord 2.0 and 3.0 support (Rails 3 users, rejoice!)
  • DataMapper 0.10 support
  • ruby-graphviz 0.9 support
  • Full compatibility with ORM dirty attribute tracking
  • Default state-driven behaviors
  • More control over whether after_transition callbacks get run after failed actions
  • Define multiple callbacks in a single before/after_transition call
  • ActiveRecord i18n support for state / event names
  • Query available state transitions with :from / :to / :on requirements
  • Earlier access to initial states during the initialization of an object
  • …and 20 bug fixes!

Here’s a very small bit of code demonstrating some of the visibily new features:

class Vehicle
  include DataMapper::Resource
 
  property :id, Serial
  property :state, :String
 
  state_machine do
    before_transition all => :parked, :do => [:set_alarm, :lock_doors]
    after_transition :do => :log_transition, :include_failures => true
 
    event :ignite { transition :parked => :first_gear }
    event :park { transition :first_gear => :parked }
 
    state :first_gear do
      def speed
        10
      end
    end
  end
 
  def speed
    0
  end
end
 
vehicle = Vehicle.new
vehicle.state_transitions
vehicle.state_transitions(:to => :parked)

See the full list of changes (and follow the project!) here. If you are on any other version, I strongly suggest (using <strong> tags, too) that you upgrade to 0.8.1. You’ll thank me later. Or you’ll yell at me for not releasing this earlier :)

I’d like to thank everyone at the academy who I paid off to vote for me.

As with every release, none of this would have been possible without the help of every person who filed a bug, sent me an e-mail, made a pull request, or tried to stalk me. The following Rubyists were particularly instrumental in putting this release together:

  • Jeremy Wells
  • Joe Lind
  • Jon Evans
  • Sandro Turriate
  • Tim Pope

I didn’t hear no bell. My hearing aid must be broken.

So where do we go from here? The road to 1.0 has been simple for me from the get-go. Over the past year or so, we’ve seen many Rubyists work together to standardize commonly-needed features through libraries like rack, i18n, activemodel, etc. That was exactly the goal for state_machine 1.0… to build a standardized implementation for state machines and become the recommended implementation for at least 1 of the 3 major Ruby ORMs. And that’s not something I can do without your help.

Either way, I’d love to hear feedback from the community. What do you think the 1.0 release should be (or should include)?

Hope you enjoyed some Pi. Delicious, delicious 3.14159……..

Visualizing Thoughtbot’s OSS Ruby Inkling markets

by aaron

Last month, Thoughtbot decided to enable my gambling addiction by opening up an open-source prediction market to answer all the questions that keep you up at night like:

  • Who will win a Ruby Heroes Award at RailsConf 2009?
  • Which RESTful controller project should I use?
  • Which state machine project should I use?
  • What am I going to wear tomorrow?

What gambling addiction? You play poker by betting “pretty” colors.

To help visualize how these markets are playing out and for the obsessive compulsive, I wrote a little jQuery plugin which pulls the real-time follower data down from GitHub and generates a Google Chart with it (it’s a hack, I’ll admit it). Now you can see who’s the market leader faster than it takes for a flame war to start involving the words Twitter and Ruby.

Check it out (may take a second to load):

And all of that can be accomplished through just a little snippet of javascript:

<script type='text/javascript' src="http://www.pluginaweek.org/wp-content/themes/pluginaweek/jquery.rubymarkets.js"></script>
jQuery(function() {
  jQuery.rubyMarkets('#market-examples', [
    'RESTful controller',
    'state machine',
    'gem creation'
  ]);
});

I’m most intimidating if I leave my cards face up in poker. No one else thinks so.

Unfortunately, it would appear as of today that Thoughtbot’s Inkling markets account expired and you can no longer participate. Fear not, I’m sure they’re aware of it. Update: The markets are back. Don’t forget, there’s a $1,295 value prize on the line for those of you making good bets. In the meantime, you can see a full visualization of the markets (using the above plugin) and their respective leaders here:

Open-source Ruby Prediction Markets

Feel free to embed these graphs on your own website using the plugin. Wear your market badge proud. Show those wall street brokers who’s boss.

state_machine 0.7: Better integrations, more access

by aaron

Fast and furious. That’s how development’s been on state_machine over the last month (and is also what happens to sequels when you run out of bad ideas). Yesterday, 0.7 was officially added to the state_machine family of releases. With this release comes better integrations, easier access to more state information, and the all new and improved Done Right™. Check out the changelog for a detailed list of changes.

REST and events – Events are auto-fired when a machine’s action is called (or when validations run) based on the value of #{attribute}_event:

purchase = Purchase.create  # => #<Purchase id: 1, state: "pending">
purchase.state_event        # => nil
 
purchase.update_attributes(:state_event => 'authorize')
purchase.state              # => "authorized"
purchase.state_event        # => nil
 
purchase.state_event = 'capture'
purchase.valid?             # => true
purchase.state              # => "captured"
purchase.save               # => true

This means that your create/update controller actions don’t change at all. For an example of the full Rails/Merb vertical slice, see the RESTful examples.

State rollbacks – If an action fails when firing an event, the transition is rolled back:

purchase = Purchase.find(1)
purchase.state              # => "pending"
purchase.amount = nil
purchase.valid?             # => false # validates_presence_of :amount
purchase.authorize          # => false
purchase.state              # => "pending" # Returned "authorized" pre-0.7

ActiveRecord Observers – The ActiveRecord integration kicked it up a notch (bam!) with new Observer hooks:

class PurchaseObserver < ActiveRecord::Observer
  def before_capture_from_pending_to_captured(p, transition); end
  def before_capture_from_pending(p, transition); end
  def before_capture_to_captured(p, transition); end
  def before_capture(p, transition); end
  def before_transition_state_from_pending_to_captured(p, transition); end
  def before_transition_state_to_captured(p, transition); end
  def before_transition_state_from_pending(p, transition); end
  def before_transition_state(p, transition); end
  def before_transition(p, transition); end
end

You can define the same types of hooks for after_* as well.

DataMapper Defaults – The DataMapper integration no longer uses transactions by default and properties are now auto-defined if they don’t already exist:

class Purchase
  include DataMapper::Resource
  property :id, Integer, :serial => true
  # property :state, String # Automatically done by state_machine
 
  state_machine :use_transactions => true do
    ...
  end
end

Available events / transitions – The full list of available events / transitions for a state machine can be accessed via two new helpers:

purchase = Purchase.find(1) # => #<Purchase id: 1, state: "pending">
purchase.state_events       # => [:authorize, :capture]
purchase.state_transitions  # => [#<StateMachine::Transition ...>, ...]
 
purchase.authorize
purchase.state_events       # => [:capture]
purchase.state_transitions  # => [#<StateMachine::Transition ...>]

This becomes particularly useful when presenting these in a view as a dropdown for users to select from.

Backwards Incompatible! Accessing a specific event’s transition has been renamed from next_#{event}_transition to #{event}_transition:

purchase.authorize_transition       # => #<StateMachine::Transition ...>
purchase.next_authorize_transition  # => NoMethodError: undefined method...

Event arguments – Arguments to event methods can be accessed from transition callbacks:

before_transition :on => :authorize do |purchase, transition|
  amount = transition.args.first
  ...
end

after_transition callbacksBackwards incompatible! The result of the action is no longer passed to after_transition callbacks. It can be accessed from the transition instead:

after_transition :on => :authorize do |purchase, transition|
  saved = transition.result
  ...
end

This affects all integrations as well.

Parallel events – For classes that use multiple state machines, you can fire events off in parallel on an object. This means their before/after callbacks get run at the same time and actions only get called once if they’re the same:

purchase = Purchase.find(1)
purchase.state                                    # => "pending"
purchase.shipped_at                               # => nil
purchase.fire_events(:capture, :deliver_shipment) # => true
purchase.state                                    # => "captured"
purchase.shipped_at                               # => Sun Apr 05 ...
 
# Bang method is similar to events
purchase.fire_events!(:capture) # => StateMachine::InvalidTransition: ...

And the rest of the gang…

In addition to the above major changes / additions, there were 6 new minor features and 4 bug fixes in this release.

A special Billy Mays shout-out goes to the folks who helped make this release happen, including:

  • Aaron Gibralter
  • Paweł Kondzior
  • Mikhail Shirkov

Stalk me, but not in a creepy way

As always, be sure to follow the project on GitHub! Happy coding!

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!

Viximo: We want you. We need you.

by aaron

Not happy with your current job? Stuck in a code formatting war with your coworker? Can’t seem to convince your boss that Chuck Norris was written in Ruby? We can help with that.

You take the blue pill – the story ends, you wake up at your company and believe whatever you want to believe

Welcome to Viximo. Viximo is a venture-funded startup, based in Boston, building a digital goods platform that will help creators, developers, and online communities monetize social interactions. At least, that’s what the biz folks say ;) We’re currently working on a pilot program on Facebook with the folks over at Birthday Calendar using a gifting platform that’s been under development over the past 9 months. Want to send your friend a cupcake for his birthday? A beer? Maybe a pair of handcuffs? This is where the magic happens ;)

You take the red pill – you come to Viximo and I show you how deep the rabbit-hole goes

I joined the company when we were a small group of 5. Nine months later we’re up to a whopping 15, but we’re still a small company with an amazing group of individuals who know their stuff and know how to have fun. We use Ruby on Rails. We’re built on EC2. We test using RSpec. And we occasionally play Hannah Montana over the office sound system.

Viximo wants you. Viximo needs you. So if you’re interested in meeting us in person, check out our Jobs page.

Is that pill chewable? Like Flintstones vitamins? I really like those.

If you’re not quite ready for a new job or are unsure, e-mail me anyway. If or when you’re in the Boston area, drop me a note and we’ll chat.

acts_as_what?

by aaron

This guy’s expression pretty much sums up my reaction whenever a new acts_as_* shows up in my RSS feed for AgileWebDevelopment plugins. As of today, there are 1,115 plugins listed there, 129 of which start with acts_as. That’s about 11.5% and I’m not even including ones like acts_as_hasselholf or acts_as_rickroll ;)

acts_as_chuck_norris is impossible – the only thing that can act like Chuck Norris is Chuck Norris

acts_as_* has sort of become the joke of the Rails community. Everything is acts_as whether it makes sense or not. Something simple like auto_validations becomes acts_as_auto_validated. And, in all honesty, I was sucked right into it like everyone else until I realized how ridiculous it had become. The majority of these plugins have names that are just terrible at describing what it is they do.

chuck_norris_fu consists of one move: the roundhouse kick

*_fu isn’t exactly much better. It seemed like once people started realizing that acts_as_* was becoming old, worn, and so last year, they resorted to suffixing every plugin with _fu. So instead of calling it auto_validations, it would be called validation_fu. The names changed, but the problems remained the same.

Where’s my acts_as_soviet_russia plugin?

Coming up with a name isn’t easy and isn’t always going to be witty and unique. Try to create something that’s, first and foremost, descriptive of what it is that your plugin provides… or you can just get lost in the acts_as_shuffle :-p Either way, I can at least say I warned you!

Disclaimer: I know I have a plugin that starts with acts_as… but I swear it actually makes sense in its context! :D

keep looking »