What is workflow?
This Gem is a fork of Vladimir Dobriakov's Workflow Gem. Credit goes to him for the core code. Please read the original README for a full introduction, as this README skims through much of that content and focuses on new / changed features.
What's different in rails-workflow
The primary difference here is the use of ActiveSupport::Callbacks
to enable a more flexible application of callbacks.
You now have access to the same DSL you're used to from ActionController Callbacks,
including the ability to wrap state transitions in an around_transition, to place
conditional logic on application of callbacks, or to have callbacks run for only
a set of state-change events.
I've made ActiveRecord and ActiveSupport into runtime dependencies.
You can also take advantage of ActiveRecord's conditional validation syntax, to apply validations only to specific state transitions.
Installation
gem install rails-workflow
Ruby Version
I've only tested with Ruby 2.3. ;) Time to upgrade.
Basic workflow definition:
class Article
include Workflow
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
end
Access an object representing the current state of the entity, including available events and transitions:
article.current_state
=> #<Workflow::State:0x7f1e3d6731f0 @events={
:submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil,
@transitions_to=:awaiting_review, @name=:submit, @meta={}>},
name:new, meta{}
On Ruby 1.9 and above, you can check whether a state comes before or after another state (by the order they were defined):
article.current_state
=> being_reviewed
article.current_state < :accepted
=> true
article.current_state >= :accepted
=> false
article.current_state.between? :awaiting_review, :rejected
=> true
Now we can call the submit event, which transitions to the :awaiting_review state:
article.submit!
article.awaiting_review? # => true
Callbacks
The DSL syntax here is very much similar to ActionController or ActiveRecord callbacks.
Callbacks with this strategy used the same as ActionController Callbacks.
You can configure any number of before, around, or after transition callbacks.
before_transition and around_transition are called in the order they are set,
and after_transition callbacks are called in reverse order.
Around Transition
Allows you to run code surrounding the state transition.
around_transition :wrap_in_transaction
def wrap_in_transaction(&block)
Article.transaction(&block)
end
You can also define the callback using a block:
around_transition do |object, transition|
object.with_lock do
transition.call
end
end
Replacement for workflow's on_error proc:
around_transition :catch_errors
def catch_errors begin yield rescue SomeApplicationError => ex logger.error 'Oh noes!' end end
before_transition
Allows you to run code prior to the state transition.
If you halt or throw :abort within a before_transition, the callback chain
will be halted, the transition will be canceled and the event action
will return false.
before_transition :check_title
def check_title
halt('Title was bad.') unless title == "Good Title"
end
Or again, in block expression:
before_transition do |article|
throw :abort unless article.title == "Good Title"
end
After Transition
Runs code after the transition.
after_transition :check_title
Prepend Transitions
To add a callback to the beginning of the sequence:
prepend_before_transition :some_before_transition
prepend_around_transition :some_around_transition
prepend_after_transition :some_after_transition
Skip Transitions
skip_before_transition :some_before_transition
Conditions
if/unless
The callback will run if or unless the named method returns a truthy value.
before_transition :do_something, if: :valid?
only/except
The callback will run if or unless the event being processed is in the list given
# Run this callback only on the `accept` and `publish` events.
before_transition :do_something, only: [:accept, :publish]
# Run this callback on events other than the `accept` and `publish` events.
before_transition :do_something_else, except: [:accept, :publish]
Conditional Validations
If you are using ActiveRecord, you'll have access to a set of methods which
describe the current transition underway.
Inside the same Article class which was begun above, the following three
validations would all run when the submit event is used to transition
from new to awaiting_review.
validates :title, presence: true, if: :transitioning_to_awaiting_review?
validates :body, presence: true, if: :transitioning_from_new?
validates :author, presence: true, if: :transitioning_via_event_submit?
Halting if validations fail
# This will create a transition callback which will stop the event
# and return false if validations fail.
halt_transition_unless_valid!
# This is the same as
Checking A Transition
Call can_transition? to determine whether the validations would pass if a
given event was called:
if article.can_transition?(:submit)
# Do something interesting
end
Transition Context
During transition you can refer to the transition_context object on your model,
for information about the current transition. See [Workflow::TransitionContext].
Naming Event Arguments
If you will normally call each of your events with the same arguments, the following will help:
class Article < ApplicationRecord
include Workflow
before_transition :check_reviewer
def check_reviewer
# Ability is a class from the cancan gem: https://github.com/CanCanCommunity/cancancan
halt('Access denied') unless Ability.new(transition_context.reviewer).can?(:review, self)
end
workflow do
event_args :reviewer, :reviewed_at
state :new do
event :review, transitions_to: :reviewed
end
state :reviewed
end
end
Transition event handler
The best way is to use convention over configuration and to define a method with the same name as the event. Then it is automatically invoked when event is raised. For the Article workflow defined earlier it would be:
class Article
def reject
puts 'sending email to the author explaining the reason...'
end
end
article.review!; article.reject! will cause state transition to
being_reviewed state, persist the new state (if integrated with
ActiveRecord), invoke this user defined reject method and finally
persist the rejected state.
Note: on successful transition from one state to another the workflow
gem immediately persists the new workflow state with update_column(),
bypassing any ActiveRecord callbacks including updated_at update.
This way it is possible to deal with the validation and to save the
pending changes to a record at some later point instead of the moment
when transition occurs.
You can also define event handler accepting/requiring additional arguments:
class Article
def review(reviewer = '')
puts "[#{reviewer}] is now reviewing the article"
end
end
article2 = Article.new
article2.submit!
article2.review!('Homer Simpson') # => [Homer Simpson] is now reviewing the article
Integration with ActiveRecord
Workflow library can handle the state persistence fully automatically. You
only need to define a string field on the table called workflow_state
and include the workflow mixin in your model class as usual:
class Order < ActiveRecord::Base
include Workflow
workflow do
# list states and transitions here
end
end
On a database record loading all the state check methods e.g.
article.state, article.awaiting_review? are immediately available.
For new records or if the workflow_state field is not set the state
defaults to the first state declared in the workflow specification. In
our example it is :new, so Article.new.new? returns true and
Article.new.approved? returns false.
At the end of a successful state transition like article.approve! the
new state is immediately saved in the database.
You can change this behaviour by overriding persist_workflow_state
method.
Scopes
Workflow library also adds automatically generated scopes with names based on states names:
class Order < ActiveRecord::Base
include Workflow
workflow do
state :approved
state :pending
end
end
# returns all orders with `approved` state
Order.with_approved_state
# returns all orders with `pending` state
Order.with_pending_state
Wrap State Transition in a locking transaction
Wrap your transition in a locking transaction to ensure that any exceptions raised later in the transition sequence will roll back earlier changes made to the record:
class Order < ActiveRecord::Base
include Workflow
workflow transactional: true do
state :approved
state :pending
end
end
Conditional event transitions
Conditions can be a "method name symbol" with a corresponding instance method, a proc or lambda which are added to events, like so:
state :off
event :turn_on, :transition_to => :on,
:if => :sufficient_battery_level?
event :turn_on, :transition_to => :low_battery,
:if => proc { |device| device.battery_level > 0 }
end
# corresponding instance method
def sufficient_battery_level?
battery_level > 10
end
When calling a device.can_<fire_event>? check, or attempting a device.<event>!, each event is checked in turn:
- With no
:ifcheck, proceed as usual. - If an
:ifcheck is present, proceed if it evaluates to true, or drop to the next event. - If you've run out of events to check (eg.
battery_level == 0), then the transition isn't possible.
Accessing your workflow specification
You can easily reflect on workflow specification programmatically - for the whole class or for the current object. Examples:
article2.current_state.events # lists possible events from here
article2.current_state.events[:reject].transitions_to # => :rejected
Article.workflow_spec.states.keys
#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
Article.workflow_spec.state_names
#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]
# list all events for all states
Article.workflow_spec.states.values.collect &:events
You can also store and later retrieve additional meta data for every state and every event:
class MyProcess
include Workflow
workflow do
state :main, :meta => {:importance => 8}
state :supplemental, :meta => {:importance => 1}
end
end
puts MyProcess.workflow_spec.states[:supplemental].[:importance] # => 1
The workflow library itself uses this feature to tweak the graphical representation of the workflow. See below.
Earlier versions
The workflow gem is the work of Vladimir Dobriakov, http://www.mobile-web-consulting.de, http://blog.geekq.net/.
This project is a fork of his work, and the bulk of the workflow specification code and DSL are virtually unchanged.
About
Author: Tyler Gannon [https://github.com/tylergannon]
Original Author: Vladimir Dobriakov, http://www.mobile-web-consulting.de, http://blog.geekq.net/
Copyright (c) 2010-2014 Vladimir Dobriakov, www.mobile-web-consulting.de
Copyright (c) 2008-2009 Vodafone
Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
Based on the work of Ryan Allen and Scott Barron
Licensed under MIT license, see the MIT-LICENSE file.