# State machines with the Workflow gem

Recently, I’ve needed to add some state machine functionality in some of my models. So, the first thing I did was check out various ruby state machine gems on the Ruby Toolbox. I prefer Sequel over ActiveRecord, but almost all of the gems listed only provide adapters for ActiveRecord or Mongoid. The amount of effort to write a Sequel adapter is definitely something to consider for me.

state_machine is the gorilla of state machine gems. It has tons of features, adapters for ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel, but can be overkill for simple uses cases. I don’t need multiple state machines per class, event parallelization, namespacing, etc. However, having generated Graphiz visualizations can be invaluable when you have complicated state transitions. I’ve actually used state_machine before, but I do feel it’s too heavy for my needs.

AASM has been around for years and has adapters for ActiveRecord and Mongoid. It’s relatively light, but it doesn’t have a Sequel adapter and from looking at the ActiveRecord adapter, it seems like it’d be a little work to write one.

Transitions is the state machine extracted from ActiveModel. It’s super simple, but requires ActiveModel compliance. For Sequel support, there is a a ActiveModel plugin that provides this. I attempted this solution first, but ran into an after_initialize exception, which I believe is an ActiveRecord specific hook.

Workflow is a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as ‘workflow’.” The API is similar to the other 3, but the immediate difference is that you define events and transitions in the context of the state. When I used state_machine, events were defined separately from states and therefore you could reuse the same event definitions. Workflow is pretty lightweight at about 500 loc and as a bonus, also supports generating Graphviz visualizations. Workflow doesn’t have a Sequel adapter, but it looks like adding support only requires defining 2 methods.

All these gems provide, states, transitions, events, generated predicates and hooks, but I decided to go with Workflow because it was lightweight and it seemed like the easiest to write a Sequel adapter for. I also have some complicated flows, and the Graphviz diagrams can help a lot in visualizaing transitions.

To write a persistence adapter for Workflow is fairly straightforward. Override load_workflow_state and persist_workflow_state(new_value) methods in the class. I packaged it into a gem, workflow_sequel_adapter. To use it, just add this to your Gemfile:

gem 'workflow_sequel_adapter'


and then in your class, include WorkflowSequelAdapter:

class Something < Sequel::Model
include Workflow

# code here
end


## Using Workflow

So, how do you use Workflow? Well, here’s one simple use case. A User model starts in an unconfirmed state. When the user signs up, they get a confirmation email with a link to confirm their account. When they click on that link, the confirm event is triggered, and the state transitions to active. While active, the event deactivated can be triggered, which will transition the state to inactive.

The code looks similar to this:

require 'bcrpyt'
require 'secure_token'

class User < Sequel::Model
include BCrypt # for password encryption
include Workflow # for workflow block
include WorkflowSequelAdapter # for workflow sequel persistence

set_allowed_columns :email, :password # allow email and password to be set via mass assignment

workflow_column :state # use 'state' as the workflow column instead of 'workflow_state'

workflow do
state :unconfirmed do
event :confirm, transition_to: :active
end

state :active do
event :deactivate, transition_to: :inactive
end

state :inactive do
event :activate, transition_to: :active
end
end

# if you define a method with the same name as the event name, it'll be invoked after the transition
# this will clear the confirmation_token after a user is confirmed
def confirm
self.confirmation_token = nil
self.save_changes
end

# equivalent of ActiveRecord scopes for each state
subset :unconfirmed, state: 'unconfirmed'
subset :active, state: 'active'
subset :inactive, state: 'inactive'

# singleton method to authenticate based on email, password
user = self[email: email, state: 'active']
end

end

end

# generate unique confirmation token after create
def after_create
super
self.confirmation_token = generate_confirmation_token
self.save_changes
end

def validate
super
validates_unique :email, :confirmation_token
end

private
def generate_confirmation_token
record = true
while record
token = SecureToken.generate
record = self.class[confirmation_token: token]
end
token
end
end


State predicates are also generated so you can tell which events can be triggered:

user = User.create(email: 'email@example.com', 'SomePassword123')
user.can_confirm? # returns true
user.can_deactivate? # returns false


Triggering an event is just invoking the event name as a method with a bang:

user.confirm!
user.confirmed? # returns true


Well, those are the basics. The Workflow GitHub README does a pretty good job documenting how to use Workflow. I hope this short intro on how I use Workflow and my Sequel adapter helps someone.