https://github.com/ianpurvis/police_state
Lightweight state machine for Active Record and Active Model.
https://github.com/ianpurvis/police_state
activerecord rails ruby state-machine
Last synced: 6 months ago
JSON representation
Lightweight state machine for Active Record and Active Model.
- Host: GitHub
- URL: https://github.com/ianpurvis/police_state
- Owner: ianpurvis
- License: mit
- Created: 2017-06-27T17:25:18.000Z (about 9 years ago)
- Default Branch: trunk
- Last Pushed: 2024-08-02T00:18:33.000Z (almost 2 years ago)
- Last Synced: 2025-03-23T16:15:31.046Z (over 1 year ago)
- Topics: activerecord, rails, ruby, state-machine
- Language: Ruby
- Homepage:
- Size: 168 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: MIT-LICENSE
Awesome Lists containing this project
README
[](https://github.com/ianpurvis/police_state/actions/workflows/continuous.yml)
[](https://codecov.io/gh/ianpurvis/police_state)
[](http://inch-ci.org/github/ianpurvis/police_state)
# 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:
```ruby
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:
```ruby
model = Model.new(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:
```ruby
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.
```ruby
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!`
```ruby
def run
update(status: :active)
end
def run!
update!(status: :active)
end
```
The bang methods defined by `ActiveRecord::Enum` work as well:
```ruby
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:
```ruby
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](spec/test_model.rb)
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'police_state'
```
And then execute:
```bash
$ bundle
```
Or install it yourself as:
```bash
$ gem install police_state
```
## License
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
[](https://purvisresearch.com)