has_state_machine 0.6.1 → 1.0.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 +105 -6
- data/lib/has_state_machine/state.rb +42 -27
- data/lib/has_state_machine/state_helpers.rb +3 -3
- data/lib/has_state_machine/version.rb +1 -1
- metadata +7 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7aad95d7b489183fa7a7c49af178871cb5cc169dbc3a9ae6bd4a93f7ce48ebea
|
4
|
+
data.tar.gz: 180dbc281d868326a908955013b17fcc935e54de43dbb367212f017e642b994d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1eca09471018292e2cc4f41a6a53ec2cf12bc087de32af80595901099a4b589131b529bbf53891de89eacc86c5a9a3c16ca7102f31faec63443003c697c45a7e
|
7
|
+
data.tar.gz: 1b5a1e0a93e9f0648deac92b4fcba653ffeec037e9b3959092e71b5f9b391c93118e352ced5e7c1f44f385ceed1130ab62f1fde11dc4282ea2fb17097e66d47a
|
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
|
@@ -117,9 +119,83 @@ post.status.transition_to(:archived)
|
|
117
119
|
# => true
|
118
120
|
```
|
119
121
|
|
122
|
+
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)
|
123
|
+
|
124
|
+
Example:
|
125
|
+
```ruby
|
126
|
+
post = Post.create(status: "draft")
|
127
|
+
|
128
|
+
post.status.can_transition?(:published) # => true
|
129
|
+
post.status.can_transition?(:other_state) # => false
|
130
|
+
```
|
131
|
+
|
132
|
+
### Validations and Error Handling
|
133
|
+
|
134
|
+
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.
|
135
|
+
|
136
|
+
By default, validations defined on the state will be run as part of the object validations if the object is in that state.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
post = Post.create(status: "published", title: "Title")
|
140
|
+
|
141
|
+
post.valid?
|
142
|
+
# => true
|
143
|
+
|
144
|
+
post.title = nil
|
145
|
+
post.valid?
|
146
|
+
# => false
|
147
|
+
```
|
148
|
+
|
149
|
+
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.
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
class Post < ApplicationRecord
|
153
|
+
has_state_machine states: %i[draft published, archived], state_validations_on_object: false
|
154
|
+
end
|
155
|
+
|
156
|
+
post = Post.create(status: "published", title: "Title")
|
157
|
+
|
158
|
+
post.valid?
|
159
|
+
# => true
|
160
|
+
|
161
|
+
post.title = nil
|
162
|
+
post.valid?
|
163
|
+
# => true
|
164
|
+
```
|
165
|
+
|
166
|
+
By default, when attempting to transition an object to another state, it checks:
|
167
|
+
* Validations defined on the object
|
168
|
+
* That the new state is one of the allowed transitions from the current state
|
169
|
+
* Any validations defined on the new state
|
170
|
+
|
171
|
+
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.
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
post = Post.create(status: "draft")
|
175
|
+
|
176
|
+
post.title = nil
|
177
|
+
post.status.transition_to(:published)
|
178
|
+
# => false
|
179
|
+
|
180
|
+
post.errors.full_messages
|
181
|
+
# => ["Title can't be blank"]
|
182
|
+
```
|
183
|
+
|
184
|
+
If you wish to bypass this behavior and skip validations during a transition, you can do that:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
post = Post.create(status: "draft")
|
188
|
+
|
189
|
+
post.title = nil
|
190
|
+
post.status.transition_to(:published, skip_validations: true)
|
191
|
+
# => true
|
192
|
+
```
|
193
|
+
|
120
194
|
### Advanced Usage
|
121
195
|
|
122
|
-
|
196
|
+
#### Transactional Transitions
|
197
|
+
|
198
|
+
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
199
|
|
124
200
|
```ruby
|
125
201
|
module Workflow
|
@@ -139,6 +215,29 @@ module Workflow
|
|
139
215
|
end
|
140
216
|
```
|
141
217
|
|
218
|
+
#### Transient Transition Variables
|
219
|
+
|
220
|
+
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.
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
module Workflow
|
224
|
+
class Post::Archived < HasStateMachine::State
|
225
|
+
state_options transients: %i[user]
|
226
|
+
|
227
|
+
after_transition do
|
228
|
+
puts "== Post archived by #{user.name} =="
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
current_user = User.create(name: "John Doe")
|
234
|
+
post = Post.create(status: "published")
|
235
|
+
|
236
|
+
post.status.transition_to(:archived, user: current_user)
|
237
|
+
# == Post archived by John Doe ==
|
238
|
+
# => true
|
239
|
+
```
|
240
|
+
|
142
241
|
## Contributing
|
143
242
|
|
144
243
|
Anyone is encouraged to help improve this project. Here are a few ways you can help:
|
@@ -18,19 +18,34 @@ module HasStateMachine
|
|
18
18
|
##
|
19
19
|
# possible_transitions - Retrieves the next available transitions for a given state.
|
20
20
|
# transactional? - Determines whether or not the transition should happen with a transactional block.
|
21
|
-
|
21
|
+
# state - The underscored name of the state
|
22
|
+
# transients - Specified list of optional transient attributes on this state
|
23
|
+
delegate :possible_transitions, :transactional?, :state, :transients, to: "self.class"
|
22
24
|
|
23
25
|
##
|
24
26
|
# Initializes the HasStateMachine::State instance.
|
25
27
|
#
|
26
28
|
# @example
|
27
29
|
# state = Workflow::Post::Draft.new(post) #=> "draft"
|
28
|
-
def initialize(object)
|
30
|
+
def initialize(object, transient_values = {})
|
29
31
|
@object = object
|
30
32
|
|
33
|
+
transient_values.to_h.slice(*transients).each do |transient, value|
|
34
|
+
instance_variable_set(:"@#{transient}", value)
|
35
|
+
end
|
36
|
+
|
31
37
|
super(state)
|
32
38
|
end
|
33
39
|
|
40
|
+
##
|
41
|
+
# Determines if the given desired state exists in the predetermined
|
42
|
+
# list of allowed transitions.
|
43
|
+
# @param desired_state [String, Symbol] the state to check if the object can transition to
|
44
|
+
# @return [Boolean] whether or not the object can transition to the desired state
|
45
|
+
def can_transition?(desired_state)
|
46
|
+
possible_transitions.include? desired_state.to_s
|
47
|
+
end
|
48
|
+
|
34
49
|
##
|
35
50
|
# Checks to see if the desired state is valid and then gives
|
36
51
|
# responsibility to the desired state's instance to make the
|
@@ -43,20 +58,24 @@ module HasStateMachine
|
|
43
58
|
# @return [Boolean] whether or not the transition took place
|
44
59
|
def transition_to(desired_state, **options)
|
45
60
|
transitioned = false
|
61
|
+
options = options.transform_keys(&:to_sym)
|
62
|
+
desired_state_instance = state_instance(desired_state, options)
|
46
63
|
|
47
64
|
with_transition_options(options) do
|
48
|
-
return false unless valid_transition?(
|
65
|
+
return false unless valid_transition?(desired_state_instance)
|
49
66
|
|
50
|
-
|
51
|
-
|
52
|
-
transitioned = if desired_state.transactional?
|
53
|
-
desired_state.perform_transactional_transition!
|
67
|
+
transitioned = if desired_state_instance.transactional?
|
68
|
+
desired_state_instance.perform_transactional_transition!
|
54
69
|
else
|
55
|
-
|
70
|
+
desired_state_instance.perform_transition!
|
56
71
|
end
|
57
72
|
end
|
58
73
|
|
59
74
|
transitioned
|
75
|
+
ensure
|
76
|
+
(desired_state_instance&.errors || []).each do |error|
|
77
|
+
object.errors.add(error.attribute, error.type)
|
78
|
+
end
|
60
79
|
end
|
61
80
|
|
62
81
|
##
|
@@ -88,13 +107,6 @@ module HasStateMachine
|
|
88
107
|
raise ActiveRecord::Rollback
|
89
108
|
end
|
90
109
|
|
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
110
|
##
|
99
111
|
# Helper method for grabbing the previous state of the object after
|
100
112
|
# it has been transitioned to the new state. Useful in
|
@@ -103,17 +115,17 @@ module HasStateMachine
|
|
103
115
|
object.previous_changes[object.state_attribute]&.first
|
104
116
|
end
|
105
117
|
|
106
|
-
def state_instance(desired_state)
|
118
|
+
def state_instance(desired_state, transient_values)
|
107
119
|
klass = "#{object.workflow_namespace}::#{desired_state.to_s.classify}".safe_constantize
|
108
|
-
klass&.new(object)
|
120
|
+
klass&.new(object, transient_values)
|
109
121
|
end
|
110
122
|
|
111
|
-
def valid_transition?(
|
123
|
+
def valid_transition?(desired_state_instance)
|
112
124
|
return true if object.skip_state_validations
|
113
125
|
|
114
126
|
object.valid? &&
|
115
|
-
can_transition?(
|
116
|
-
|
127
|
+
can_transition?(desired_state_instance) &&
|
128
|
+
desired_state_instance&.valid?
|
117
129
|
end
|
118
130
|
|
119
131
|
def with_transition_options(options, &block)
|
@@ -135,21 +147,24 @@ module HasStateMachine
|
|
135
147
|
@transactional || false
|
136
148
|
end
|
137
149
|
|
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")
|
150
|
+
def transients
|
151
|
+
@transients || []
|
144
152
|
end
|
145
153
|
|
146
154
|
##
|
147
155
|
# Set the options for the HasStateMachine::State classes to define the possible
|
148
156
|
# states the current state can transition to and whether or not transitioning
|
149
157
|
# to the state should be performed within a transaction.
|
150
|
-
def state_options(transitions_to: [], transactional: false)
|
158
|
+
def state_options(transitions_to: [], transactional: false, transients: [])
|
151
159
|
@possible_transitions = transitions_to.map(&:to_s)
|
152
160
|
@transactional = transactional
|
161
|
+
@transients = transients.map(&:to_sym)
|
162
|
+
|
163
|
+
transients.each do |transient_name|
|
164
|
+
define_method(transient_name) do
|
165
|
+
instance_variable_get(:"@#{transient_name}")
|
166
|
+
end
|
167
|
+
end
|
153
168
|
end
|
154
169
|
end
|
155
170
|
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: 0.
|
4
|
+
version: 1.0.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
|
@@ -119,7 +118,6 @@ homepage: https://www.github.com/encampment/has_state_machine
|
|
119
118
|
licenses:
|
120
119
|
- MIT
|
121
120
|
metadata: {}
|
122
|
-
post_install_message:
|
123
121
|
rdoc_options: []
|
124
122
|
require_paths:
|
125
123
|
- lib
|
@@ -134,8 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
132
|
- !ruby/object:Gem::Version
|
135
133
|
version: '0'
|
136
134
|
requirements: []
|
137
|
-
rubygems_version: 3.
|
138
|
-
signing_key:
|
135
|
+
rubygems_version: 3.6.7
|
139
136
|
specification_version: 4
|
140
137
|
summary: Class based state machine for ActiveRecord models.
|
141
138
|
test_files: []
|