Build Status codecov Doc Status
Lightweight state machine for Active Record and Active Model.
After experimenting with state machines in a recent project, I became interested in a workflow that felt more natural for rails. In particular, I wanted to reduce architectural overlap incurred by flow control, guard, and callback workflows.
The goal of Police State is to let you easily work with state machines based on ActiveModel::Dirty, ActiveModel::Validation, and ActiveModel::Callbacks
Police State revolves around the use of TransitionValidator and two helper methods, attribute_transitioning? and attribute_transitioned?.
To get started, just include PoliceState in your model and define a set of valid transitions:
class Model < ApplicationRecord include PoliceState enum status: { queued: 0, active: 1, complete: 2, failed: 3 } validates :status, transition: { from: nil, to: :queued } validates :status, transition: { from: :queued, to: :active } validates :status, transition: { from: :active, to: :complete } validates :status, transition: { from: [:queued, :active], to: :failed } end
One aspect of Police State that will feel different than other ruby state machines is the idea that in-memory state has not fully transitioned until it is persisted to the database. This lets you operate within a traditional Active Record workflow:
model = Model.new(status: :complete) # => #<Model:0x007fa94844d088 @status=:complete> model.status_transitioning?(from: nil) # => true model.status_transitioning?(to: :complete) # => true model.valid? # => false model.errors.to_hash # => {:status=>["can't transition to complete"]} model.save # => false model.save! # => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to complete model.status = :queued # => :queued model.valid? # => true model.save # => true model.status_transitioned?(from: nil, to: :queued) # => true
Guard conditions can be introduced for a state by adding a conditional ActiveRecord validation:
validates :another_field, :presence, if: -> { queued? }
Callbacks can be attached to specific transitions by adding a condition on attribute_transitioned?. If the callback needs to occur before persistence, attribute_transitioning? can also be used.
after_commit :notify, if: -> { status_transitioned?(to: :complete) } after_commit :alert, if: -> { status_transitioned?(from: :active, to: :failed) } after_commit :log, if: -> { status_transitioned? }
Explicit event languge can be added to models by wrapping update and / or update!
def run update(status: :active) end def run! update!(status: :active) end
The bang methods defined by ActiveRecord::Enum work as well:
model.active! # => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to active
One important note about TransitionValidator is that it performs a unidirectional validation. For example, the following ensures that the active state can only be reached from the queued state:
validates :status, transition: { from: :queued, to: :active }
However, this does not prevent queued from transitioning to other states. Those states must be controlled by their own validators.
If you are using Active Model, make sure your class correctly implements ActiveModel::Dirty. For an example, check out spec/test_model.rb
Add this line to your application's Gemfile:
gem 'police_state'
And then execute:
$ bundle
Or install it yourself as:
$ gem install police_state
The gem is available as open source under the terms of the MIT License.