Skip to content
Lightweight state machine for Active Record and Active Model.
Ruby Roff
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
bin
gemfiles
lib
spec
.gitignore
.rspec
.ruby-version Upgrades .ruby-version to 2.6.4 Sep 20, 2019
.travis.yml
Gemfile
Gemfile.lock Bump rspec from 3.8.0 to 3.9.0 Oct 8, 2019
MIT-LICENSE
README.md
Rakefile Replaces rake test tasks with rspec Aug 25, 2017
codecov.yml
logo.svg Adds logo and link to readme Sep 12, 2019
police_state.gemspec Installs activerecord-nulldb-adapter 0.4.0 Oct 2, 2019

README.md

Build Status codecov Doc Status

Police State

Lightweight state machine for Active Record and Active Model.

Background

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

Usage

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

Committing a Transition

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

Guard conditions can be introduced for a state by adding a conditional ActiveRecord validation:

validates :another_field, :presence, if: -> { queued? }

Callbacks

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? }

Events

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

Validation Logic

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.

Active Model

If you are using Active Model, make sure your class correctly implements ActiveModel::Dirty. For an example, check out spec/test_model.rb

Installation

Add this line to your application's Gemfile:

gem 'police_state'

And then execute:

$ bundle

Or install it yourself as:

$ gem install police_state

License

The gem is available as open source under the terms of the MIT License.

https://purvisresearch.com

You can’t perform that action at this time.