Strange Symphonies Don’t worry, be happy

Related tags

7Dec/090

Ruby on Rails Finite State Machine Plugin: Workflow

I've been using the acts_as_state_machine plugin for Ruby on Rails for ages, but I thought it was time I look for alternatives, to see if there has been any development in the Finite State Machine scene. I also started getting worried that even though acts_as_state_machine still works, development on it has pretty much been discontinued. Good or bad, you can't really say.

Lucky me. I found one, and a pretty great one.

Introducing Workflow, your alternate Finite State Machine for Ruby on Rails.

acts_as_state_machine vs Workflow

At its core acts_as_state_machine and Workflow function exactly the same way. Feature wise Workflow wins out as it makes it easier to manage the callbacks. Workflow is more up to date, and I personally prefer its syntax a lot better than acts_as_state_machine.

Workflow's syntax looks a lot more like a Domain Specific Language.

Installing Workflow

Installing Workflow as a Ruby on Rails Plugin

Workflow can be installed either via Ruby Gems or a Ruby on Rails plugin.

As a Ruby on Rails plugin, go to the root folder of your Rails application and execute:

./script/plugin install \
     git://github.com/geekq/workflow.git

Note: You may need Git installed

Installing Workflow via Ruby Gems

Likewise as a Ruby Gem, Workflow can be easily installed. As your root user execute:

gem install workflow

We will need to update your config/environments.rb, and in the Rails::Initializer.run block we will need to add in config.gem 'workflow'.

Your config/environments.rb may look like so:

Rails::Initializer.run do |config|
  ...
  config.gem 'workflow'
  ..
end

Using Workflow

Lets have a look at a sample code, featuring a Person.

class Person < ActiveRecord::Base
  include Workflow
  workflow do
    state :sleeping do
      event :shower, :transitions_to => :showering
      event :work, :transitions_to => :working
    end

    state :showering do
      event :sleep, :transitions_to => :sleeping
      event :work, :transitions_to => :working
      event :date, :transitions_to => :dating do |romantic_interest|
        successful = self.flirt(romantic_interest)
        halt unless successful
      end
    end

    state :working do
      event :shower, :transitions_to => :showering
    end

    state :dating do
      event :shower, :transitions_to => :showering
      event :sleep, :transitions_to => :showering
      event :work, :transitions_to => :showering
    end
  end
end

Note: Workflow makes the assumption that the state of your model is saved in a field called workflow_state.

In line 2 we have to specifically include include Workflow into our model. From there we begin describing the workflow by opening up a workflow block. Inside we define what states the model is going to contain, and what events can be fired to transition the model from one state to another.

The initial state of a model is the first state defined, in this case it is the sleeping state. States are defined in a state block as seen on lines 4, 9, 17, and 23. We have four states here: sleeping, working, showering, dating. Possible events a state may fire are contained within the state block as an event method. These events describe which state they would transition to if they were fired.

Methods are created every time you define a state or event. The method created when you define is a state tests wether or not the state is within a particular state, ie person.sleeping?. The methods created when you define an event is the event itself, ie person.sleep!.

There is also an additional method current_state, where you can investigate the current state of the object.

Given our example model above, these methods were created for our model:

  • sleeping?
  • showering?
  • working?
  • dating?
  • sleep!
  • work!
  • shower!
  • date!
  • current_state

Events

Events help you to transition from one state to another. So suppose your person is sleeping, and you want him to shower, well we'll just call shower!.

person.current_state # "sleeping"
person.shower!
person.current_state # "showering"

Note: When firing an event, Workflow calls ActiveRecord::Base.update_attribute. This means that only your record's state will be saved in the database, any other changes to the record are not. You still have to call ActiveRecord::Base.save. It also doesn't call any validations. Validations will be handled by your guards, which will be discussed later. Calling an event from a new unsaved record, will save the record into the database.

Event Arguments

You can even have your events accept arguments, this can be see on line 12, in the :date block.

    state :showering do
      event :sleep, :transitions_to => :sleeping
      event :work, :transitions_to => :working
      event :date, :transitions_to => :dating do |romantic_interest|
        successful = self.flirt(romantic_interest)
        halt unless successful
      end
    end

Note: The scope of the block is within the instance. Treat it as any other method. self refers to the instance itself. flirt is a method defined in the instance. You would have to define the flirt method yourself.

Lets have a look at an example:

person.current_state # showering

romantic_interest = Person.new
person.date! romantic_interest
person.current_state # dating

With events being able to accept arguments, this gives us a wider range of flexibility on handling and organizing the flow of your model. But they can get more powerful with callbacks.

Callbacks

A state also includes two callbacks. One for entering the state, and one for exiting the state.

Like the event blocks, the callback blocks also accept arguments.

You can define a state's callback like so:

state :sleeping do
  event :shower, :transitions_to => :showering
  event :work, :transitions_to => :working

  on_entry do |prior_state, triggering_event, *event_args|
    # code
  end

  on_exit do |new_state, triggering_event, *event_args|
    # code
  end
end
  • The on_entry callback is called when a person enters the sleeping state.
  • The on_exit callback is called when a person exits the sleeping state.

You can also define your callbacks outside of the state block and in your model itself as methods:

state :sleeping do
  event :shower, :transitions_to => :showering
  event :work, :transitions_to => :working
end

def on_sleeping_entry(prior_state, triggering_event, *event_args)
  # code
end

def on_sleeping_exit(new_state, triggering_event, *event_args)
  # code
end

Note: Notice the naming convention of the method name? To use this method, the callback is called on_ + state name + _entry, and on_ + state name + _entry.

The arguments for the block and method are optional. Therefore for clarity, you may just enter:

state :sleeping do
  event :shower, :transitions_to => :showering
  event :work, :transitions_to => :working

  on_entry do
    # code
  end

def on_sleeping_exit(new_state, triggering_event, *event_args)
  # code
end

Global Callback

This gets tedious if you want to monitor the transitions of every state change. Don't worry, Workflow can handle that.

Workflow comes with an on_transition callback which is placed inside the workflow block:

workflow do
  on_transition do |from, to, triggering_event, *event_args|
    puts "from #{from} to #{to} via #{triggering_event}"
  end
end

Callback Order

With all these callbacks, it gets a bit confusing. Here are Workflow's callbacks in context of ActiveRecord's callbacks:

  • Workflow's in event
  • Workflow's on_transition
  • Workflow's on_exit
  • ActiveRecord::Base.before_save
  • ActiveRecord::Base.save
  • ActiveRecord::Base.after_save
  • Workflow's on_entry

Guarding States: halt!

Using Workflow your model's validations do not work when an event is fired. In fact, only the model's workflow_state is saved when an event is fired, and not any other data.

In Workflow we can setup a guard, or a halt. This will prevent the transition from ever occurring, and provide us with the safety of having our models validated.

Guards can only be used inside the event block itself. For example:

    state :showering do
      event :date, :transitions_to => :dating do |romantic_interest|
        successful = self.flirt(romantic_interest)
        halt unless successful
      end
    end

Assuming that the flirting with the romantic interest was unsuccessful, the person would still be in the the showering state. All thanks to the halt on line 15.

This would run as follows:

person.current_state # showering

romantic_interest = Person.new
person.date! romantic_interest
person.current_state # showering

Instead of just a halt, you can make it more destructive by calling halt! (with an exclamation mark) instead, which will throw the Workflow::TransitionHalted Exception instead.

Conclusion

As you can see Workflow is quite feature rich, and provides more flexibility than acts_as_state_machine. I would recommend using this over acts_as_state_machine any day. So why don't you experiment with it, and have some fun?

26Nov/090

Finder for Workflow States

Now a days for all my Finite State Machine needs on Ruby on Rails I use geekq's workflow. I find it much better to use than the previous popular Finite State Machine plugin on Rails, acts_as_state_machine.

named_scope

I need to locate all the models that are of a certain state, and thanks to named_scope. This can be done quite easily. Except when we have a lot of states I'd rather not write a named_scope for each individual state. For example:

class Order < ActiveRecord::Base
  named_scope :completed, :conditions => { :workflow_state => 'completed' }
end

It would be encumbersome to write this multiple times. I'd rather have a named_scope created for each state. This is my quick hack.

class Order < ActiveRecord::Base
  include Workflow
  workflow do
    state :new do
      ...
    end

    ...
  end

  (self.workflow_spec.states.keys - [:new]).each do |state|
    named_scope state, :conditions => { :workflow_state => state.to_s }
  end
end

After the workflow code you place you write:

  (self.workflow_spec.states.keys - [:new]).each do |state|
    named_scope state, :conditions => { :workflow_state => state.to_s }
  end

This iterates through each of our workflow states, and creates a named_scope for each one based on the name of the state. So you can easily do Order.completed and find all completed Orders.

Note: I had to remove :new (on line 12) from the array of keys as it would cause problems with my code. This is caused from overwriting the new method. You can solve this by probably putting a prefix for the named_scope name.

I know it can be extracted to use alias_method_chain and modify the Workflow sourcecode, but I enjoy my hack.

4Jun/070

Fixtures Without Rails

So continuing my series of ActiveRecord Without Rails, I thought I would inspect another popular feature of ActiveRecord, Fixtures.

Fixtures are a great way to populate your database with an expected result set. It makes it easy to prepare your database for testing and presentation to your client.

Why would you do this? Well you've already got ActiveRecord without Rails working, and probably are using the Migrations without Rails. So why not continue with fixtures?

Note:

  • I won't go into getting ActiveRecord working without Rails, please refer to that post. It's dead simple.
  • All these commands are executed from the root directory of your non-rails application.

Creating the Fixtures Directory

The fixtures will need to be stored somewhere, so we'll just create the necessary directory for them to enjoy a rather peaceful life.

mkdir -p test/fixtures

Setting up the Rakefile

Now we'll need a Rakefile. If your not familiar with Rakefiles just create a file called: Rakefile File

Note:

  • If you were following my ActiveRecord Migrations Without Rails, this is the same Rakefile. I've changed the default task to execute to load the fixtures instead. You can opt to continue using the migrate task if you want. Just remember to call either rake migrate or rake fixtures, depending on which task you would like call, that isn't your default task.
  • If you were not following my ActiveRecord Migrations Without Rails, you can ignore the migrate task.

require 'active_record'
require 'active_record/fixtures'
require 'yaml'
require 'erb'

task :default => :fixtures # Your choice

desc "Load fixtures into the current database.  Load specific fixtures using FIXTURES=x,y"
task :fixtures => :environment do
  fixtures = ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(File.dirname(__FILE__), 'test', 'fixtures', '*.{yml,csv}'))
  fixtures.each do |fixture_file|
    Fixtures.create_fixtures('test/fixtures', File.basename(fixture_file, '.*'))
  end
end

desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"
task :migrate => :environment do
  ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
end

task :environment do
  ActiveRecord::Base.establish_connection(YAML::load(File.open('database.yml')))
  ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'a'))
end

Your First Fixture

Now we'll create our first fixture, and save it into test/fixtures/users.yml File. The filename corresponds to the table for where the fixtures will be loaded into.

aizatto:
  name: Ezwan Aizat Bin Abdullah Faiz

Warning:

  • Loading fixtures is destructive. It will destroy everything in the table.
  • This task loads all fixtures into the database. We'll cover specifying individual fixtures later.

Now the fixture is ready for loading and we execute:

rake

Loading Specific Fixtures

By default all fixtures are loaded into the database. Rather than loading all the fixtures, you can also specify which fixtures you want to load by appending them to a FIXTURES argument.

rake FIXTURES=users,roles

Embedded Ruby in Fixtures

In the same way like Rails, you can also continue to embed ruby inside your fixtures, giving you more flexibility to your hour models will be loaded.

aizatto:
  name: Ezwan Aizat Bin Abdullah Faiz
  created_at: <%= 1.day.ago %>

Conclusion

So by now you've probably accumulated quite a bit of knowledge in using ActiveRecord and its features outside of Rails.

So lets hear it, what else would you like to see?