has_state_machine 0.6.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +123 -7
- data/lib/has_state_machine/state.rb +66 -37
- data/lib/has_state_machine/state_helpers.rb +3 -3
- data/lib/has_state_machine/version.rb +1 -1
- metadata +21 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 902498121bd57b97de373beaa68557c3591de8a221d524af5b9033be1cca8a73
|
|
4
|
+
data.tar.gz: fb4806d204772d488c4fb2bc69360cd989edf4c8a5312508b37868d961adf059
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1baeee9d74f64325427b52e2e867cab05db7848f384316b8175cbb5d49a2c4030dcf905020ac8091c495f6cfa20f8dcae657bcffd78b86987e9c867665f5cf42
|
|
7
|
+
data.tar.gz: 7e998865b342452f802266b3408bd673b26fb622672ed48ff18cb384259c34a7ec1f591a9ed99efb12f33364e5a1c9bff33d9b95bd4fbac3794ff9d3f2ccdd25
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HasStateMachine
|
|
2
2
|
|
|
3
|
-
[](https://github.com/beehiiv/has_state_machine/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/testdouble/standard)
|
|
5
5
|
|
|
6
6
|
HasStateMachine uses ruby classes to make creating a finite state machine for your ActiveRecord models a breeze.
|
|
@@ -10,7 +10,9 @@ HasStateMachine uses ruby classes to make creating a finite state machine for yo
|
|
|
10
10
|
- [HasStateMachine](#hasstatemachine)
|
|
11
11
|
- [Contents](#contents)
|
|
12
12
|
- [Installation](#installation)
|
|
13
|
-
- [Usage](#usage)
|
|
13
|
+
- [Usage](#basic-usage)
|
|
14
|
+
- [Basic Usage](#basic-usage)
|
|
15
|
+
- [Validations & Error Handling](#validations-and-error-handling)
|
|
14
16
|
- [Advanced Usage](#advanced-usage)
|
|
15
17
|
- [Contributing](#contributing)
|
|
16
18
|
- [License](#license)
|
|
@@ -35,12 +37,12 @@ Or install it yourself as:
|
|
|
35
37
|
$ gem install has_state_machine
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
## Usage
|
|
40
|
+
## Basic Usage
|
|
39
41
|
|
|
40
42
|
You must first use the `has_state_machine` macro to define your state machine at
|
|
41
43
|
a high level. This includes defining the possible states for your object as well
|
|
42
44
|
as some optional configuration should you want to change the default behavior of
|
|
43
|
-
the state machine.
|
|
45
|
+
the state machine (more on this later).
|
|
44
46
|
|
|
45
47
|
```ruby
|
|
46
48
|
# By default, it is assumed that the "state" of the object is
|
|
@@ -52,7 +54,7 @@ end
|
|
|
52
54
|
|
|
53
55
|
Now you must define the classes for the states in your state machine. By default,
|
|
54
56
|
`HasStateMachine` assumes that these will be under the `Workflow` namespace following
|
|
55
|
-
the pattern of `Workflow::#{ObjectClass}::#{State}`. The state
|
|
57
|
+
the pattern of `Workflow::#{ObjectClass}::#{State}`. The state classes must inherit
|
|
56
58
|
from `HasStateMachine::State`.
|
|
57
59
|
|
|
58
60
|
```ruby
|
|
@@ -94,6 +96,13 @@ module Workflow
|
|
|
94
96
|
# after_transition callbacks as well.
|
|
95
97
|
Rails.logger.info "== Transitioned from #{previous_state} ==\n"
|
|
96
98
|
end
|
|
99
|
+
|
|
100
|
+
# after_transition_commit runs only once the transition has been
|
|
101
|
+
# committed: after the record is saved for normal transitions, and
|
|
102
|
+
# outside the transaction for transactional transitions (see below).
|
|
103
|
+
after_transition_commit do
|
|
104
|
+
MyJob.perform_later(object)
|
|
105
|
+
end
|
|
97
106
|
end
|
|
98
107
|
end
|
|
99
108
|
```
|
|
@@ -117,9 +126,83 @@ post.status.transition_to(:archived)
|
|
|
117
126
|
# => true
|
|
118
127
|
```
|
|
119
128
|
|
|
129
|
+
If you'd like to check that an object can be transitioned into a new state, use the `can_transition?` method. This checks to see if the provided argument is in the `transitions_to` array defined on the object's current state. (This does not run any validations that may be defined on the new state)
|
|
130
|
+
|
|
131
|
+
Example:
|
|
132
|
+
```ruby
|
|
133
|
+
post = Post.create(status: "draft")
|
|
134
|
+
|
|
135
|
+
post.status.can_transition?(:published) # => true
|
|
136
|
+
post.status.can_transition?(:other_state) # => false
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Validations and Error Handling
|
|
140
|
+
|
|
141
|
+
You can define custom validations on a given state to determine whether an object in that state or a transition to that state is valid.
|
|
142
|
+
|
|
143
|
+
By default, validations defined on the state will be run as part of the object validations if the object is in that state.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
post = Post.create(status: "published", title: "Title")
|
|
147
|
+
|
|
148
|
+
post.valid?
|
|
149
|
+
# => true
|
|
150
|
+
|
|
151
|
+
post.title = nil
|
|
152
|
+
post.valid?
|
|
153
|
+
# => false
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
If you wish to change this behavior and not have the state validations run on the object, you can specify that with the `state_validations_on_object` option when defining your state machine.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class Post < ApplicationRecord
|
|
160
|
+
has_state_machine states: %i[draft published, archived], state_validations_on_object: false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
post = Post.create(status: "published", title: "Title")
|
|
164
|
+
|
|
165
|
+
post.valid?
|
|
166
|
+
# => true
|
|
167
|
+
|
|
168
|
+
post.title = nil
|
|
169
|
+
post.valid?
|
|
170
|
+
# => true
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
By default, when attempting to transition an object to another state, it checks:
|
|
174
|
+
* Validations defined on the object
|
|
175
|
+
* That the new state is one of the allowed transitions from the current state
|
|
176
|
+
* Any validations defined on the new state
|
|
177
|
+
|
|
178
|
+
If any are found to be invalid, the transition will fail. Any errors from validations on the new state will be added to the object.
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
post = Post.create(status: "draft")
|
|
182
|
+
|
|
183
|
+
post.title = nil
|
|
184
|
+
post.status.transition_to(:published)
|
|
185
|
+
# => false
|
|
186
|
+
|
|
187
|
+
post.errors.full_messages
|
|
188
|
+
# => ["Title can't be blank"]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If you wish to bypass this behavior and skip validations during a transition, you can do that:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
post = Post.create(status: "draft")
|
|
195
|
+
|
|
196
|
+
post.title = nil
|
|
197
|
+
post.status.transition_to(:published, skip_validations: true)
|
|
198
|
+
# => true
|
|
199
|
+
```
|
|
200
|
+
|
|
120
201
|
### Advanced Usage
|
|
121
202
|
|
|
122
|
-
|
|
203
|
+
#### Transactional Transitions
|
|
204
|
+
|
|
205
|
+
There may be a situation where you want to manually rollback a state change in one of the provided transition callbacks. To do this, add the `transactional: true` option to the `state_options` declaration. This results in the transition being wrapped in a transaction. You can then use the `rollback_transition` method in your callback when you want to trigger a rollback of the transaction. This will allow you to prevent the transition from persisting if something further down the line fails.
|
|
123
206
|
|
|
124
207
|
```ruby
|
|
125
208
|
module Workflow
|
|
@@ -130,15 +213,48 @@ module Workflow
|
|
|
130
213
|
rollback_transition unless notified_watchers?
|
|
131
214
|
end
|
|
132
215
|
|
|
216
|
+
after_transition_commit do
|
|
217
|
+
enqueue_external_work
|
|
218
|
+
end
|
|
219
|
+
|
|
133
220
|
private
|
|
134
221
|
|
|
222
|
+
def enqueue_external_work
|
|
223
|
+
# Any work you want to happen only after the transition is committed.
|
|
224
|
+
# Enqueuing a job, calling an external API, sending a webhook, etc.
|
|
225
|
+
end
|
|
226
|
+
|
|
135
227
|
def notified_watchers?
|
|
136
|
-
|
|
228
|
+
# Any dependent work that you want to run that should play a part in determining
|
|
229
|
+
# whether the transition was successful or not and needs to be rolled back.
|
|
137
230
|
end
|
|
138
231
|
end
|
|
139
232
|
end
|
|
140
233
|
```
|
|
141
234
|
|
|
235
|
+
#### Transient Transition Variables
|
|
236
|
+
|
|
237
|
+
Sometimes you may may want to pass additional arguments to a state transition for additional context in your transition callbacks. To do this, add the `transients` option to the `state_options` declaration. This allows you to define any additional attributes you want to be able to pass along during a state transition to that state.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
module Workflow
|
|
241
|
+
class Post::Archived < HasStateMachine::State
|
|
242
|
+
state_options transients: %i[user]
|
|
243
|
+
|
|
244
|
+
after_transition do
|
|
245
|
+
puts "== Post archived by #{user.name} =="
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
current_user = User.create(name: "John Doe")
|
|
251
|
+
post = Post.create(status: "published")
|
|
252
|
+
|
|
253
|
+
post.status.transition_to(:archived, user: current_user)
|
|
254
|
+
# == Post archived by John Doe ==
|
|
255
|
+
# => true
|
|
256
|
+
```
|
|
257
|
+
|
|
142
258
|
## Contributing
|
|
143
259
|
|
|
144
260
|
Anyone is encouraged to help improve this project. Here are a few ways you can help:
|
|
@@ -15,22 +15,42 @@ module HasStateMachine
|
|
|
15
15
|
# for use on a HasStateMachine::State instance.
|
|
16
16
|
define_model_callbacks :transition, only: %i[before after]
|
|
17
17
|
|
|
18
|
+
##
|
|
19
|
+
# Defines the after_transition_commit callback. It runs only after a
|
|
20
|
+
# transition has committed.
|
|
21
|
+
define_model_callbacks :transition_commit, only: %i[after]
|
|
22
|
+
|
|
18
23
|
##
|
|
19
24
|
# possible_transitions - Retrieves the next available transitions for a given state.
|
|
20
25
|
# transactional? - Determines whether or not the transition should happen with a transactional block.
|
|
21
|
-
|
|
26
|
+
# state - The underscored name of the state
|
|
27
|
+
# transients - Specified list of optional transient attributes on this state
|
|
28
|
+
delegate :possible_transitions, :transactional?, :state, :transients, to: "self.class"
|
|
22
29
|
|
|
23
30
|
##
|
|
24
31
|
# Initializes the HasStateMachine::State instance.
|
|
25
32
|
#
|
|
26
33
|
# @example
|
|
27
34
|
# state = Workflow::Post::Draft.new(post) #=> "draft"
|
|
28
|
-
def initialize(object)
|
|
35
|
+
def initialize(object, transient_values = {})
|
|
29
36
|
@object = object
|
|
30
37
|
|
|
38
|
+
transient_values.to_h.slice(*transients).each do |transient, value|
|
|
39
|
+
instance_variable_set(:"@#{transient}", value)
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
super(state)
|
|
32
43
|
end
|
|
33
44
|
|
|
45
|
+
##
|
|
46
|
+
# Determines if the given desired state exists in the predetermined
|
|
47
|
+
# list of allowed transitions.
|
|
48
|
+
# @param desired_state [String, Symbol] the state to check if the object can transition to
|
|
49
|
+
# @return [Boolean] whether or not the object can transition to the desired state
|
|
50
|
+
def can_transition?(desired_state)
|
|
51
|
+
possible_transitions.include? desired_state.to_s
|
|
52
|
+
end
|
|
53
|
+
|
|
34
54
|
##
|
|
35
55
|
# Checks to see if the desired state is valid and then gives
|
|
36
56
|
# responsibility to the desired state's instance to make the
|
|
@@ -43,43 +63,56 @@ module HasStateMachine
|
|
|
43
63
|
# @return [Boolean] whether or not the transition took place
|
|
44
64
|
def transition_to(desired_state, **options)
|
|
45
65
|
transitioned = false
|
|
66
|
+
options = options.transform_keys(&:to_sym)
|
|
67
|
+
desired_state_instance = state_instance(desired_state, options)
|
|
46
68
|
|
|
47
69
|
with_transition_options(options) do
|
|
48
|
-
return false unless valid_transition?(
|
|
49
|
-
|
|
50
|
-
desired_state = state_instance(desired_state.to_s)
|
|
70
|
+
return false unless valid_transition?(desired_state_instance)
|
|
51
71
|
|
|
52
|
-
transitioned = if
|
|
53
|
-
|
|
72
|
+
transitioned = if desired_state_instance.transactional?
|
|
73
|
+
desired_state_instance.perform_transactional_transition!
|
|
54
74
|
else
|
|
55
|
-
|
|
75
|
+
desired_state_instance.perform_transition!
|
|
56
76
|
end
|
|
57
77
|
end
|
|
58
78
|
|
|
59
79
|
transitioned
|
|
80
|
+
ensure
|
|
81
|
+
(desired_state_instance&.errors || []).each do |error|
|
|
82
|
+
object.errors.add(error.attribute, error.type)
|
|
83
|
+
end
|
|
60
84
|
end
|
|
61
85
|
|
|
62
86
|
##
|
|
63
87
|
# Makes the actual transition from one state to the next and
|
|
64
|
-
# runs the before and after transition callbacks.
|
|
88
|
+
# runs the before and after transition callbacks. The
|
|
89
|
+
# after_transition_commit callbacks run after the update completes
|
|
90
|
+
# and only when it succeeds.
|
|
65
91
|
def perform_transition!
|
|
66
|
-
run_callbacks :
|
|
67
|
-
|
|
92
|
+
run_callbacks :transition_commit do
|
|
93
|
+
run_callbacks :transition do
|
|
94
|
+
object.update("#{object.state_attribute}": state)
|
|
95
|
+
end
|
|
68
96
|
end
|
|
69
97
|
end
|
|
70
98
|
|
|
71
99
|
##
|
|
72
100
|
# Makes the actual transition from one state to the next and
|
|
73
101
|
# runs the before and after transition callbacks in a transaction
|
|
74
|
-
# to allow for roll backs.
|
|
102
|
+
# to allow for roll backs. The after_transition_commit callbacks run
|
|
103
|
+
# outside the transaction and only when it commits (not on rollback).
|
|
75
104
|
def perform_transactional_transition!
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
105
|
+
run_callbacks :transition_commit do
|
|
106
|
+
ActiveRecord::Base.transaction(requires_new: true, joinable: false) do
|
|
107
|
+
run_callbacks :transition do
|
|
108
|
+
rollback_transition unless object.update("#{object.state_attribute}": state)
|
|
109
|
+
end
|
|
79
110
|
end
|
|
80
|
-
end
|
|
81
111
|
|
|
82
|
-
|
|
112
|
+
@previous_state = previous_state
|
|
113
|
+
|
|
114
|
+
object.reload.public_send(object.state_attribute) == state
|
|
115
|
+
end
|
|
83
116
|
end
|
|
84
117
|
|
|
85
118
|
private
|
|
@@ -88,32 +121,25 @@ module HasStateMachine
|
|
|
88
121
|
raise ActiveRecord::Rollback
|
|
89
122
|
end
|
|
90
123
|
|
|
91
|
-
##
|
|
92
|
-
# Determines if the given desired state exists in the predetermined
|
|
93
|
-
# list of allowed transitions.
|
|
94
|
-
def can_transition?(desired_state)
|
|
95
|
-
possible_transitions.include? desired_state
|
|
96
|
-
end
|
|
97
|
-
|
|
98
124
|
##
|
|
99
125
|
# Helper method for grabbing the previous state of the object after
|
|
100
126
|
# it has been transitioned to the new state. Useful in
|
|
101
127
|
# after_transition blocks
|
|
102
128
|
def previous_state
|
|
103
|
-
object.previous_changes[object.state_attribute]&.first
|
|
129
|
+
@previous_state.presence || object.previous_changes[object.state_attribute]&.first
|
|
104
130
|
end
|
|
105
131
|
|
|
106
|
-
def state_instance(desired_state)
|
|
132
|
+
def state_instance(desired_state, transient_values)
|
|
107
133
|
klass = "#{object.workflow_namespace}::#{desired_state.to_s.classify}".safe_constantize
|
|
108
|
-
klass&.new(object)
|
|
134
|
+
klass&.new(object, transient_values)
|
|
109
135
|
end
|
|
110
136
|
|
|
111
|
-
def valid_transition?(
|
|
137
|
+
def valid_transition?(desired_state_instance)
|
|
112
138
|
return true if object.skip_state_validations
|
|
113
139
|
|
|
114
140
|
object.valid? &&
|
|
115
|
-
can_transition?(
|
|
116
|
-
|
|
141
|
+
can_transition?(desired_state_instance) &&
|
|
142
|
+
desired_state_instance&.valid?
|
|
117
143
|
end
|
|
118
144
|
|
|
119
145
|
def with_transition_options(options, &block)
|
|
@@ -135,21 +161,24 @@ module HasStateMachine
|
|
|
135
161
|
@transactional || false
|
|
136
162
|
end
|
|
137
163
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
# states the current state can transition to.
|
|
141
|
-
def transitions_to(states)
|
|
142
|
-
state_options(transitions_to: states)
|
|
143
|
-
HasStateMachine::Deprecation.deprecation_warning(:transitions_to, "use state_options instead")
|
|
164
|
+
def transients
|
|
165
|
+
@transients || []
|
|
144
166
|
end
|
|
145
167
|
|
|
146
168
|
##
|
|
147
169
|
# Set the options for the HasStateMachine::State classes to define the possible
|
|
148
170
|
# states the current state can transition to and whether or not transitioning
|
|
149
171
|
# to the state should be performed within a transaction.
|
|
150
|
-
def state_options(transitions_to: [], transactional: false)
|
|
172
|
+
def state_options(transitions_to: [], transactional: false, transients: [])
|
|
151
173
|
@possible_transitions = transitions_to.map(&:to_s)
|
|
152
174
|
@transactional = transactional
|
|
175
|
+
@transients = transients.map(&:to_sym)
|
|
176
|
+
|
|
177
|
+
transients.each do |transient_name|
|
|
178
|
+
define_method(transient_name) do
|
|
179
|
+
instance_variable_get(:"@#{transient_name}")
|
|
180
|
+
end
|
|
181
|
+
end
|
|
153
182
|
end
|
|
154
183
|
end
|
|
155
184
|
end
|
|
@@ -114,10 +114,10 @@ module HasStateMachine
|
|
|
114
114
|
def state_instance_validations
|
|
115
115
|
return unless state_class.present?
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
return if
|
|
117
|
+
current_state_instance = public_send(state_attribute.to_s)
|
|
118
|
+
return if current_state_instance.valid?
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
current_state_instance.errors.each do |error|
|
|
121
121
|
errors.add(error.attribute, error.type)
|
|
122
122
|
end
|
|
123
123
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: has_state_machine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Benjamin Hargett
|
|
8
8
|
- Jake Hurd
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
12
|
dependencies:
|
|
14
13
|
- !ruby/object:Gem::Dependency
|
|
15
14
|
name: rails
|
|
@@ -29,16 +28,16 @@ dependencies:
|
|
|
29
28
|
name: sqlite3
|
|
30
29
|
requirement: !ruby/object:Gem::Requirement
|
|
31
30
|
requirements:
|
|
32
|
-
- - "
|
|
31
|
+
- - ">="
|
|
33
32
|
- !ruby/object:Gem::Version
|
|
34
|
-
version: '1.
|
|
33
|
+
version: '1.5'
|
|
35
34
|
type: :development
|
|
36
35
|
prerelease: false
|
|
37
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
38
37
|
requirements:
|
|
39
|
-
- - "
|
|
38
|
+
- - ">="
|
|
40
39
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: '1.
|
|
40
|
+
version: '1.5'
|
|
42
41
|
- !ruby/object:Gem::Dependency
|
|
43
42
|
name: pry
|
|
44
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,6 +94,20 @@ dependencies:
|
|
|
95
94
|
- - ">="
|
|
96
95
|
- !ruby/object:Gem::Version
|
|
97
96
|
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: minitest
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '5.1'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '5.1'
|
|
98
111
|
description: HasStateMachine uses ruby classes to make creating a finite state machine
|
|
99
112
|
in your ActiveRecord models a breeze.
|
|
100
113
|
email:
|
|
@@ -119,7 +132,6 @@ homepage: https://www.github.com/encampment/has_state_machine
|
|
|
119
132
|
licenses:
|
|
120
133
|
- MIT
|
|
121
134
|
metadata: {}
|
|
122
|
-
post_install_message:
|
|
123
135
|
rdoc_options: []
|
|
124
136
|
require_paths:
|
|
125
137
|
- lib
|
|
@@ -134,8 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
146
|
- !ruby/object:Gem::Version
|
|
135
147
|
version: '0'
|
|
136
148
|
requirements: []
|
|
137
|
-
rubygems_version: 3.
|
|
138
|
-
signing_key:
|
|
149
|
+
rubygems_version: 3.6.7
|
|
139
150
|
specification_version: 4
|
|
140
151
|
summary: Class based state machine for ActiveRecord models.
|
|
141
152
|
test_files: []
|