rails_state_machine 1.1.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/.travis.yml +20 -8
- data/CHANGELOG.md +12 -2
- data/Gemfile.5.1.pg.lock +3 -3
- data/Gemfile.5.2.pg.lock +3 -3
- data/Gemfile.6.0.pg +15 -0
- data/Gemfile.6.0.pg.lock +173 -0
- data/README.md +42 -34
- data/lib/rails_state_machine.rb +2 -0
- data/lib/rails_state_machine/callbacks.rb +68 -0
- data/lib/rails_state_machine/model.rb +57 -7
- data/lib/rails_state_machine/state_machine.rb +41 -134
- data/lib/rails_state_machine/state_manager.rb +47 -0
- data/lib/rails_state_machine/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 12ccebe03922ae8add9f35e88e0b2d9759e8105b8278713242fc88df697a320a
|
4
|
+
data.tar.gz: 44c8d685840968e1e69ce0a225062a9ca84e2715fd515cc7cbada5923dff204a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e1a075ea8c01a222e872672925ce96c884491f4deced754c21dc598f65dd5aecb9381f0097269c9efdd5da855a10a69e9a1da63b1decaf047990cc7e8e57487
|
7
|
+
data.tar.gz: 6337d7f34d934b8a62239dcef8ed45a88fe545311cbc0e0c7b0728a03f047362e237104802caf929ff3ea72e669ba445365dc8afa7e92737893904421f798ff4
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.4.
|
1
|
+
2.4.6
|
data/.travis.yml
CHANGED
@@ -7,19 +7,31 @@ before_script:
|
|
7
7
|
- mysql -e 'create database IF NOT EXISTS rails_state_machine_test;'
|
8
8
|
|
9
9
|
install:
|
10
|
+
- gem install bundler:2.0.2
|
10
11
|
# Replace default Travis CI bundler script with a version that doesn't
|
11
12
|
# explode when lockfile doesn't match recently bumped version
|
12
13
|
- bundle install --no-deployment --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle}
|
13
14
|
|
14
15
|
script: bundle exec rake current_rspec
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
gemfile:
|
22
|
-
|
23
|
-
|
17
|
+
matrix:
|
18
|
+
include:
|
19
|
+
- rvm: 2.3.8
|
20
|
+
gemfile: Gemfile.5.1.pg
|
21
|
+
- rvm: 2.4.6
|
22
|
+
gemfile: Gemfile.5.1.pg
|
23
|
+
- rvm: 2.3.8
|
24
|
+
gemfile: Gemfile.5.2.pg
|
25
|
+
- rvm: 2.4.6
|
26
|
+
gemfile: Gemfile.5.2.pg
|
27
|
+
- rvm: 2.5.6
|
28
|
+
gemfile: Gemfile.5.2.pg
|
29
|
+
- rvm: 2.5.6
|
30
|
+
gemfile: Gemfile.6.0.pg
|
31
|
+
- rvm: 2.6.4
|
32
|
+
gemfile: Gemfile.6.0.pg
|
24
33
|
|
25
34
|
dist: trusty
|
35
|
+
|
36
|
+
addons:
|
37
|
+
postgresql: 9.3
|
data/CHANGELOG.md
CHANGED
@@ -5,13 +5,23 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
|
|
5
5
|
|
6
6
|
## Unreleased
|
7
7
|
|
8
|
+
### Compatible changes
|
9
|
+
|
8
10
|
### Breaking changes
|
9
11
|
|
10
|
-
|
12
|
+
|
13
|
+
## 2.0.0 2019-09-30
|
11
14
|
|
12
15
|
### Compatible changes
|
13
16
|
|
14
|
-
-
|
17
|
+
- Added: State machine can now use an attribute other than `state` to represent the machine's state.
|
18
|
+
- Added: It is now possible to define multiple state machines on the same model. States and event names
|
19
|
+
have to differ, though.
|
20
|
+
|
21
|
+
### Breaking changes
|
22
|
+
|
23
|
+
- Removed: Dropped support for adding a state machine to a model without including `RailsStateMachine::Model`.
|
24
|
+
|
15
25
|
|
16
26
|
## 1.1.3 2019-08-12
|
17
27
|
|
data/Gemfile.5.1.pg.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rails_state_machine (
|
4
|
+
rails_state_machine (2.0.0)
|
5
5
|
activerecord
|
6
6
|
|
7
7
|
GEM
|
@@ -53,7 +53,7 @@ GEM
|
|
53
53
|
database_cleaner (1.6.2)
|
54
54
|
diff-lcs (1.3)
|
55
55
|
erubi (1.7.0)
|
56
|
-
gemika (0.
|
56
|
+
gemika (0.4.0)
|
57
57
|
globalid (0.4.1)
|
58
58
|
activesupport (>= 4.2.0)
|
59
59
|
i18n (0.9.1)
|
@@ -146,4 +146,4 @@ DEPENDENCIES
|
|
146
146
|
rspec (~> 3.5)
|
147
147
|
|
148
148
|
BUNDLED WITH
|
149
|
-
|
149
|
+
2.0.2
|
data/Gemfile.5.2.pg.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rails_state_machine (
|
4
|
+
rails_state_machine (2.0.0)
|
5
5
|
activerecord
|
6
6
|
|
7
7
|
GEM
|
@@ -57,7 +57,7 @@ GEM
|
|
57
57
|
database_cleaner (1.7.0)
|
58
58
|
diff-lcs (1.3)
|
59
59
|
erubi (1.7.1)
|
60
|
-
gemika (0.
|
60
|
+
gemika (0.4.0)
|
61
61
|
globalid (0.4.1)
|
62
62
|
activesupport (>= 4.2.0)
|
63
63
|
i18n (1.1.0)
|
@@ -154,4 +154,4 @@ DEPENDENCIES
|
|
154
154
|
rspec (~> 3.5)
|
155
155
|
|
156
156
|
BUNDLED WITH
|
157
|
-
|
157
|
+
2.0.2
|
data/Gemfile.6.0.pg
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Runtime dependencies
|
4
|
+
gem 'rails', '~>6.0.0'
|
5
|
+
gem 'pg'
|
6
|
+
|
7
|
+
# Development dependencies
|
8
|
+
gem 'rspec', '~>3.5'
|
9
|
+
gem 'rake'
|
10
|
+
gem 'pry-byebug'
|
11
|
+
gem 'gemika'
|
12
|
+
gem 'database_cleaner'
|
13
|
+
|
14
|
+
# Gem under test
|
15
|
+
gem 'rails_state_machine', :path => '.'
|
data/Gemfile.6.0.pg.lock
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rails_state_machine (2.0.0)
|
5
|
+
activerecord
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actioncable (6.0.0)
|
11
|
+
actionpack (= 6.0.0)
|
12
|
+
nio4r (~> 2.0)
|
13
|
+
websocket-driver (>= 0.6.1)
|
14
|
+
actionmailbox (6.0.0)
|
15
|
+
actionpack (= 6.0.0)
|
16
|
+
activejob (= 6.0.0)
|
17
|
+
activerecord (= 6.0.0)
|
18
|
+
activestorage (= 6.0.0)
|
19
|
+
activesupport (= 6.0.0)
|
20
|
+
mail (>= 2.7.1)
|
21
|
+
actionmailer (6.0.0)
|
22
|
+
actionpack (= 6.0.0)
|
23
|
+
actionview (= 6.0.0)
|
24
|
+
activejob (= 6.0.0)
|
25
|
+
mail (~> 2.5, >= 2.5.4)
|
26
|
+
rails-dom-testing (~> 2.0)
|
27
|
+
actionpack (6.0.0)
|
28
|
+
actionview (= 6.0.0)
|
29
|
+
activesupport (= 6.0.0)
|
30
|
+
rack (~> 2.0)
|
31
|
+
rack-test (>= 0.6.3)
|
32
|
+
rails-dom-testing (~> 2.0)
|
33
|
+
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
34
|
+
actiontext (6.0.0)
|
35
|
+
actionpack (= 6.0.0)
|
36
|
+
activerecord (= 6.0.0)
|
37
|
+
activestorage (= 6.0.0)
|
38
|
+
activesupport (= 6.0.0)
|
39
|
+
nokogiri (>= 1.8.5)
|
40
|
+
actionview (6.0.0)
|
41
|
+
activesupport (= 6.0.0)
|
42
|
+
builder (~> 3.1)
|
43
|
+
erubi (~> 1.4)
|
44
|
+
rails-dom-testing (~> 2.0)
|
45
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
46
|
+
activejob (6.0.0)
|
47
|
+
activesupport (= 6.0.0)
|
48
|
+
globalid (>= 0.3.6)
|
49
|
+
activemodel (6.0.0)
|
50
|
+
activesupport (= 6.0.0)
|
51
|
+
activerecord (6.0.0)
|
52
|
+
activemodel (= 6.0.0)
|
53
|
+
activesupport (= 6.0.0)
|
54
|
+
activestorage (6.0.0)
|
55
|
+
actionpack (= 6.0.0)
|
56
|
+
activejob (= 6.0.0)
|
57
|
+
activerecord (= 6.0.0)
|
58
|
+
marcel (~> 0.3.1)
|
59
|
+
activesupport (6.0.0)
|
60
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
61
|
+
i18n (>= 0.7, < 2)
|
62
|
+
minitest (~> 5.1)
|
63
|
+
tzinfo (~> 1.1)
|
64
|
+
zeitwerk (~> 2.1, >= 2.1.8)
|
65
|
+
builder (3.2.3)
|
66
|
+
byebug (10.0.2)
|
67
|
+
coderay (1.1.2)
|
68
|
+
concurrent-ruby (1.1.5)
|
69
|
+
crass (1.0.4)
|
70
|
+
database_cleaner (1.7.0)
|
71
|
+
diff-lcs (1.3)
|
72
|
+
erubi (1.9.0)
|
73
|
+
gemika (0.4.0)
|
74
|
+
globalid (0.4.2)
|
75
|
+
activesupport (>= 4.2.0)
|
76
|
+
i18n (1.6.0)
|
77
|
+
concurrent-ruby (~> 1.0)
|
78
|
+
loofah (2.3.0)
|
79
|
+
crass (~> 1.0.2)
|
80
|
+
nokogiri (>= 1.5.9)
|
81
|
+
mail (2.7.1)
|
82
|
+
mini_mime (>= 0.1.1)
|
83
|
+
marcel (0.3.3)
|
84
|
+
mimemagic (~> 0.3.2)
|
85
|
+
method_source (0.9.2)
|
86
|
+
mimemagic (0.3.3)
|
87
|
+
mini_mime (1.0.2)
|
88
|
+
mini_portile2 (2.4.0)
|
89
|
+
minitest (5.12.2)
|
90
|
+
nio4r (2.5.2)
|
91
|
+
nokogiri (1.10.4)
|
92
|
+
mini_portile2 (~> 2.4.0)
|
93
|
+
pg (1.1.2)
|
94
|
+
pry (0.11.3)
|
95
|
+
coderay (~> 1.1.0)
|
96
|
+
method_source (~> 0.9.0)
|
97
|
+
pry-byebug (3.6.0)
|
98
|
+
byebug (~> 10.0)
|
99
|
+
pry (~> 0.10)
|
100
|
+
rack (2.0.7)
|
101
|
+
rack-test (1.1.0)
|
102
|
+
rack (>= 1.0, < 3)
|
103
|
+
rails (6.0.0)
|
104
|
+
actioncable (= 6.0.0)
|
105
|
+
actionmailbox (= 6.0.0)
|
106
|
+
actionmailer (= 6.0.0)
|
107
|
+
actionpack (= 6.0.0)
|
108
|
+
actiontext (= 6.0.0)
|
109
|
+
actionview (= 6.0.0)
|
110
|
+
activejob (= 6.0.0)
|
111
|
+
activemodel (= 6.0.0)
|
112
|
+
activerecord (= 6.0.0)
|
113
|
+
activestorage (= 6.0.0)
|
114
|
+
activesupport (= 6.0.0)
|
115
|
+
bundler (>= 1.3.0)
|
116
|
+
railties (= 6.0.0)
|
117
|
+
sprockets-rails (>= 2.0.0)
|
118
|
+
rails-dom-testing (2.0.3)
|
119
|
+
activesupport (>= 4.2.0)
|
120
|
+
nokogiri (>= 1.6)
|
121
|
+
rails-html-sanitizer (1.2.0)
|
122
|
+
loofah (~> 2.2, >= 2.2.2)
|
123
|
+
railties (6.0.0)
|
124
|
+
actionpack (= 6.0.0)
|
125
|
+
activesupport (= 6.0.0)
|
126
|
+
method_source
|
127
|
+
rake (>= 0.8.7)
|
128
|
+
thor (>= 0.20.3, < 2.0)
|
129
|
+
rake (13.0.0)
|
130
|
+
rspec (3.8.0)
|
131
|
+
rspec-core (~> 3.8.0)
|
132
|
+
rspec-expectations (~> 3.8.0)
|
133
|
+
rspec-mocks (~> 3.8.0)
|
134
|
+
rspec-core (3.8.0)
|
135
|
+
rspec-support (~> 3.8.0)
|
136
|
+
rspec-expectations (3.8.1)
|
137
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
138
|
+
rspec-support (~> 3.8.0)
|
139
|
+
rspec-mocks (3.8.0)
|
140
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
141
|
+
rspec-support (~> 3.8.0)
|
142
|
+
rspec-support (3.8.0)
|
143
|
+
sprockets (3.7.2)
|
144
|
+
concurrent-ruby (~> 1.0)
|
145
|
+
rack (> 1, < 3)
|
146
|
+
sprockets-rails (3.2.1)
|
147
|
+
actionpack (>= 4.0)
|
148
|
+
activesupport (>= 4.0)
|
149
|
+
sprockets (>= 3.0.0)
|
150
|
+
thor (0.20.3)
|
151
|
+
thread_safe (0.3.6)
|
152
|
+
tzinfo (1.2.5)
|
153
|
+
thread_safe (~> 0.1)
|
154
|
+
websocket-driver (0.7.1)
|
155
|
+
websocket-extensions (>= 0.1.0)
|
156
|
+
websocket-extensions (0.1.4)
|
157
|
+
zeitwerk (2.1.10)
|
158
|
+
|
159
|
+
PLATFORMS
|
160
|
+
ruby
|
161
|
+
|
162
|
+
DEPENDENCIES
|
163
|
+
database_cleaner
|
164
|
+
gemika
|
165
|
+
pg
|
166
|
+
pry-byebug
|
167
|
+
rails (~> 6.0.0)
|
168
|
+
rails_state_machine!
|
169
|
+
rake
|
170
|
+
rspec (~> 3.5)
|
171
|
+
|
172
|
+
BUNDLED WITH
|
173
|
+
2.0.2
|
data/README.md
CHANGED
@@ -71,39 +71,6 @@ event :request_feedback do
|
|
71
71
|
end
|
72
72
|
```
|
73
73
|
|
74
|
-
As an alternative to using `RailsStateMachine::Model` and `state_machine do`, configure the state machine manually. This only adds the `state_machine` to your model, but no `states` or `state_events`.
|
75
|
-
|
76
|
-
```ruby
|
77
|
-
class YourModel < ApplicationRecord
|
78
|
-
RailsStateMachine::StateMachine.new(self).configure do
|
79
|
-
state :draft, initial: true
|
80
|
-
state :review_pending
|
81
|
-
state :approved
|
82
|
-
state :rejected
|
83
|
-
|
84
|
-
event :request_review do
|
85
|
-
transitions from: [:draft, :rejected], to: :review_pending
|
86
|
-
end
|
87
|
-
|
88
|
-
event :approve do
|
89
|
-
transitions from: :review_pending, to: :approved
|
90
|
-
end
|
91
|
-
|
92
|
-
event :reject do
|
93
|
-
transitions from: :review_pending, to: :rejected
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
def self.states
|
98
|
-
state_machine.state_names
|
99
|
-
end
|
100
|
-
|
101
|
-
def self.state_events
|
102
|
-
state_machine.event_names
|
103
|
-
end
|
104
|
-
end
|
105
|
-
```
|
106
|
-
|
107
74
|
## Event callbacks
|
108
75
|
|
109
76
|
Here is a list with all the available callbacks, listed in the same order in which they will get called during the respective operations. The callbacks are chained with the existing active record callbacks on the model.
|
@@ -137,6 +104,47 @@ event :request_review do
|
|
137
104
|
end
|
138
105
|
```
|
139
106
|
|
107
|
+
## Other state attributes and multiple state machines on the same model
|
108
|
+
|
109
|
+
To use a state attribute other than the default `state`, pass it to the `.state_machine` method:
|
110
|
+
|
111
|
+
```
|
112
|
+
state_machine :review_state do
|
113
|
+
# ...
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
This also allows you to define multiple state machines on the same model. Note that event
|
118
|
+
and state names still have to be unique for the whole model.
|
119
|
+
|
120
|
+
|
121
|
+
## Taking multiple transitions
|
122
|
+
|
123
|
+
You can safely take a second transition inside an after_save callback. All relevant
|
124
|
+
callbacks will be run.
|
125
|
+
|
126
|
+
```
|
127
|
+
state_machine do
|
128
|
+
state :draft, initial: true
|
129
|
+
state :review_pending
|
130
|
+
state :approved
|
131
|
+
|
132
|
+
event :request_review do
|
133
|
+
transitions from: [:draft, :rejected], to: :review_pending
|
134
|
+
|
135
|
+
after_save do
|
136
|
+
if auto_approve?
|
137
|
+
approve
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
event :approve do
|
143
|
+
transitions from: :review_pending, to: :approved
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
140
148
|
## Development
|
141
149
|
|
142
150
|
There are tests in `spec`. We only accept PRs with tests. To run tests:
|
@@ -175,4 +183,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
175
183
|
|
176
184
|
## Credits
|
177
185
|
|
178
|
-
Arne Hartherz
|
186
|
+
Arne Hartherz, Emanuel Denzel, Tobias Kraze from [makandra](https://makandra.de/).
|
data/lib/rails_state_machine.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'rails_state_machine/version'
|
2
2
|
require 'rails_state_machine/state'
|
3
3
|
require 'rails_state_machine/event'
|
4
|
+
require 'rails_state_machine/callbacks'
|
4
5
|
require 'rails_state_machine/state_machine'
|
6
|
+
require 'rails_state_machine/state_manager'
|
5
7
|
require 'rails_state_machine/model'
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module RailsStateMachine
|
2
|
+
module Callbacks
|
3
|
+
class << self
|
4
|
+
def included(model)
|
5
|
+
register_callbacks(model)
|
6
|
+
register_validations(model)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def register_callbacks(model)
|
12
|
+
model.class_eval do
|
13
|
+
before_validation :run_state_events_before_validation
|
14
|
+
before_save :register_state_events_for_callbacks
|
15
|
+
before_save { flush_state_event_callbacks(:before_save) }
|
16
|
+
after_save { flush_state_event_callbacks(:after_save) }
|
17
|
+
after_commit { flush_state_event_callbacks(:after_commit) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def register_validations(model)
|
22
|
+
model.class_eval do
|
23
|
+
after_validation :revert_states, if: -> { errors.any? }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_state_events_before_validation
|
29
|
+
# Since validations may be skipped, we will not register validation callbacks in @state_event_callbacks,
|
30
|
+
# but call them explicitly when before_validation callbacks are triggered.
|
31
|
+
state_machine_state_managers.each do |state_manager|
|
32
|
+
state_manager.next_event&.run_before_validation(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def register_state_events_for_callbacks
|
37
|
+
@state_event_callbacks ||= {
|
38
|
+
before_save: [],
|
39
|
+
after_save: [],
|
40
|
+
after_commit: []
|
41
|
+
}
|
42
|
+
state_machine_state_managers.each do |state_manager|
|
43
|
+
if (next_event = state_manager.next_event)
|
44
|
+
@state_event_callbacks[:before_save] << next_event
|
45
|
+
@state_event_callbacks[:after_save] << next_event
|
46
|
+
@state_event_callbacks[:after_commit] << next_event
|
47
|
+
state_manager.next_event = nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def flush_state_event_callbacks(name)
|
55
|
+
if @state_event_callbacks
|
56
|
+
while (event = @state_event_callbacks[name].shift)
|
57
|
+
event.public_send("run_#{name}", self)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def revert_states
|
63
|
+
state_machine_state_managers.each do |state_manager|
|
64
|
+
state_manager.revert
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,21 +1,71 @@
|
|
1
1
|
module RailsStateMachine
|
2
|
+
DEFAULT_STATE_ATTRIBUTE = :state
|
3
|
+
|
2
4
|
module Model
|
3
5
|
def self.included(base)
|
4
|
-
base.
|
6
|
+
base.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
|
9
|
+
cattr_accessor :state_machines
|
10
|
+
self.state_machines = {}
|
11
|
+
|
12
|
+
delegate :state_machine, to: :class
|
13
|
+
end
|
5
14
|
end
|
6
15
|
|
7
16
|
module ClassMethods
|
8
|
-
def state_machine(&block)
|
9
|
-
StateMachine.new(self)
|
17
|
+
def state_machine(state_attribute = DEFAULT_STATE_ATTRIBUTE, &block)
|
18
|
+
state_machine = state_machines[state_attribute] ||= StateMachine.new(self, state_attribute)
|
19
|
+
if block
|
20
|
+
include(Callbacks) unless self < Callbacks
|
21
|
+
state_machine.configure(&block)
|
22
|
+
end
|
23
|
+
state_machine
|
24
|
+
end
|
25
|
+
|
26
|
+
def states(state_attribute = DEFAULT_STATE_ATTRIBUTE)
|
27
|
+
state_machine(state_attribute).state_names
|
28
|
+
end
|
29
|
+
|
30
|
+
def state_events(state_attribute = DEFAULT_STATE_ATTRIBUTE)
|
31
|
+
state_machine(state_attribute).event_names
|
10
32
|
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
private
|
11
37
|
|
12
|
-
|
13
|
-
|
38
|
+
def state_machine_state_manager(state_attribute)
|
39
|
+
@state_machine_state_managers ||= {}
|
40
|
+
@state_machine_state_managers[state_attribute] ||= StateManager.new(self, state_machine(state_attribute), state_attribute)
|
41
|
+
end
|
42
|
+
|
43
|
+
def state_machine_state_managers
|
44
|
+
self.state_machines.keys.collect do |state_attribute|
|
45
|
+
state_machine_state_manager(state_attribute)
|
14
46
|
end
|
47
|
+
end
|
15
48
|
|
16
|
-
|
17
|
-
|
49
|
+
def prepare_state_event_change(attributes)
|
50
|
+
if ActiveRecord::VERSION::STRING < '5.2' && saved_changes?
|
51
|
+
# After calling `save`, ActiveRecord 5.1 will flag the changes that it just stored as saved.
|
52
|
+
# https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/attribute_methods/dirty.rb#L33-L46
|
53
|
+
#
|
54
|
+
# When taking multiple state events (e.g. a second event called inside an `after_save` callback) and thus
|
55
|
+
# saving after other changes were just saved, we need to mimic that behavior. Otherwise, ActiveRecord will
|
56
|
+
# print deprecation warnings like these:
|
57
|
+
#
|
58
|
+
# DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the
|
59
|
+
# next version of Rails. The new return value will reflect the behavior of calling the method after
|
60
|
+
# `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use
|
61
|
+
# `attribute_before_last_save` instead.
|
62
|
+
#
|
63
|
+
# These actually originate from ActiveRecord internals which try to determine the changes that should be
|
64
|
+
# stored for the second save. It is probably a shortcoming of ActiveRecord 5.1.x that will be fixed, but
|
65
|
+
# since the current/previous save was already successful, the right action is to just call `changes_applied`.
|
66
|
+
changes_applied
|
18
67
|
end
|
68
|
+
self.attributes = attributes
|
19
69
|
end
|
20
70
|
end
|
21
71
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module RailsStateMachine
|
2
2
|
class StateMachine
|
3
|
-
|
4
|
-
@model = model
|
5
|
-
|
6
|
-
model_constant('StateMachineMethods', Module.new)
|
7
|
-
@model.include(@model::StateMachineMethods)
|
3
|
+
attr_reader :model
|
8
4
|
|
5
|
+
def initialize(model, state_attribute)
|
6
|
+
@model = model
|
7
|
+
@state_attribute = state_attribute
|
9
8
|
@states_by_name = {}
|
10
9
|
@events_by_name = {}
|
10
|
+
build_model_module
|
11
11
|
end
|
12
12
|
|
13
13
|
def configure(&block)
|
@@ -18,11 +18,6 @@ module RailsStateMachine
|
|
18
18
|
register_initial_state
|
19
19
|
|
20
20
|
define_event_methods
|
21
|
-
|
22
|
-
register_callbacks
|
23
|
-
register_validations
|
24
|
-
register_state_machine
|
25
|
-
|
26
21
|
define_model_methods
|
27
22
|
end
|
28
23
|
|
@@ -60,18 +55,6 @@ module RailsStateMachine
|
|
60
55
|
event = Event.new(name, self)
|
61
56
|
event.configure(&block)
|
62
57
|
|
63
|
-
model_methods do
|
64
|
-
define_method "#{event.name}" do |**attributes|
|
65
|
-
prepare_state_event_change(attributes.merge(state_event: event.name))
|
66
|
-
save
|
67
|
-
end
|
68
|
-
|
69
|
-
define_method "#{event.name}!" do |**attributes|
|
70
|
-
prepare_state_event_change(attributes.merge(state_event: event.name))
|
71
|
-
save!
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
58
|
@events_by_name[name] = event
|
76
59
|
end
|
77
60
|
|
@@ -79,18 +62,24 @@ module RailsStateMachine
|
|
79
62
|
@model.const_set(name, value)
|
80
63
|
end
|
81
64
|
|
82
|
-
def
|
65
|
+
def build_model_module
|
83
66
|
# Using a state machine defines several methods on the model.
|
84
67
|
# The model should be able to re-define them and `super` into the original method, if necessary.
|
85
68
|
# For that, we use a module to store all methods. The module is loaded into the model class.
|
86
|
-
@
|
69
|
+
@model_module = Module.new
|
70
|
+
@model.include(@model_module)
|
71
|
+
end
|
72
|
+
|
73
|
+
def model_module_eval(&block)
|
74
|
+
@model_module.module_eval(&block)
|
87
75
|
end
|
88
76
|
|
89
77
|
def define_state_methods
|
78
|
+
state_attribute = @state_attribute
|
90
79
|
state_names.each do |state_name|
|
91
|
-
|
80
|
+
model_module_eval do
|
92
81
|
define_method "#{state_name}?" do
|
93
|
-
|
82
|
+
state_machine_state_manager(state_attribute).state == state_name.to_s
|
94
83
|
end
|
95
84
|
end
|
96
85
|
end
|
@@ -103,131 +92,49 @@ module RailsStateMachine
|
|
103
92
|
end
|
104
93
|
|
105
94
|
def register_initial_state
|
106
|
-
|
107
|
-
|
95
|
+
state_attribute = @state_attribute
|
96
|
+
initial_state_name = states.detect(&:initial?)&.name
|
97
|
+
return unless initial_state_name
|
108
98
|
|
109
99
|
@model.after_initialize do
|
110
|
-
|
100
|
+
manager = state_machine_state_manager(state_attribute)
|
101
|
+
if new_record? && !manager.state
|
102
|
+
manager.state = initial_state_name
|
103
|
+
end
|
111
104
|
end
|
112
105
|
end
|
113
106
|
|
114
107
|
def define_event_methods
|
108
|
+
state_attribute = @state_attribute
|
115
109
|
event_names.each do |event_name, event|
|
116
|
-
|
117
|
-
define_method "
|
118
|
-
|
110
|
+
model_module_eval do
|
111
|
+
define_method "#{event_name}" do |**attributes|
|
112
|
+
prepare_state_event_change(attributes.merge("#{state_attribute}_event": event_name))
|
113
|
+
save
|
119
114
|
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def register_callbacks
|
125
|
-
@model.class_eval do
|
126
|
-
before_validation :run_state_event_before_validation
|
127
|
-
before_save :register_state_events_for_callbacks
|
128
|
-
before_save { flush_state_event_callbacks(:before_save) }
|
129
|
-
after_save { flush_state_event_callbacks(:after_save) }
|
130
|
-
after_save :unset_next_state_machine_event
|
131
|
-
after_commit { flush_state_event_callbacks(:after_commit) }
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
def register_validations
|
136
|
-
@model.class_eval do
|
137
|
-
after_validation :revert_state, if: -> { errors.any? }
|
138
|
-
end
|
139
|
-
end
|
140
115
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
delegate :state_machine, to: :class
|
145
|
-
end
|
146
|
-
|
147
|
-
@model.state_machine = self
|
148
|
-
end
|
149
|
-
|
150
|
-
def define_model_methods
|
151
|
-
model_methods do
|
152
|
-
def state_event=(event_name)
|
153
|
-
@next_state_machine_event = state_machine.find_event(event_name)
|
154
|
-
@state_before_state_event = source_state
|
155
|
-
|
156
|
-
# If the event can not transition from source_state, a TransitionNotFoundError will be raised
|
157
|
-
self.state = @next_state_machine_event.future_state_name(source_state).to_s
|
158
|
-
end
|
159
|
-
|
160
|
-
def state_event
|
161
|
-
@next_state_machine_event&.name
|
162
|
-
end
|
163
|
-
|
164
|
-
def source_state
|
165
|
-
if new_record?
|
166
|
-
state
|
167
|
-
else
|
168
|
-
state_in_database
|
116
|
+
define_method "#{event_name}!" do |**attributes|
|
117
|
+
prepare_state_event_change(attributes.merge("#{state_attribute}_event": event_name))
|
118
|
+
save!
|
169
119
|
end
|
170
|
-
end
|
171
|
-
|
172
|
-
private
|
173
120
|
|
174
|
-
|
175
|
-
|
176
|
-
# but call them explicitly when before_validation callbacks are triggered.
|
177
|
-
@next_state_machine_event&.run_before_validation(self)
|
178
|
-
end
|
179
|
-
|
180
|
-
def register_state_events_for_callbacks
|
181
|
-
@state_event_callbacks ||= {
|
182
|
-
before_save: [],
|
183
|
-
after_save: [],
|
184
|
-
after_commit: []
|
185
|
-
}
|
186
|
-
if @next_state_machine_event
|
187
|
-
@state_event_callbacks[:before_save] << @next_state_machine_event
|
188
|
-
@state_event_callbacks[:after_save] << @next_state_machine_event
|
189
|
-
@state_event_callbacks[:after_commit] << @next_state_machine_event
|
190
|
-
end
|
191
|
-
|
192
|
-
true
|
193
|
-
end
|
194
|
-
|
195
|
-
def flush_state_event_callbacks(name)
|
196
|
-
if @state_event_callbacks
|
197
|
-
while (event = @state_event_callbacks[name].shift)
|
198
|
-
event.public_send("run_#{name}", self)
|
199
|
-
end
|
121
|
+
define_method "may_#{event_name}?" do
|
122
|
+
state_machine_state_manager(state_attribute).transition_allowed_for?(event_name)
|
200
123
|
end
|
201
124
|
end
|
125
|
+
end
|
126
|
+
end
|
202
127
|
|
203
|
-
|
204
|
-
|
205
|
-
end
|
128
|
+
def define_model_methods
|
129
|
+
state_attribute = @state_attribute
|
206
130
|
|
207
|
-
|
208
|
-
|
131
|
+
model_module_eval do
|
132
|
+
define_method :"#{state_attribute}_event=" do |event_name|
|
133
|
+
state_machine_state_manager(state_attribute).transition_to(event_name)
|
209
134
|
end
|
210
135
|
|
211
|
-
|
212
|
-
|
213
|
-
# After calling `save`, ActiveRecord 5.1 will flag the changes that it just stored as saved.
|
214
|
-
# https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/attribute_methods/dirty.rb#L33-L46
|
215
|
-
#
|
216
|
-
# When taking multiple state events (e.g. a second event called inside an `after_save` callback) and thus
|
217
|
-
# saving after other changes were just saved, we need to mimic that behavior. Otherwise, ActiveRecord will
|
218
|
-
# print deprecation warnings like these:
|
219
|
-
#
|
220
|
-
# DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the
|
221
|
-
# next version of Rails. The new return value will reflect the behavior of calling the method after
|
222
|
-
# `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use
|
223
|
-
# `attribute_before_last_save` instead.
|
224
|
-
#
|
225
|
-
# These actually originate from ActiveRecord internals which try to determine the changes that should be
|
226
|
-
# stored for the second save. It is probably a shortcoming of ActiveRecord 5.1.x that will be fixed, but
|
227
|
-
# since the current/previous save was already successful, the right action is to just call `changes_applied`.
|
228
|
-
changes_applied
|
229
|
-
end
|
230
|
-
self.attributes = attributes
|
136
|
+
define_method :"#{state_attribute}_event" do
|
137
|
+
state_machine_state_manager(state_attribute).next_event&.name
|
231
138
|
end
|
232
139
|
end
|
233
140
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module RailsStateMachine
|
2
|
+
class StateManager
|
3
|
+
attr_accessor :next_event, :state_before_state_event
|
4
|
+
|
5
|
+
def initialize(record, state_machine, state_attribute)
|
6
|
+
@record = record
|
7
|
+
@state_machine = state_machine
|
8
|
+
@state_attribute = state_attribute
|
9
|
+
end
|
10
|
+
|
11
|
+
def state
|
12
|
+
@record.public_send(@state_attribute)
|
13
|
+
end
|
14
|
+
|
15
|
+
def state_in_database
|
16
|
+
@record.public_send(:"#{@state_attribute}_in_database").to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def state=(value)
|
20
|
+
@record.public_send(:"#{@state_attribute}=", value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def revert
|
24
|
+
self.state = @state_before_state_event if @next_event
|
25
|
+
end
|
26
|
+
|
27
|
+
def source_state
|
28
|
+
if @record.new_record?
|
29
|
+
state
|
30
|
+
else
|
31
|
+
state_in_database
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def transition_to(event_name)
|
36
|
+
@next_event = @state_machine.find_event(event_name)
|
37
|
+
@state_before_state_event = source_state
|
38
|
+
|
39
|
+
# If the event can not transition from source_state, a TransitionNotFoundError will be raised
|
40
|
+
self.state = @next_event.future_state_name(source_state).to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
def transition_allowed_for?(event_name)
|
44
|
+
@state_machine.find_event(event_name).allowed_from?(source_state)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_state_machine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Arne Hartherz
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-
|
12
|
+
date: 2019-09-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -84,6 +84,8 @@ files:
|
|
84
84
|
- Gemfile.5.1.pg.lock
|
85
85
|
- Gemfile.5.2.pg
|
86
86
|
- Gemfile.5.2.pg.lock
|
87
|
+
- Gemfile.6.0.pg
|
88
|
+
- Gemfile.6.0.pg.lock
|
87
89
|
- Gemfile.lock
|
88
90
|
- LICENSE
|
89
91
|
- LICENSE.txt
|
@@ -92,10 +94,12 @@ files:
|
|
92
94
|
- bin/console
|
93
95
|
- bin/setup
|
94
96
|
- lib/rails_state_machine.rb
|
97
|
+
- lib/rails_state_machine/callbacks.rb
|
95
98
|
- lib/rails_state_machine/event.rb
|
96
99
|
- lib/rails_state_machine/model.rb
|
97
100
|
- lib/rails_state_machine/state.rb
|
98
101
|
- lib/rails_state_machine/state_machine.rb
|
102
|
+
- lib/rails_state_machine/state_manager.rb
|
99
103
|
- lib/rails_state_machine/version.rb
|
100
104
|
- rails_state_machine.gemspec
|
101
105
|
homepage: https://github.com/makandra/rails_state_machine
|
@@ -117,8 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
121
|
- !ruby/object:Gem::Version
|
118
122
|
version: '0'
|
119
123
|
requirements: []
|
120
|
-
|
121
|
-
rubygems_version: 2.6.14.1
|
124
|
+
rubygems_version: 3.0.3
|
122
125
|
signing_key:
|
123
126
|
specification_version: 4
|
124
127
|
summary: ActiveRecord-bound state machine
|