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
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
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
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
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:
and then in your class,
class Something < Sequel::Model include Workflow include WorkflowSequelAdapter # code here end
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, the event
deactivated can be triggered, which will transition the state to
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 def self.authenticate(email, unencrypted_password) user = self[email: email, state: 'active'] user if user && user.password == unencrypted_password end def password @password ||= Password.new(encrypted_password) end def password=(new_password) @password = Password.create(new_password) self.encrypted_password = @password 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_presence [:email, :encrypted_password] 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: 'firstname.lastname@example.org', '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.