statesman 3.5.0 → 7.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +49 -250
  3. data/.rubocop.yml +1 -1
  4. data/.rubocop_todo.yml +26 -6
  5. data/CHANGELOG.md +106 -0
  6. data/Gemfile +10 -4
  7. data/Guardfile +2 -0
  8. data/README.md +78 -48
  9. data/Rakefile +2 -4
  10. data/lib/generators/statesman/active_record_transition_generator.rb +2 -0
  11. data/lib/generators/statesman/generator_helpers.rb +2 -0
  12. data/lib/generators/statesman/migration_generator.rb +2 -0
  13. data/lib/statesman.rb +14 -4
  14. data/lib/statesman/adapters/active_record.rb +259 -37
  15. data/lib/statesman/adapters/active_record_queries.rb +100 -36
  16. data/lib/statesman/adapters/active_record_transition.rb +2 -0
  17. data/lib/statesman/adapters/memory.rb +2 -0
  18. data/lib/statesman/adapters/memory_transition.rb +2 -0
  19. data/lib/statesman/callback.rb +2 -0
  20. data/lib/statesman/config.rb +28 -0
  21. data/lib/statesman/exceptions.rb +34 -2
  22. data/lib/statesman/guard.rb +3 -4
  23. data/lib/statesman/machine.rb +29 -7
  24. data/lib/statesman/railtie.rb +2 -0
  25. data/lib/statesman/utils.rb +2 -0
  26. data/lib/statesman/version.rb +3 -1
  27. data/lib/tasks/statesman.rake +3 -1
  28. data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_with_partial_index.rb +2 -0
  29. data/spec/fixtures/add_constraints_to_most_recent_for_bacon_transitions_without_partial_index.rb +2 -0
  30. data/spec/fixtures/add_most_recent_to_bacon_transitions.rb +2 -0
  31. data/spec/generators/statesman/active_record_transition_generator_spec.rb +2 -0
  32. data/spec/generators/statesman/migration_generator_spec.rb +2 -0
  33. data/spec/spec_helper.rb +3 -30
  34. data/spec/statesman/adapters/active_record_queries_spec.rb +167 -91
  35. data/spec/statesman/adapters/active_record_spec.rb +15 -1
  36. data/spec/statesman/adapters/active_record_transition_spec.rb +2 -0
  37. data/spec/statesman/adapters/memory_spec.rb +2 -0
  38. data/spec/statesman/adapters/memory_transition_spec.rb +2 -0
  39. data/spec/statesman/adapters/shared_examples.rb +2 -0
  40. data/spec/statesman/callback_spec.rb +2 -0
  41. data/spec/statesman/config_spec.rb +2 -0
  42. data/spec/statesman/exceptions_spec.rb +88 -0
  43. data/spec/statesman/guard_spec.rb +2 -0
  44. data/spec/statesman/machine_spec.rb +79 -4
  45. data/spec/statesman/utils_spec.rb +2 -0
  46. data/spec/support/active_record.rb +9 -12
  47. data/spec/support/generators_shared_examples.rb +2 -0
  48. data/statesman.gemspec +19 -7
  49. metadata +40 -32
  50. data/lib/generators/statesman/mongoid_transition_generator.rb +0 -25
  51. data/lib/generators/statesman/templates/mongoid_transition_model.rb.erb +0 -14
  52. data/lib/statesman/adapters/mongoid.rb +0 -66
  53. data/lib/statesman/adapters/mongoid_transition.rb +0 -10
  54. data/spec/generators/statesman/mongoid_transition_generator_spec.rb +0 -23
  55. data/spec/statesman/adapters/mongoid_spec.rb +0 -86
  56. 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 { 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
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
- describe ".in_state" do
63
- context "given a single state" do
64
- subject { MyActiveRecordModel.in_state(:succeeded) }
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
- it { is_expected.to include model }
67
- it { is_expected.to_not include other_model }
63
+ MyActiveRecordModel.send(:has_one, :other_active_record_model)
64
+ OtherActiveRecordModel.send(:belongs_to, :my_active_record_model)
68
65
  end
69
66
 
70
- context "given multiple states" do
71
- subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
67
+ describe ".in_state" do
68
+ context "given a single state" do
69
+ subject { MyActiveRecordModel.in_state(:succeeded) }
72
70
 
73
- it { is_expected.to include model }
74
- it { is_expected.to include other_model }
75
- end
71
+ it { is_expected.to include model }
72
+ it { is_expected.to_not include other_model }
73
+ end
76
74
 
77
- context "given the initial state" do
78
- subject { MyActiveRecordModel.in_state(:initial) }
75
+ context "given multiple states" do
76
+ subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
79
77
 
80
- it { is_expected.to include initial_state_model }
81
- it { is_expected.to include returned_to_initial_model }
82
- end
78
+ it { is_expected.to include model }
79
+ it { is_expected.to include other_model }
80
+ end
83
81
 
84
- context "given an array of states" do
85
- subject { MyActiveRecordModel.in_state(%i[succeeded failed]) }
82
+ context "given the initial state" do
83
+ subject { MyActiveRecordModel.in_state(:initial) }
86
84
 
87
- it { is_expected.to include model }
88
- it { is_expected.to include other_model }
89
- end
85
+ it { is_expected.to include initial_state_model }
86
+ it { is_expected.to include returned_to_initial_model }
87
+ end
90
88
 
91
- context "merging two queries" do
92
- subject do
93
- MyActiveRecordModel.in_state(:succeeded).
94
- joins(:other_active_record_model).
95
- merge(OtherActiveRecordModel.in_state(:initial))
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
- it { is_expected.to be_empty }
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
- describe ".not_in_state" do
103
- context "given a single state" do
104
- subject { MyActiveRecordModel.not_in_state(:failed) }
107
+ describe ".not_in_state" do
108
+ context "given a single state" do
109
+ subject { MyActiveRecordModel.not_in_state(:failed) }
105
110
 
106
- it { is_expected.to include model }
107
- it { is_expected.to_not include other_model }
108
- end
111
+ it { is_expected.to include model }
112
+ it { is_expected.to_not include other_model }
113
+ end
109
114
 
110
- context "given multiple states" do
111
- subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
115
+ context "given multiple states" do
116
+ subject(:not_in_state) { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
112
117
 
113
- it do
114
- expect(not_in_state).to match_array([initial_state_model,
115
- returned_to_initial_model])
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
- context "given an array of states" do
120
- subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
124
+ context "given an array of states" do
125
+ subject(:not_in_state) { MyActiveRecordModel.not_in_state(%i[succeeded failed]) }
121
126
 
122
- it do
123
- expect(not_in_state).to match_array([initial_state_model,
124
- returned_to_initial_model])
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
- context "with a custom name for the transition association" do
130
- before do
131
- # Switch to using OtherActiveRecordModelTransition, so the existing
132
- # relation with MyActiveRecordModelTransition doesn't interfere with
133
- # this spec.
134
- MyActiveRecordModel.send(:has_many,
135
- :custom_name,
136
- class_name: "OtherActiveRecordModelTransition")
137
-
138
- MyActiveRecordModel.class_eval do
139
- def self.transition_class
140
- OtherActiveRecordModelTransition
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
- describe ".in_state" do
146
- subject(:query) { MyActiveRecordModel.in_state(:succeeded) }
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
- specify { expect { query }.to_not raise_error }
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.class_eval do
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
- before { MyActiveRecordModelTransition.serialize(:metadata, JSON) }
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")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
  require "json"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
  require "statesman/adapters/shared_examples"
3
5
  require "statesman/adapters/memory_transition"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
  require "statesman/adapters/memory_transition"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
 
3
5
  # All adpators must define seven methods:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
 
3
5
  describe Statesman::Callback do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "spec_helper"
2
4
 
3
5
  describe Statesman::Config do
@@ -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