statesman 3.5.0 → 7.4.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 +49 -250
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +26 -6
- data/CHANGELOG.md +106 -0
- data/Gemfile +10 -4
- data/Guardfile +2 -0
- data/README.md +78 -48
- 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.rb +14 -4
- data/lib/statesman/adapters/active_record.rb +259 -37
- 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 +28 -0
- data/lib/statesman/exceptions.rb +34 -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/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 +167 -91
- data/spec/statesman/adapters/active_record_spec.rb +15 -1
- 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/exceptions_spec.rb +88 -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 +19 -7
- metadata +40 -32
- 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
@@ -1,39 +1,28 @@
|
|
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
|
7
20
|
prepare_other_model_table
|
8
21
|
prepare_other_transitions_table
|
9
22
|
|
10
|
-
Statesman.configure
|
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
|
23
|
+
Statesman.configure do
|
24
|
+
storage_adapter(Statesman::Adapters::ActiveRecord)
|
21
25
|
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
26
|
end
|
38
27
|
|
39
28
|
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
@@ -59,105 +48,164 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
59
48
|
model
|
60
49
|
end
|
61
50
|
|
62
|
-
|
63
|
-
|
64
|
-
|
51
|
+
shared_examples "testing methods" do
|
52
|
+
before do
|
53
|
+
if config_type == :old
|
54
|
+
configure_old(MyActiveRecordModel, MyActiveRecordModelTransition)
|
55
|
+
configure_old(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
56
|
+
elsif config_type == :new
|
57
|
+
configure_new(MyActiveRecordModel, MyActiveRecordModelTransition)
|
58
|
+
configure_new(OtherActiveRecordModel, OtherActiveRecordModelTransition)
|
59
|
+
else
|
60
|
+
raise "Unknown config type #{config_type}"
|
61
|
+
end
|
65
62
|
|
66
|
-
|
67
|
-
|
63
|
+
MyActiveRecordModel.send(:has_one, :other_active_record_model)
|
64
|
+
OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
|
68
65
|
end
|
69
66
|
|
70
|
-
|
71
|
-
|
67
|
+
describe ".in_state" do
|
68
|
+
context "given a single state" do
|
69
|
+
subject { MyActiveRecordModel.in_state(:succeeded) }
|
72
70
|
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
it { is_expected.to include model }
|
72
|
+
it { is_expected.to_not include other_model }
|
73
|
+
end
|
76
74
|
|
77
|
-
|
78
|
-
|
75
|
+
context "given multiple states" do
|
76
|
+
subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
|
79
77
|
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
it { is_expected.to include model }
|
79
|
+
it { is_expected.to include other_model }
|
80
|
+
end
|
83
81
|
|
84
|
-
|
85
|
-
|
82
|
+
context "given the initial state" do
|
83
|
+
subject { MyActiveRecordModel.in_state(:initial) }
|
86
84
|
|
87
|
-
|
88
|
-
|
89
|
-
|
85
|
+
it { is_expected.to include initial_state_model }
|
86
|
+
it { is_expected.to include returned_to_initial_model }
|
87
|
+
end
|
90
88
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
89
|
+
context "given an array of states" do
|
90
|
+
subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
|
91
|
+
|
92
|
+
it { is_expected.to include model }
|
93
|
+
it { is_expected.to include other_model }
|
96
94
|
end
|
97
95
|
|
98
|
-
|
96
|
+
context "merging two queries" do
|
97
|
+
subject do
|
98
|
+
MyActiveRecordModel.in_state(:succeeded).
|
99
|
+
joins(:other_active_record_model).
|
100
|
+
merge(OtherActiveRecordModel.in_state(:initial))
|
101
|
+
end
|
102
|
+
|
103
|
+
it { is_expected.to be_empty }
|
104
|
+
end
|
99
105
|
end
|
100
|
-
end
|
101
106
|
|
102
|
-
|
103
|
-
|
104
|
-
|
107
|
+
describe ".not_in_state" do
|
108
|
+
context "given a single state" do
|
109
|
+
subject { MyActiveRecordModel.not_in_state(:failed) }
|
105
110
|
|
106
|
-
|
107
|
-
|
108
|
-
|
111
|
+
it { is_expected.to include model }
|
112
|
+
it { is_expected.to_not include other_model }
|
113
|
+
end
|
109
114
|
|
110
|
-
|
111
|
-
|
115
|
+
context "given multiple states" do
|
116
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
|
112
117
|
|
113
|
-
|
114
|
-
|
115
|
-
|
118
|
+
it do
|
119
|
+
expect(not_in_state).to match_array([initial_state_model,
|
120
|
+
returned_to_initial_model])
|
121
|
+
end
|
116
122
|
end
|
117
|
-
end
|
118
123
|
|
119
|
-
|
120
|
-
|
124
|
+
context "given an array of states" do
|
125
|
+
subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
|
121
126
|
|
122
|
-
|
123
|
-
|
124
|
-
|
127
|
+
it do
|
128
|
+
expect(not_in_state).to match_array([initial_state_model,
|
129
|
+
returned_to_initial_model])
|
130
|
+
end
|
125
131
|
end
|
126
132
|
end
|
127
|
-
end
|
128
133
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
134
|
+
context "with a custom name for the transition association" do
|
135
|
+
before do
|
136
|
+
# Switch to using OtherActiveRecordModelTransition, so the existing
|
137
|
+
# relation with MyActiveRecordModelTransition doesn't interfere with
|
138
|
+
# this spec.
|
139
|
+
MyActiveRecordModel.send(:has_many,
|
140
|
+
:custom_name,
|
141
|
+
class_name: "OtherActiveRecordModelTransition")
|
142
|
+
|
143
|
+
MyActiveRecordModel.class_eval do
|
144
|
+
def self.transition_class
|
145
|
+
OtherActiveRecordModelTransition
|
146
|
+
end
|
141
147
|
end
|
142
148
|
end
|
149
|
+
|
150
|
+
describe ".in_state" do
|
151
|
+
subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
|
152
|
+
|
153
|
+
specify { expect { query }.to_not raise_error }
|
154
|
+
end
|
143
155
|
end
|
144
156
|
|
145
|
-
|
146
|
-
|
157
|
+
context "after_commit transactional integrity" do
|
158
|
+
before do
|
159
|
+
MyStateMachine.class_eval do
|
160
|
+
cattr_accessor(:after_commit_callback_executed) { false }
|
147
161
|
|
148
|
-
|
162
|
+
after_transition(from: :initial, to: :succeeded, after_commit: true) do
|
163
|
+
# This leaks state in a testable way if transactional integrity is broken.
|
164
|
+
MyStateMachine.after_commit_callback_executed = true
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
after do
|
170
|
+
MyStateMachine.class_eval do
|
171
|
+
callbacks[:after_commit] = []
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
let!(:model) do
|
176
|
+
MyActiveRecordModel.create
|
177
|
+
end
|
178
|
+
|
179
|
+
# rubocop:disable RSpec/ExampleLength
|
180
|
+
it do
|
181
|
+
expect do
|
182
|
+
ActiveRecord::Base.transaction do
|
183
|
+
model.state_machine.transition_to!(:succeeded)
|
184
|
+
raise ActiveRecord::Rollback
|
185
|
+
end
|
186
|
+
end.to_not change(MyStateMachine, :after_commit_callback_executed)
|
187
|
+
end
|
188
|
+
# rubocop:enable RSpec/ExampleLength
|
149
189
|
end
|
150
190
|
end
|
151
191
|
|
192
|
+
context "using old configuration method" do
|
193
|
+
let(:config_type) { :old }
|
194
|
+
|
195
|
+
include_examples "testing methods"
|
196
|
+
end
|
197
|
+
|
198
|
+
context "using new configuration method" do
|
199
|
+
let(:config_type) { :new }
|
200
|
+
|
201
|
+
include_examples "testing methods"
|
202
|
+
end
|
203
|
+
|
152
204
|
context "with no association with the transition class" do
|
153
205
|
before do
|
154
206
|
class UnknownModelTransition < OtherActiveRecordModelTransition; end
|
155
207
|
|
156
|
-
MyActiveRecordModel
|
157
|
-
def self.transition_class
|
158
|
-
UnknownModelTransition
|
159
|
-
end
|
160
|
-
end
|
208
|
+
configure_old(MyActiveRecordModel, UnknownModelTransition)
|
161
209
|
end
|
162
210
|
|
163
211
|
describe ".in_state" do
|
@@ -168,4 +216,32 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
|
|
168
216
|
end
|
169
217
|
end
|
170
218
|
end
|
219
|
+
|
220
|
+
describe "check_missing_methods!" do
|
221
|
+
subject(:check_missing_methods!) { described_class.check_missing_methods!(base) }
|
222
|
+
|
223
|
+
context "when base has no missing methods" do
|
224
|
+
let(:base) do
|
225
|
+
Class.new do
|
226
|
+
def self.transition_class; end
|
227
|
+
|
228
|
+
def self.initial_state; end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
it "does not raise an error" do
|
233
|
+
expect { check_missing_methods! }.to_not raise_exception(NotImplementedError)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
context "when base has missing methods" do
|
238
|
+
let(:base) do
|
239
|
+
Class.new
|
240
|
+
end
|
241
|
+
|
242
|
+
it "raises an error" do
|
243
|
+
expect { check_missing_methods! }.to raise_exception(NotImplementedError)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
171
247
|
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"
|
@@ -7,9 +9,19 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
7
9
|
before do
|
8
10
|
prepare_model_table
|
9
11
|
prepare_transitions_table
|
12
|
+
|
13
|
+
MyActiveRecordModelTransition.serialize(:metadata, JSON)
|
14
|
+
|
15
|
+
Statesman.configure do
|
16
|
+
# Rubocop requires described_class to be used, but this block
|
17
|
+
# is instance_eval'd and described_class won't be defined
|
18
|
+
# rubocop:disable RSpec/DescribedClass
|
19
|
+
storage_adapter(Statesman::Adapters::ActiveRecord)
|
20
|
+
# rubocop:enable RSpec/DescribedClass
|
21
|
+
end
|
10
22
|
end
|
11
23
|
|
12
|
-
|
24
|
+
after { Statesman.configure { storage_adapter(Statesman::Adapters::Memory) } }
|
13
25
|
|
14
26
|
let(:observer) { double(Statesman::Machine, execute: nil) }
|
15
27
|
let(:model) { MyActiveRecordModel.create(current_state: :pending) }
|
@@ -227,6 +239,7 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
227
239
|
it "still has the old state" do
|
228
240
|
allow(observer).to receive(:execute) do |phase|
|
229
241
|
next unless phase == :before
|
242
|
+
|
230
243
|
expect(
|
231
244
|
model.transitions.where(most_recent: true).first.to_state,
|
232
245
|
).to eq("y")
|
@@ -240,6 +253,7 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
|
|
240
253
|
it "still has the old state" do
|
241
254
|
allow(observer).to receive(:execute) do |phase|
|
242
255
|
next unless phase == :after
|
256
|
+
|
243
257
|
expect(
|
244
258
|
model.transitions.where(most_recent: true).first.to_state,
|
245
259
|
).to eq("z")
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe Statesman do
|
6
|
+
describe "InvalidStateError" do
|
7
|
+
subject(:error) { Statesman::InvalidStateError.new }
|
8
|
+
|
9
|
+
its(:message) { is_expected.to eq("Statesman::InvalidStateError") }
|
10
|
+
its "string matches its message" do
|
11
|
+
expect(error.to_s).to eq(error.message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "InvalidTransitionError" do
|
16
|
+
subject(:error) { Statesman::InvalidTransitionError.new }
|
17
|
+
|
18
|
+
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
19
|
+
its "string matches its message" do
|
20
|
+
expect(error.to_s).to eq(error.message)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "InvalidCallbackError" do
|
25
|
+
subject(:error) { Statesman::InvalidTransitionError.new }
|
26
|
+
|
27
|
+
its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
|
28
|
+
its "string matches its message" do
|
29
|
+
expect(error.to_s).to eq(error.message)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "TransitionConflictError" do
|
34
|
+
subject(:error) { Statesman::TransitionConflictError.new }
|
35
|
+
|
36
|
+
its(:message) { is_expected.to eq("Statesman::TransitionConflictError") }
|
37
|
+
its "string matches its message" do
|
38
|
+
expect(error.to_s).to eq(error.message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "MissingTransitionAssociation" do
|
43
|
+
subject(:error) { Statesman::MissingTransitionAssociation.new }
|
44
|
+
|
45
|
+
its(:message) { is_expected.to eq("Statesman::MissingTransitionAssociation") }
|
46
|
+
its "string matches its message" do
|
47
|
+
expect(error.to_s).to eq(error.message)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "TransitionFailedError" do
|
52
|
+
subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
|
53
|
+
|
54
|
+
its(:message) { is_expected.to eq("Cannot transition from 'from' to 'to'") }
|
55
|
+
its "string matches its message" do
|
56
|
+
expect(error.to_s).to eq(error.message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "GuardFailedError" do
|
61
|
+
subject(:error) { Statesman::GuardFailedError.new("from", "to") }
|
62
|
+
|
63
|
+
its(:message) do
|
64
|
+
is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
|
65
|
+
end
|
66
|
+
its "string matches its message" do
|
67
|
+
expect(error.to_s).to eq(error.message)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "UnserializedMetadataError" do
|
72
|
+
subject(:error) { Statesman::UnserializedMetadataError.new("foo") }
|
73
|
+
|
74
|
+
its(:message) { is_expected.to match(/foo#metadata is not serialized/) }
|
75
|
+
its "string matches its message" do
|
76
|
+
expect(error.to_s).to eq(error.message)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "IncompatibleSerializationError" do
|
81
|
+
subject(:error) { Statesman::IncompatibleSerializationError.new("foo") }
|
82
|
+
|
83
|
+
its(:message) { is_expected.to match(/foo#metadata column type cannot be json/) }
|
84
|
+
its "string matches its message" do
|
85
|
+
expect(error.to_s).to eq(error.message)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|