statesman 3.5.0 → 6.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 +5 -5
- data/.circleci/config.yml +45 -225
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +26 -6
- data/CHANGELOG.md +69 -0
- data/Gemfile +9 -3
- data/Guardfile +2 -0
- data/README.md +77 -47
- data/Rakefile +2 -4
- data/lib/generators/statesman/active_record_transition_generator.rb +2 -0
- data/lib/generators/statesman/generator_helpers.rb +2 -0
- data/lib/generators/statesman/migration_generator.rb +2 -0
- data/lib/statesman/adapters/active_record.rb +88 -6
- data/lib/statesman/adapters/active_record_queries.rb +100 -36
- data/lib/statesman/adapters/active_record_transition.rb +2 -0
- data/lib/statesman/adapters/memory.rb +2 -0
- data/lib/statesman/adapters/memory_transition.rb +2 -0
- data/lib/statesman/callback.rb +2 -0
- data/lib/statesman/config.rb +2 -0
- data/lib/statesman/exceptions.rb +29 -2
- data/lib/statesman/guard.rb +3 -4
- data/lib/statesman/machine.rb +29 -7
- data/lib/statesman/railtie.rb +2 -0
- data/lib/statesman/utils.rb +2 -0
- data/lib/statesman/version.rb +3 -1
- data/lib/statesman.rb +2 -3
- data/lib/tasks/statesman.rake +3 -1
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_with_partial_index.rb +2 -0
- data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_without_partial_index.rb +2 -0
- data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +2 -0
- data/spec/generators/statesman/active_record_transition_generator_spec.rb +2 -0
- data/spec/generators/statesman/migration_generator_spec.rb +2 -0
- data/spec/spec_helper.rb +3 -30
- data/spec/statesman/adapters/active_record_queries_spec.rb +165 -91
- data/spec/statesman/adapters/active_record_spec.rb +4 -0
- data/spec/statesman/adapters/active_record_transition_spec.rb +2 -0
- data/spec/statesman/adapters/memory_spec.rb +2 -0
- data/spec/statesman/adapters/memory_transition_spec.rb +2 -0
- data/spec/statesman/adapters/shared_examples.rb +2 -0
- data/spec/statesman/callback_spec.rb +2 -0
- data/spec/statesman/config_spec.rb +2 -0
- data/spec/statesman/guard_spec.rb +2 -0
- data/spec/statesman/machine_spec.rb +79 -4
- data/spec/statesman/utils_spec.rb +2 -0
- data/spec/support/active_record.rb +9 -12
- data/spec/support/generators_shared_examples.rb +2 -0
- data/statesman.gemspec +5 -3
- metadata +17 -22
- data/lib/generators/statesman/mongoid_transition_generator.rb +0 -25
- data/lib/generators/statesman/templates/mongoid_transition_model.rb.erb +0 -14
- data/lib/statesman/adapters/mongoid.rb +0 -66
- data/lib/statesman/adapters/mongoid_transition.rb +0 -10
- data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -23
- data/spec/statesman/adapters/mongoid_spec.rb +0 -86
- data/spec/support/mongoid.rb +0 -28
data/lib/statesman/machine.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative "version"
|
2
4
|
require_relative "exceptions"
|
3
5
|
require_relative "guard"
|
@@ -46,10 +48,12 @@ module Statesman
|
|
46
48
|
|
47
49
|
def callbacks
|
48
50
|
@callbacks ||= {
|
49
|
-
before:
|
50
|
-
after:
|
51
|
+
before: [],
|
52
|
+
after: [],
|
53
|
+
after_transition_failure: [],
|
54
|
+
after_guard_failure: [],
|
51
55
|
after_commit: [],
|
52
|
-
guards:
|
56
|
+
guards: [],
|
53
57
|
}
|
54
58
|
end
|
55
59
|
|
@@ -83,6 +87,16 @@ module Statesman
|
|
83
87
|
from: options[:from], to: options[:to], &block)
|
84
88
|
end
|
85
89
|
|
90
|
+
def after_transition_failure(options = {}, &block)
|
91
|
+
add_callback(callback_type: :after_transition_failure, callback_class: Callback,
|
92
|
+
from: options[:from], to: options[:to], &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
def after_guard_failure(options = {}, &block)
|
96
|
+
add_callback(callback_type: :after_guard_failure, callback_class: Callback,
|
97
|
+
from: options[:from], to: options[:to], &block)
|
98
|
+
end
|
99
|
+
|
86
100
|
def validate_callback_condition(options = { from: nil, to: nil })
|
87
101
|
from = to_s_or_nil(options[:from])
|
88
102
|
to = array_to_s_or_nil(options[:to])
|
@@ -219,6 +233,17 @@ module Statesman
|
|
219
233
|
@storage_adapter.create(initial_state, new_state, metadata)
|
220
234
|
|
221
235
|
true
|
236
|
+
rescue TransitionFailedError => e
|
237
|
+
execute_on_failure(:after_transition_failure, initial_state, new_state, e)
|
238
|
+
raise
|
239
|
+
rescue GuardFailedError => e
|
240
|
+
execute_on_failure(:after_guard_failure, initial_state, new_state, e)
|
241
|
+
raise
|
242
|
+
end
|
243
|
+
|
244
|
+
def execute_on_failure(phase, initial_state, new_state, exception)
|
245
|
+
callbacks = callbacks_for(phase, from: initial_state, to: new_state)
|
246
|
+
callbacks.each { |cb| cb.call(@object, exception) }
|
222
247
|
end
|
223
248
|
|
224
249
|
def execute(phase, initial_state, new_state, transition)
|
@@ -265,10 +290,7 @@ module Statesman
|
|
265
290
|
to = to_s_or_nil(options[:to])
|
266
291
|
|
267
292
|
successors = self.class.successors[from] || []
|
268
|
-
unless successors.include?(to)
|
269
|
-
raise TransitionFailedError,
|
270
|
-
"Cannot transition from '#{from}' to '#{to}'"
|
271
|
-
end
|
293
|
+
raise TransitionFailedError.new(from, to) unless successors.include?(to)
|
272
294
|
|
273
295
|
# Call all guards, they raise exceptions if they fail
|
274
296
|
guards_for(from: from, to: to).each do |guard|
|
data/lib/statesman/railtie.rb
CHANGED
data/lib/statesman/utils.rb
CHANGED
data/lib/statesman/version.rb
CHANGED
data/lib/statesman.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Statesman
|
2
4
|
autoload :Config, "statesman/config"
|
3
5
|
autoload :Machine, "statesman/machine"
|
@@ -12,9 +14,6 @@ module Statesman
|
|
12
14
|
"statesman/adapters/active_record_transition"
|
13
15
|
autoload :ActiveRecordQueries,
|
14
16
|
"statesman/adapters/active_record_queries"
|
15
|
-
autoload :Mongoid, "statesman/adapters/mongoid"
|
16
|
-
autoload :MongoidTransition,
|
17
|
-
"statesman/adapters/mongoid_transition"
|
18
17
|
end
|
19
18
|
require "statesman/railtie" if defined?(::Rails::Railtie)
|
20
19
|
|
data/lib/tasks/statesman.rake
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :statesman do
|
2
4
|
desc "Set most_recent to false for old transitions and to true for the "\
|
3
5
|
"latest one. Safe to re-run"
|
@@ -19,7 +21,7 @@ namespace :statesman do
|
|
19
21
|
batch_size = 500
|
20
22
|
|
21
23
|
parent_class.find_in_batches(batch_size: batch_size) do |models|
|
22
|
-
ActiveRecord::Base.transaction do
|
24
|
+
ActiveRecord::Base.transaction(requires_new: true) do
|
23
25
|
if Statesman::Adapters::ActiveRecord.database_supports_partial_indexes?
|
24
26
|
# Set all transitions' most_recent to FALSE
|
25
27
|
transition_class.where(parent_fk => models.map(&:id)).
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "statesman"
|
2
4
|
require "sqlite3"
|
3
5
|
require "mysql2"
|
@@ -20,36 +22,7 @@ RSpec.configure do |config|
|
|
20
22
|
config.order = "random"
|
21
23
|
|
22
24
|
def connection_failure
|
23
|
-
if defined?(Moped)
|
24
|
-
Moped::Errors::ConnectionFailure
|
25
|
-
else
|
26
|
-
Mongo::Error::NoServerAvailable
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
if config.exclusion_filter[:mongo]
|
31
|
-
puts "Skipping Mongo tests"
|
32
|
-
else
|
33
|
-
require "mongoid"
|
34
|
-
|
35
|
-
# Try a mongo connection at the start of the suite and raise if it fails
|
36
|
-
begin
|
37
|
-
Mongoid.configure do |mongo_config|
|
38
|
-
if defined?(Moped)
|
39
|
-
mongo_config.connect_to("statesman_test")
|
40
|
-
mongo_config.sessions["default"]["options"]["max_retries"] = 2
|
41
|
-
else
|
42
|
-
mongo_config.connect_to("statesman_test", server_selection_timeout: 2)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
# Attempting a mongo operation will trigger 2 retries then throw an
|
46
|
-
# exception if mongo is not running.
|
47
|
-
Mongoid.purge!
|
48
|
-
rescue connection_failure => error
|
49
|
-
puts "The spec suite requires MongoDB to be installed and running locally"
|
50
|
-
puts "Mongo dependent specs can be filtered with rspec --tag '~mongo'"
|
51
|
-
raise(error)
|
52
|
-
end
|
25
|
+
Moped::Errors::ConnectionFailure if defined?(Moped)
|
53
26
|
end
|
54
27
|
|
55
28
|
if config.exclusion_filter[:active_record]
|
@@ -1,6 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
6
|
+
def configure_old(klass, transition_class)
|
7
|
+
klass.define_singleton_method(:transition_class) { transition_class }
|
8
|
+
klass.define_singleton_method(:initial_state) { :initial }
|
9
|
+
klass.send(:include, described_class)
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure_new(klass, transition_class)
|
13
|
+
klass.send(:include, described_class[transition_class: transition_class,
|
14
|
+
initial_state: :initial])
|
15
|
+
end
|
16
|
+
|
4
17
|
before do
|
5
18
|
prepare_model_table
|
6
19
|
prepare_transitions_table
|
@@ -8,32 +21,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
8
21
|
prepare_other_transitions_table
|
9
22
|
|
10
23
|
Statesman.configure { storage_adapter(Statesman::Adapters::ActiveRecord) }
|
11
|
-
|
12
|
-
MyActiveRecordModel.send(:include, Statesman::Adapters::ActiveRecordQueries)
|
13
|
-
MyActiveRecordModel.class_eval do
|
14
|
-
def self.transition_class
|
15
|
-
MyActiveRecordModelTransition
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.initial_state
|
19
|
-
:initial
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
OtherActiveRecordModel.send(:include,
|
24
|
-
Statesman::Adapters::ActiveRecordQueries)
|
25
|
-
OtherActiveRecordModel.class_eval do
|
26
|
-
def self.transition_class
|
27
|
-
OtherActiveRecordModelTransition
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.initial_state
|
31
|
-
:initial
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
MyActiveRecordModel.send(:has_one, :other_active_record_model)
|
36
|
-
OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
|
37
24
|
end
|
38
25
|
|
39
26
|
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
@@ -59,105 +46,164 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
59
46
|
model
|
60
47
|
end
|
61
48
|
|
62
|
-
|
63
|
-
|
64
|
-
|
49
|
+
shared_examples "testing methods" do
|
50
|
+
before do
|
51
|
+
if config_type == :old
|
52
|
+
configure_old(MyActiveRecordModel, MyActiveRecordModelTransition)
|
53
|
+
configure_old(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
54
|
+
elsif config_type == :new
|
55
|
+
configure_new(MyActiveRecordModel, MyActiveRecordModelTransition)
|
56
|
+
configure_new(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
57
|
+
else
|
58
|
+
raise "Unknown config type #{config_type}"
|
59
|
+
end
|
65
60
|
|
66
|
-
|
67
|
-
|
61
|
+
MyActiveRecordModel.send(:has_one, :other_active_record_model)
|
62
|
+
OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
|
68
63
|
end
|
69
64
|
|
70
|
-
|
71
|
-
|
65
|
+
describe ".in_state" do
|
66
|
+
context "given a single state" do
|
67
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
68
|
+
|
69
|
+
it { is_expected.to include model }
|
70
|
+
it { is_expected.to_not include other_model }
|
71
|
+
end
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
end
|
73
|
+
context "given multiple states" do
|
74
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
76
75
|
|
77
|
-
|
78
|
-
|
76
|
+
it { is_expected.to include model }
|
77
|
+
it { is_expected.to include other_model }
|
78
|
+
end
|
79
79
|
|
80
|
-
|
81
|
-
|
82
|
-
end
|
80
|
+
context "given the initial state" do
|
81
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
83
82
|
|
84
|
-
|
85
|
-
|
83
|
+
it { is_expected.to include initial_state_model }
|
84
|
+
it { is_expected.to include returned_to_initial_model }
|
85
|
+
end
|
86
86
|
|
87
|
-
|
88
|
-
|
89
|
-
end
|
87
|
+
context "given an array of states" do
|
88
|
+
subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
|
90
89
|
|
91
|
-
|
92
|
-
|
93
|
-
MyActiveRecordModel.in_state(:succeeded).
|
94
|
-
joins(:other_active_record_model).
|
95
|
-
merge(OtherActiveRecordModel.in_state(:initial))
|
90
|
+
it { is_expected.to include model }
|
91
|
+
it { is_expected.to include other_model }
|
96
92
|
end
|
97
93
|
|
98
|
-
|
94
|
+
context "merging two queries" do
|
95
|
+
subject do
|
96
|
+
MyActiveRecordModel.in_state(:succeeded).
|
97
|
+
joins(:other_active_record_model).
|
98
|
+
merge(OtherActiveRecordModel.in_state(:initial))
|
99
|
+
end
|
100
|
+
|
101
|
+
it { is_expected.to be_empty }
|
102
|
+
end
|
99
103
|
end
|
100
|
-
end
|
101
104
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
+
describe ".not_in_state" do
|
106
|
+
context "given a single state" do
|
107
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
105
108
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
+
it { is_expected.to include model }
|
110
|
+
it { is_expected.to_not include other_model }
|
111
|
+
end
|
109
112
|
|
110
|
-
|
111
|
-
|
113
|
+
context "given multiple states" do
|
114
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
112
115
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
+
it do
|
117
|
+
expect(not_in_state).to match_array([initial_state_model,
|
118
|
+
returned_to_initial_model])
|
119
|
+
end
|
116
120
|
end
|
117
|
-
end
|
118
121
|
|
119
|
-
|
120
|
-
|
122
|
+
context "given an array of states" do
|
123
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
|
121
124
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
+
it do
|
126
|
+
expect(not_in_state).to match_array([initial_state_model,
|
127
|
+
returned_to_initial_model])
|
128
|
+
end
|
125
129
|
end
|
126
130
|
end
|
127
|
-
end
|
128
131
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
132
|
+
context "with a custom name for the transition association" do
|
133
|
+
before do
|
134
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
135
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
136
|
+
# this spec.
|
137
|
+
MyActiveRecordModel.send(:has_many,
|
138
|
+
:custom_name,
|
139
|
+
class_name: "OtherActiveRecordModelTransition")
|
140
|
+
|
141
|
+
MyActiveRecordModel.class_eval do
|
142
|
+
def self.transition_class
|
143
|
+
OtherActiveRecordModelTransition
|
144
|
+
end
|
141
145
|
end
|
142
146
|
end
|
147
|
+
|
148
|
+
describe ".in_state" do
|
149
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
150
|
+
|
151
|
+
specify { expect { query }.to_not raise_error }
|
152
|
+
end
|
143
153
|
end
|
144
154
|
|
145
|
-
|
146
|
-
|
155
|
+
context "after_commit transactional integrity" do
|
156
|
+
before do
|
157
|
+
MyStateMachine.class_eval do
|
158
|
+
cattr_accessor(:after_commit_callback_executed) { false }
|
147
159
|
|
148
|
-
|
160
|
+
after_transition(from: :initial, to: :succeeded, after_commit: true) do
|
161
|
+
# This leaks state in a testable way if transactional integrity is broken.
|
162
|
+
MyStateMachine.after_commit_callback_executed = true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
after do
|
168
|
+
MyStateMachine.class_eval do
|
169
|
+
callbacks[:after_commit] = []
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
let!(:model) do
|
174
|
+
MyActiveRecordModel.create
|
175
|
+
end
|
176
|
+
|
177
|
+
# rubocop:disable RSpec/ExampleLength
|
178
|
+
it do
|
179
|
+
expect do
|
180
|
+
ActiveRecord::Base.transaction do
|
181
|
+
model.state_machine.transition_to!(:succeeded)
|
182
|
+
raise ActiveRecord::Rollback
|
183
|
+
end
|
184
|
+
end.to_not change(MyStateMachine, :after_commit_callback_executed)
|
185
|
+
end
|
186
|
+
# rubocop:enable RSpec/ExampleLength
|
149
187
|
end
|
150
188
|
end
|
151
189
|
|
190
|
+
context "using old configuration method" do
|
191
|
+
let(:config_type) { :old }
|
192
|
+
|
193
|
+
include_examples "testing methods"
|
194
|
+
end
|
195
|
+
|
196
|
+
context "using new configuration method" do
|
197
|
+
let(:config_type) { :new }
|
198
|
+
|
199
|
+
include_examples "testing methods"
|
200
|
+
end
|
201
|
+
|
152
202
|
context "with no association with the transition class" do
|
153
203
|
before do
|
154
204
|
class UnknownModelTransition < OtherActiveRecordModelTransition; end
|
155
205
|
|
156
|
-
MyActiveRecordModel
|
157
|
-
def self.transition_class
|
158
|
-
UnknownModelTransition
|
159
|
-
end
|
160
|
-
end
|
206
|
+
configure_old(MyActiveRecordModel, UnknownModelTransition)
|
161
207
|
end
|
162
208
|
|
163
209
|
describe ".in_state" do
|
@@ -168,4 +214,32 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
168
214
|
end
|
169
215
|
end
|
170
216
|
end
|
217
|
+
|
218
|
+
describe "check_missing_methods!" do
|
219
|
+
subject(:check_missing_methods!) { described_class.check_missing_methods!(base) }
|
220
|
+
|
221
|
+
context "when base has no missing methods" do
|
222
|
+
let(:base) do
|
223
|
+
Class.new do
|
224
|
+
def self.transition_class; end
|
225
|
+
|
226
|
+
def self.initial_state; end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
it "does not raise an error" do
|
231
|
+
expect { check_missing_methods! }.to_not raise_exception(NotImplementedError)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context "when base has missing methods" do
|
236
|
+
let(:base) do
|
237
|
+
Class.new
|
238
|
+
end
|
239
|
+
|
240
|
+
it "raises an error" do
|
241
|
+
expect { check_missing_methods! }.to raise_exception(NotImplementedError)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
171
245
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
require "timecop"
|
3
5
|
require "statesman/adapters/shared_examples"
|
@@ -227,6 +229,7 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
227
229
|
it "still has the old state" do
|
228
230
|
allow(observer).to receive(:execute) do |phase|
|
229
231
|
next unless phase == :before
|
232
|
+
|
230
233
|
expect(
|
231
234
|
model.transitions.where(most_recent: true).first.to_state,
|
232
235
|
).to eq("y")
|
@@ -240,6 +243,7 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
240
243
|
it "still has the old state" do
|
241
244
|
allow(observer).to receive(:execute) do |phase|
|
242
245
|
next unless phase == :after
|
246
|
+
|
243
247
|
expect(
|
244
248
|
model.transitions.where(most_recent: true).first.to_state,
|
245
249
|
).to eq("z")
|