police_state 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +147 -0
- data/Rakefile +26 -0
- data/lib/police_state.rb +38 -0
- data/lib/police_state/transition_helpers.rb +71 -0
- data/lib/police_state/transition_validator.rb +32 -0
- data/lib/police_state/validation_helpers.rb +34 -0
- data/lib/police_state/version.rb +3 -0
- data/lib/tasks/police_state_tasks.rake +4 -0
- metadata +95 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2cb2c1110ee730d95c0736792fe123449bb87b02
|
4
|
+
data.tar.gz: 7a757181a7fb2ebf15f4d98930c56c6249f3a238
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2a14b69fa95a25bb675c66d4bb2b9fc1a0ad6caa310343eefc1d04a3fac46df5229894a88721378d9e4dcc218d916ab7d676ac21591613b92e5179c121708ddb
|
7
|
+
data.tar.gz: fa2f01744990415a13690664aef9d4cd652b5c03c94291a94fab1da7af7e560f284b111f045e36c7a5f7784320486dfa43c1ccb31008853568e7b12a1ed12444
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2017 Ian Purvis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/ianpurvis/police_state.svg?branch=master)](https://travis-ci.org/ianpurvis/police_state)
|
2
|
+
[![Doc Status](http://inch-ci.org/github/ianpurvis/police_state.svg?branch=master)](http://inch-ci.org/github/ianpurvis/police_state)
|
3
|
+
|
4
|
+
# Police State
|
5
|
+
Lightweight state machine for Active Record and Active Model.
|
6
|
+
|
7
|
+
|
8
|
+
## Background
|
9
|
+
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.
|
10
|
+
|
11
|
+
The goal of Police State is to let you easily work with state machines based on `ActiveModel::Dirty`, `ActiveModel::Validation`, and `ActiveModel::Callbacks`
|
12
|
+
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
Police State revolves around the use of `TransitionValidator` and two helper methods, `attribute_transitioning?` and `attribute_transitioned?`.
|
16
|
+
|
17
|
+
To get started, just include `PoliceState` in your model and define a set of valid transitions:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class Model < ApplicationRecord
|
21
|
+
include PoliceState
|
22
|
+
|
23
|
+
enum status: {
|
24
|
+
queued: 0,
|
25
|
+
active: 1,
|
26
|
+
complete: 2,
|
27
|
+
failed: 3
|
28
|
+
}
|
29
|
+
|
30
|
+
validates :status, transition: { from: nil, to: :queued }
|
31
|
+
validates :status, transition: { from: :queued, to: :active }
|
32
|
+
validates :status, transition: { from: :active, to: :complete }
|
33
|
+
validates :status, transition: { from: [:queued, :active], to: :failed }
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
### Committing a Transition
|
38
|
+
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:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
model = Model.new(status: :complete)
|
42
|
+
# => #<Model:0x007fa94844d088 @status=:complete>
|
43
|
+
|
44
|
+
model.status_transitioning?(from: nil)
|
45
|
+
# => true
|
46
|
+
|
47
|
+
model.status_transitioning?(to: :complete)
|
48
|
+
# => true
|
49
|
+
|
50
|
+
model.valid?
|
51
|
+
# => false
|
52
|
+
|
53
|
+
model.errors.to_hash
|
54
|
+
# => {:status=>["can't transition to complete"]}
|
55
|
+
|
56
|
+
model.save
|
57
|
+
# => false
|
58
|
+
|
59
|
+
model.save!
|
60
|
+
# => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to complete
|
61
|
+
|
62
|
+
model.status = :queued
|
63
|
+
# => :queued
|
64
|
+
|
65
|
+
model.valid?
|
66
|
+
# => true
|
67
|
+
|
68
|
+
model.save
|
69
|
+
# => true
|
70
|
+
|
71
|
+
model.status_transitioned?(from: nil, to: :queued)
|
72
|
+
# => true
|
73
|
+
|
74
|
+
```
|
75
|
+
|
76
|
+
|
77
|
+
### Guard Conditions
|
78
|
+
Guard conditions can be introduced for a state by adding a conditional ActiveRecord validation:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
validates :another_field, :presence, if: -> { queued? }
|
82
|
+
```
|
83
|
+
|
84
|
+
### Callbacks
|
85
|
+
|
86
|
+
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.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
after_commit :notify, if: -> { status_transitioned?(to: :complete) }
|
90
|
+
after_commit :alert, if: -> { status_transitioned?(from: :active, to: :failed) }
|
91
|
+
after_commit :log, if: -> { status_transitioned? }
|
92
|
+
```
|
93
|
+
|
94
|
+
### Events
|
95
|
+
Explicit event languge can be added to models by wrapping `update` and / or `update!`
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
def run
|
99
|
+
update(status: :active)
|
100
|
+
end
|
101
|
+
|
102
|
+
def run!
|
103
|
+
update!(status: :active)
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
The bang methods defined by `ActiveRecord::Enum` work as well:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
model.active!
|
111
|
+
# => ActiveRecord::RecordInvalid: Validation failed: Status can't transition to active
|
112
|
+
```
|
113
|
+
|
114
|
+
### Validation Logic
|
115
|
+
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:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
validates :status, transition: { from: :queued, to: :active }
|
119
|
+
```
|
120
|
+
|
121
|
+
However, this does not prevent `queued` from transitioning to other states. Those states must be controlled by their own validators.
|
122
|
+
|
123
|
+
|
124
|
+
### Active Model
|
125
|
+
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)
|
126
|
+
|
127
|
+
|
128
|
+
## Installation
|
129
|
+
Add this line to your application's Gemfile:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
gem 'police_state'
|
133
|
+
```
|
134
|
+
|
135
|
+
And then execute:
|
136
|
+
```bash
|
137
|
+
$ bundle
|
138
|
+
```
|
139
|
+
|
140
|
+
Or install it yourself as:
|
141
|
+
```bash
|
142
|
+
$ gem install police_state
|
143
|
+
```
|
144
|
+
|
145
|
+
|
146
|
+
## License
|
147
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'PoliceState'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'bundler/gem_tasks'
|
18
|
+
|
19
|
+
begin
|
20
|
+
require 'rspec/core/rake_task'
|
21
|
+
RSpec::Core::RakeTask.new(:spec)
|
22
|
+
task default: :spec
|
23
|
+
rescue LoadError
|
24
|
+
puts 'Could not load RSpec'
|
25
|
+
end
|
26
|
+
|
data/lib/police_state.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2017 Ian Purvis
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require "active_model"
|
25
|
+
require "active_support/core_ext/array/wrap.rb"
|
26
|
+
require "active_support/core_ext/hash/slice.rb"
|
27
|
+
require "police_state/transition_helpers"
|
28
|
+
require "police_state/transition_validator"
|
29
|
+
require "police_state/validation_helpers"
|
30
|
+
|
31
|
+
module PoliceState
|
32
|
+
extend ActiveSupport::Concern
|
33
|
+
|
34
|
+
included do
|
35
|
+
include PoliceState::TransitionHelpers
|
36
|
+
extend PoliceState::ValidationHelpers
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module PoliceState
|
2
|
+
|
3
|
+
# == Police State Transition Helpers
|
4
|
+
#
|
5
|
+
# Provides a way to monitor attribute state transitions for Active Record and Active Model objects.
|
6
|
+
#
|
7
|
+
# Note: If using with Active Model, make sure that your class implements +ActiveModel::Dirty+.
|
8
|
+
module TransitionHelpers
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
include ActiveModel::AttributeMethods
|
11
|
+
|
12
|
+
# :stopdoc:
|
13
|
+
included do
|
14
|
+
raise ArgumentError, "Including class must implement ActiveModel::Dirty" unless include?(ActiveModel::Dirty)
|
15
|
+
attribute_method_suffix "_transitioned?", "_transitioning?"
|
16
|
+
end
|
17
|
+
# :startdoc:
|
18
|
+
|
19
|
+
# Returns +true+ if +attribute+ transitioned at the last save event, otherwise +false+.
|
20
|
+
#
|
21
|
+
# You can specify origin and destination states using the +:from+ and +:to+
|
22
|
+
# options. If +attribute+ is an +ActiveRecord::Enum+, these may be
|
23
|
+
# specified as either symbol or their native enum value.
|
24
|
+
#
|
25
|
+
# model = Model.create!(status: :complete)
|
26
|
+
# # => #<Model:0x007fa94844d088 @status=:complete>
|
27
|
+
#
|
28
|
+
# model.status_transitioned? # => true
|
29
|
+
# model.status_transitioned?(from: nil) # => true
|
30
|
+
# model.status_transitioned?(to: :complete) # => true
|
31
|
+
# model.status_transitioned?(to: "complete") # => true
|
32
|
+
# model.status_transitioned?(from: nil, to: :complete) # => true
|
33
|
+
def attribute_transitioned?(attr, options={})
|
34
|
+
options = _transform_options_for_attribute(attr, options)
|
35
|
+
!!previous_changes_include?(attr) &&
|
36
|
+
(!options.include?(:to) || options[:to] == previous_changes[attr].last) &&
|
37
|
+
(!options.include?(:from) || options[:from] == previous_changes[attr].first)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns +true+ if +attribute+ is currently transitioning but not saved, otherwise +false+.
|
41
|
+
#
|
42
|
+
# You can specify origin and destination states using the +:from+ and +:to+
|
43
|
+
# options. If +attribute+ is an +ActiveRecord::Enum+, these may be
|
44
|
+
# specified as either symbol or their native enum value.
|
45
|
+
#
|
46
|
+
# model = Model.new(status: :complete)
|
47
|
+
# # => #<Model:0x007fa94844d088 @status=:complete>
|
48
|
+
#
|
49
|
+
# model.status_transitioning? # => true
|
50
|
+
# model.status_transitioning?(from: nil) # => true
|
51
|
+
# model.status_transitioning?(to: :complete) # => true
|
52
|
+
# model.status_transitioning?(to: "complete") # => true
|
53
|
+
# model.status_transitioning?(from: nil, to: :complete) # => true
|
54
|
+
def attribute_transitioning?(attr, options={})
|
55
|
+
options = _transform_options_for_attribute(attr, options)
|
56
|
+
attribute_changed?(attr, options)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Facilitates easier change checking for ActiveRecord::Enum attributes
|
63
|
+
# by casting any symbolized :to and :from values into their native strings.
|
64
|
+
def _transform_options_for_attribute(attr, options={})
|
65
|
+
return options unless self.class.respond_to?(:attribute_types)
|
66
|
+
options.transform_values {|value|
|
67
|
+
self.class.attribute_types.with_indifferent_access[attr].cast(value)
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module PoliceState
|
2
|
+
class TransitionValidator < ActiveModel::EachValidator # :nodoc:
|
3
|
+
|
4
|
+
def check_validity!
|
5
|
+
raise ArgumentError, "Options must include :from" unless options.include?(:from)
|
6
|
+
raise ArgumentError, "Options must include :to" unless options.include?(:to)
|
7
|
+
raise ArgumentError, "Options cannot specify array for :to" if options[:to].is_a?(Array)
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate_each(record, attr_name, value)
|
11
|
+
unless transition_allowed?(record, attr_name)
|
12
|
+
record.errors.add(attr_name, "can't transition to #{value}", options)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def destination
|
20
|
+
options[:to]
|
21
|
+
end
|
22
|
+
|
23
|
+
def origins
|
24
|
+
options[:from].instance_eval {nil? ? [nil] : Array.wrap(self)}
|
25
|
+
end
|
26
|
+
|
27
|
+
def transition_allowed?(record, attr_name)
|
28
|
+
!record.attribute_transitioning?(attr_name, to: destination) ||
|
29
|
+
origins.any? {|origin| record.attribute_transitioning?(attr_name, from: origin)}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PoliceState
|
2
|
+
|
3
|
+
# == Police State Validation Helpers
|
4
|
+
module ValidationHelpers
|
5
|
+
|
6
|
+
# Validates that the specified attribute is transitioning to a
|
7
|
+
# destination state from one or more origin states.
|
8
|
+
#
|
9
|
+
# class Model < ActiveRecord::Base
|
10
|
+
# validates_transition_of :status, from: nil, to: :queued
|
11
|
+
# validates_transition_of :status, from: :queued, to: :active
|
12
|
+
# validates_transition_of :status, from: :active, to: :complete
|
13
|
+
# validates_transition_of :status, from: [:queued, :active], to: :failed
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# Note: This check is only performed if the attribute is transitioning to the
|
17
|
+
# destination state.
|
18
|
+
#
|
19
|
+
# You can specify +nil+ states for nullable attributes.
|
20
|
+
#
|
21
|
+
# Configuration options:
|
22
|
+
#
|
23
|
+
# * <tt>:from</tt> - The origin state(s) of the attribute. This can be supplied as a
|
24
|
+
# single value or an array.
|
25
|
+
# * <tt>:to</tt> - The destination state of the attribute.
|
26
|
+
#
|
27
|
+
# There is also a list of default options supported by every validator:
|
28
|
+
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
|
29
|
+
# See <tt>ActiveModel::Validation#validates</tt> for more information
|
30
|
+
def validates_transition_of(*attr_names)
|
31
|
+
validates_with PoliceState::TransitionValidator, _merge_attributes(attr_names)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: police_state
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ian Purvis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.0.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.0.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec-rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sqlite3
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- ian@purvisresearch.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/police_state.rb
|
66
|
+
- lib/police_state/transition_helpers.rb
|
67
|
+
- lib/police_state/transition_validator.rb
|
68
|
+
- lib/police_state/validation_helpers.rb
|
69
|
+
- lib/police_state/version.rb
|
70
|
+
- lib/tasks/police_state_tasks.rake
|
71
|
+
homepage:
|
72
|
+
licenses:
|
73
|
+
- MIT
|
74
|
+
metadata: {}
|
75
|
+
post_install_message:
|
76
|
+
rdoc_options: []
|
77
|
+
require_paths:
|
78
|
+
- lib
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
requirements: []
|
90
|
+
rubyforge_project:
|
91
|
+
rubygems_version: 2.6.13
|
92
|
+
signing_key:
|
93
|
+
specification_version: 4
|
94
|
+
summary: Lightweight state machine for Active Record and Active Model.
|
95
|
+
test_files: []
|