• Stars
    star
    233
  • Rank 172,230 (Top 4 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 16 years ago
  • Updated over 15 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Ruby state machine library, like assm / acts_as_state_machine, but with a nicer, more sensible API (in my opinion).

Workflow¶ ↑

THIS COPY IS UNMAINTAINED: LINK TO ACTIVE FORK!¶ ↑

Hello! This copy of Workflow isn’t currently being maintained. The active fork is located here:

http://github.com/geekq/workflow/tree/master

Cheers!

MAILING LIST!¶ ↑

Hi! We’ve now got a mailing list to talk about Workflow, and that’s good! Come visit and post your problems or ideas or anything!!!

http://groups.google.com/group/ruby-workflow

See you there!

What is workflow?¶ ↑

Workflow is a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as ‘workflow’.

A lot of business modeling tends to involve workflow-like concepts, and the aim of this library is to make the expression of these concepts as clear as possible, using similar terminology as found in state machine theory.

So, a workflow has a state. It can only be in one state at a time. When a workflow changes state, we call that a transition. Transitions occur on an event, so events cause transitions to occur. Additionally, when an event fires, other random code can be executed, we call those actions. So any given state has a bunch of events, any event in a state causes a transition to another state and potentially causes code to be executed (an action). We can hook into states when they are entered, and exited from, and we can cause transitions to fail (guards), and we can hook in to every transition that occurs ever for whatever reason we can come up with.

Now, all that’s a mouthful, but we’ll demonstrate the API bit by bit with a real-ish world example.

Let’s say we’re modeling article submission from journalists. An article is written, then submitted. When it’s submitted, it’s awaiting review. Someone reviews the article, and then either accepts or rejects it. Explaining all that is a pain in the arse. Here is the expression of this workflow using the API:

Workflow.specify 'Article Workflow' do
  state :new do
    event :submit, :transitions_to => :awaiting_review
  end
  state :awaiting_review do
    event :review, :transitions_to => :being_reviewed
  end
  state :being_reviewed do
    event :accept, :transitions_to => :accepted
    event :reject, :transitions_to => :rejected
  end
  state :accepted
  state :rejected
end

Much better, isn’t it!

The initial state is :new – in this example that’s somewhat meaningless. (?) However, the :submit event :transitions_to => :being_reviewed. So, lets instantiate an instance of this Workflow:

workflow = Workflow.new('Article Workflow')
workflow.state # => :new

Now we can call the submit event, which transitions to the :awaiting_review state:

workflow.submit
workflow.state # => :awaiting_review

Events are actually instance methods on a workflow, and depending on the state you’re in, you’ll have a different set of events used to transition to other states.

Given this workflow is now :awaiting_approval, we have a :review event, that we call when someone begins to review the article, which puts the workflow into the :being_reviewed state.

States can also be queried via predicates for convenience like so:

workflow = Workflow.new('Article Workflow')
workflow.new?             # => true
workflow.awaiting_review? # => false
workflow.submit
workflow.new?             # => false
workflow.awaiting_review? # => true

Lets say that the business rule is that only one person can review an article at a time – having a state :being_reviewed allows for doing things like checking which articles are being reviewed, and being able to select from a pool of articles that are awaiting review, etc. (rewrite?)

Now lets say another business rule is that we need to keep track of who is currently reviewing what, how do we do this? We’ll now introduce the concept of an action by rewriting our :review event.

event :review, :transitions_to => :being_reviewed do |reviewer|
  # store the reviewer somewhere for later
end

By using Ruby blocks we’ve now introduced extra code to be fired when an event is called. The block parameters are treated as method arguments on the event, so, given we have a reference to the reviewer, the event call becomes:

# we gots a reviewer
workflow.reivew(reviewer)

OK, so how do we store the reviewer? What is the scope inside that block? Ah, we’ll get to that in a bit. An instance of a workflow isn’t as useful as a workflow bound to an instance of another class. We’ll introduce you to plain old Class integration and ActiveRecord integration later in this document.

So we’ve covered events, states, transitions and actions (as Ruby blocks). Now we’re going to go over some hooks you have access to in a workflow. These are on_exit, on_entry and on_transition.

When states transition, they are entered into, and exited out of, we can hook into this and do fancy junk.

state :being_reviewed do
  event :accept, :transitions_to => :accepted
  event :reject, :transitions_to => :rejected
  on_exit do |new_state, triggering_event, *event_args|
    # do something related to coming out of :being_reviewed
  end
end

state :accepted do
  on_entry do |prior_state, triggering_event, *event_args|
    # do something relevant to coming in to :accepted
  end
end

Now why don’t we just put this code into an action block? Well, you might not have only one event that transitions into a state, you may have multiple events that transition to a particular state, so by using the on_entry and on_exit hooks you’re guaranteeing that a certain bit of code is executed, regardless what event fires the transition.

Billy Bob the Manager comes to you and says “I need to know EVERYTHING THAT HAPPENS EVERYWHERE AT ANY TIME FOR EVERYTHING”. For whatever reasons you have to record the history of the entire workflow. That’s easy using on_transition.

on_transition do |from, to, triggering_event, *event_args|
  # record everything, or something
end

Workflow doesn’t try to tell you how to store your log messages, (but we’d suggest using a *splat and storing that somewhere, and keep your log messages flexible).

Finite state machines have the concept of a guard. The idea is that if a certain set of arbitrary conditions are not fulfilled, it will halt the transition from one state to another. We haven’t really figured out how to do this, and we don’t like the idea of going :guard => Proc.new {}, coz that’s a bit lame, so instead we have halt!

The halt! method is the implementation of the guard concept. Let’s take a look.

state :being_reviewed do
  event :accept, :transitions_to => :accepted do
    halt if true # does not transition to :accepted
  end
end

Inline with how ActiveRecord does things, halt! also can be called via halt, which makes the event return false, so you can trap it with if workflow.event instead of using a rescue block. Using halt returns false.

# using halt
workflow.state   # => :being_reviewed
workflow.accept  # => false
workflow.halted? # => true
workflow.state   # => :being_reviewed

# using halt!
workflow.state  # => :being_reviewed
begin
  workflow.accept
rescue Workflow::Halted => e
  # we gots an exception
end
workflow.halted? # => true
workflow.state   # => :being_reviewed

Furthermore, halt! and halt accept an argument, which is the message why the workflow was halted.

state :being_reviewed do
  event :accept, :transitions_to => :accepted do
    halt 'coz I said so!' if true # does not transition to :accepted
  end
end

And the API for, like, getting this message, with both halt and halt!:

# using halt
workflow.state          # => :being_reviewed
workflow.accept         # => false
workflow.halted?        # => true
workflow.halted_because # => 'coz I said so!'
workflow.state          # => :being_reviewed

# using halt!
workflow.state  # => :being_reviewed
begin
  workflow.accept
rescue Workflow::Halted => e
  e.halted_because # => 'coz I said so!'
end
workflow.halted? # => true
workflow.state   # => :being_reviewed

We can reflect off the workflow to (attempt) to automate as much as we can. There are two types of reflection in Workflow - reflection and meta-reflection. We’ll explain the former first.

workflow.states # => [:new, :awaiting_review, :being_reviewed, :accepted, :rejected]
workflow.states(:new).events # => [:submit]
workflow.states(:being_reviewed).events # => [:accept, :reject]
workflow.states(:being_reviewed).events(:accept).transitions_to # => :accepted

Meta-reflection allows you to add further information to your states, events in order to allow you to build whatever interface/controller/etc you require for your application. If reflection were Batman then meta-reflection is Robin, always there to lend a helping hand when Batman just isn’t enough.

state :new, :meta => :ui_widget => :radio_buttons do
  event :submit, :meta => :label => 'Upload...'
end

And as per the last example, getting yo meta is very similar:

workflow.states(:new).meta # => {:ui_widget => :radio_buttons}
workflow.states(:new).meta[:ui_widget] # => :radio_buttons
workflow.states(:new).meta.ui_widget # => :radio_buttons

workflow.states(:new).events(:submit).meta # => {:label => 'Upload...'}
workflow.states(:new).events(:submit).meta[:label] # => 'Upload...'
workflow.states(:new).events(:submit).meta.label # => 'Upload...'

Thankfully, meta responds to each so you can iterate over your values if you’re so inclined.

workflow.states(:new).meta.each { |key, value| puts key, value }

The order of which things are fired when an event are as follows:

* action
* on_transition (if action didn't halt)
* on_exit
* WORKFLOW STATE CHANGES, i.e. transition
* on_entry

Note that any event arguments are passed by reference, so if you modify action arguments in the action, or any of the hooks, it may affect hooked fired later.

We promised that we’d show you how to integrate workflow with your existing classes and instances, let look.

class Article  
  include Workflow
  workflow do
    state :new do
      event :submit, :transitions_to => :awaiting_review
    end
    state :awaiting_review do
      event :approve, :transitions_to => :approved
    end
    state :approved
    # ...
  end
end

article = Article.new
article.state          # => :new
article.submit         
article.state          # => :awaiting_review
article.approve
article.state          # => :approved

And as ActiveRecord is all the rage these days, all you need is a string field on the table called “workflow_state”, which is used to store the current state. Workflow handles auto-setting of a state after a find, yet it doesn’t save a record after a transition (though you could make it do this in on_transition).

class Article < ActiveRecord::Base
  include Workflow
  workflow do
    # ...
  end
end

When integrating with other classes, behind the scenes, Workflow sets up a Proxy to method missing. A probable common error would be to call an event that doesn’t exist, so we catch NoMethodError‘s and helpfully let you know what available events exist:

class Article  
  include Workflow
  workflow do
    state :new do
      event :submit, :transitions_to => :awaiting_review
    end
    state :awaiting_review do
      event :approve, :transitions_to => :approved
    end
    state :approved
    # ...
  end
end

article = Article.new
article.aaaa
NoMethodError: undefined method `aaaa' for #<Article:0xe4e8>, conversely, if you were looking to call an event for its workflow, you're in the :new state, and the available events are [:submit]

So just incase you screw something up (like I did while testing this library), it’ll give you a useful message.

You can blatter existing workflows, by simply opening them up again (similar to how Ruby works!).

Workflow.specify 'Blatter' do
  state :opened do
    event :close, :transitions_to => :closed
  end
  state :closed
end

workflow = Workflow.new('Blatter')
workflow.close
workflow.state # => :closed
workflow.open  # => raises a (nice) NoMethodError exception!

Workflow.specify 'Blatter' do
  state :closed do
    event :open, :transitions_to => :opened
  end
end

workflow.open
workflow.state # => :opened

Workflow.specify 'Blatter' do
  state :open do
    event :close, :transitions_to => :jammed # the door is now faulty :)
  end
  state :jammed
end

workflow.close
workflow.state # => :jammed

Why can we do this? Well, we needed it for our production app, so there.

And that’s about it. A update to the implementation may allow multiple workflows per instance of a class or ActiveRecord, but we haven’t figured out if that’s required or appropriate.

Ryan Allen, March 2008.

More Repositories

1

lispy

Code-as-data in Ruby, without the metaprogramming madness.
Ruby
122
star
2

w1000-super-total-mega-shark-cache-on-a-boat-on-a-plane

Fork of wp-cache plugin that uses uri based filepaths (like wp-super-cache) but with query strings and without wp-super cache's codebase and behaviour. Aim is to be the absolute fastest and simplest wp cache plugin coz each of them have their warts in their own ways.
24
star
3

sir-sync-a-lot

Baby got backups! (a fast S3 backup tool).
Ruby
22
star
4

lumberjack

Lumberjacks cut down trees. This lumberjack builds them (it's a generic DSL for constructing object trees).
Ruby
16
star
5

hack

The tiniest web-framework, ever.
Ruby
12
star
6

dns

Migrating a lot of DNS? Yeah, well this should help a bit.
Ruby
11
star
7

aspect

A kind of sort of AOP-ish library thingy that I did, everyone else is mad, I swear.
Ruby
7
star
8

modelling

Wraps some common-ish plain-ruby object modelling junk.
Ruby
6
star
9

not

Inverts booleans. I'm not sure if this was not or not done before, but it's not done now!
Ruby
6
star
10

offload

A background job library that doesn't exist (really).
5
star
11

dashboard

Set this as your homepage and put things on it that you need in front of your face often (coz you always forget).
5
star
12

superheroes

Tease apart the semantics of what things are versus what they can do.
Ruby
5
star
13

ryan-allen.github.com

5
star
14

hijack

Another AOP-like (hijacking) library, when I forgot that I had already written one (aspect), and I still swear everyone else is mad.
Ruby
3
star
15

best3

The best Ruby Amazon S3 library in existence (well, not yet).
Ruby
3
star
16

lost-socks

Lost Socks is a Ruby DSL (lol) for specifying DNS records, and verifying these records against name servers. Handy for large DNS migrations, where a record here or there are inevitably left behind, misspelled or entered incorrectly.
Ruby
3
star
17

lest

A minimal Lua testing framework, just for kicks.
Lua
2
star
18

tdsa

TDSA = Test Driven System Administration. Well, this only plays with web servers. But anyway.
Ruby
2
star
19

rock_view

A deprecated view library, ERB on steroids. We replaced it with about 10 lines of helper methods.
Ruby
2
star
20

tilde-slash

Junk that lives in my home directory, .gvimrc, .profile, .gitconfig, etc :)
Vim Script
2
star
21

jdigits

Key combinations in JavaScript because, like, it's never been done before. LOL.
JavaScript
2
star
22

hiccup

The same thing, over and over again, until it's not.
2
star
23

turns.js

Single-player option-based story engine thing.
JavaScript
2
star
24

yeahnah.org

My frig website.
2
star
25

stumpy

Automatotron.
Ruby
1
star
26

douche

WHAT IF I DIDN'T WANT TO BUY THE POTION? WHAT APOUT QUESTS?
Ruby
1
star
27

interceptor

Mad Max had one, and he was freaking awesome.
Ruby
1
star
28

comb

A simple assembler for client-side modularity in JavaScript applications.
Ruby
1
star
29

tree-face

Tree Face is the successor to Lumberjack. It's a big nasty tree with a big nasty face and it's out for revenge!
1
star
30

blaaag

My bl(o|aaa)g.
1
star
31

the-railroad

Now when people ask "What are you working on?" I can say "I've been working on the-railroad". They ask "How long have you been working on that?" and my reply "All the live long day."
1
star