flow_machine 0.2.2 → 0.2.3
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 +8 -2
- data/Rakefile +13 -17
- data/lib/flow_machine.rb +1 -2
- data/lib/flow_machine/callback.rb +36 -33
- data/lib/flow_machine/change_callback.rb +10 -8
- data/lib/flow_machine/state_callback.rb +5 -3
- data/lib/flow_machine/version.rb +1 -1
- data/lib/flow_machine/workflow.rb +6 -6
- data/lib/flow_machine/workflow/factory_methods.rb +2 -1
- data/lib/flow_machine/workflow_state.rb +132 -122
- data/spec/flow_machine/factory_methods_spec.rb +23 -15
- data/spec/flow_machine/multiple_workflow_spec.rb +14 -8
- data/spec/flow_machine/workflow/model_extension_spec.rb +15 -15
- data/spec/flow_machine/workflow_callback_spec.rb +38 -34
- data/spec/flow_machine/workflow_change_callback_spec.rb +13 -10
- data/spec/flow_machine/workflow_spec.rb +55 -45
- data/spec/flow_machine/workflow_state/transition_callbacks_spec.rb +66 -0
- data/spec/flow_machine/workflow_state_spec.rb +100 -58
- data/spec/spec_helper.rb +7 -7
- metadata +22 -6
@@ -0,0 +1,66 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
RSpec.describe FlowMachine::WorkflowState do
|
4
|
+
class DraftClass < described_class
|
5
|
+
def self.state_name
|
6
|
+
:draft
|
7
|
+
end
|
8
|
+
on_exit { object.published_at = :exited_draft }
|
9
|
+
event :publish do
|
10
|
+
transition to: :published
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class PublishedClass < described_class
|
15
|
+
def self.state_name
|
16
|
+
:published
|
17
|
+
end
|
18
|
+
on_enter :entering
|
19
|
+
on_exit :exiting
|
20
|
+
def entering
|
21
|
+
object.published_at = :entered_published
|
22
|
+
end
|
23
|
+
|
24
|
+
def exiting
|
25
|
+
object.published_at = :exited_published
|
26
|
+
end
|
27
|
+
event :draft do
|
28
|
+
transition to: :draft
|
29
|
+
end
|
30
|
+
event :to_self do
|
31
|
+
transition to: :published
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class WorkflowTestClass
|
36
|
+
include FlowMachine::Workflow
|
37
|
+
state DraftClass
|
38
|
+
state PublishedClass
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:workflow) { WorkflowTestClass.new(object) }
|
42
|
+
|
43
|
+
describe "when transitioning from draft to published" do
|
44
|
+
let(:object) { OpenStruct.new(state: :draft) }
|
45
|
+
|
46
|
+
it "triggers the entering of published state" do
|
47
|
+
expect { workflow.publish }.to change(object, :published_at).to be(:entered_published)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "when transitioning from published to draft" do
|
52
|
+
let(:object) { OpenStruct.new(state: :published, published_at: :something) }
|
53
|
+
|
54
|
+
it "triggers the exiting of published state" do
|
55
|
+
expect { workflow.draft }.to change(object, :published_at).from(:something).to(:exited_published)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "when transitioning from published to published" do
|
60
|
+
let(:object) { OpenStruct.new(state: :published, published_at: :something) }
|
61
|
+
|
62
|
+
it "does not trigger the on_enter or on_exit" do
|
63
|
+
expect { workflow.to_self }.not_to change(object, :published_at)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,155 +1,168 @@
|
|
1
1
|
RSpec.describe FlowMachine::WorkflowState do
|
2
2
|
class StateTestClass < described_class
|
3
|
-
def self.state_name
|
3
|
+
def self.state_name
|
4
|
+
:test
|
5
|
+
end
|
6
|
+
|
4
7
|
def guard1?; end
|
8
|
+
|
5
9
|
def guard2?; end
|
10
|
+
|
6
11
|
def after_hook; end
|
7
12
|
end
|
8
13
|
|
9
14
|
class WorkflowTestClass
|
10
15
|
include FlowMachine::Workflow
|
11
16
|
def workflow_guard?; end
|
17
|
+
|
12
18
|
def workflow_hook; end
|
13
19
|
end
|
14
20
|
|
15
21
|
let(:state_class) { Class.new(StateTestClass) }
|
16
22
|
let(:workflow_class) { Class.new WorkflowTestClass }
|
17
23
|
|
18
|
-
before
|
24
|
+
before do
|
19
25
|
workflow_class.state state_class
|
20
26
|
end
|
21
27
|
|
22
28
|
let(:object) { double state: :test, changes: {}, save: true, new_record?: true }
|
23
29
|
let(:workflow) { workflow_class.new(object) }
|
24
30
|
|
25
|
-
describe
|
26
|
-
context
|
31
|
+
describe "defining events" do
|
32
|
+
context "a basic event" do
|
33
|
+
subject(:state) { state_class.new(workflow) }
|
27
34
|
|
28
35
|
before { state_class.event :event1 }
|
29
|
-
subject(:state) { state_class.new(workflow) }
|
30
36
|
|
31
|
-
it
|
37
|
+
it "defines the event on the object" do
|
32
38
|
expect(state).to respond_to :event1
|
33
39
|
end
|
34
40
|
|
35
|
-
it
|
41
|
+
it "defines may_event1?" do
|
36
42
|
expect(state).to respond_to :may_event1?
|
37
43
|
end
|
38
44
|
|
39
|
-
it
|
45
|
+
it "defines the bang event" do
|
40
46
|
expect(state).to respond_to :event1!
|
41
47
|
end
|
42
48
|
end
|
43
49
|
end
|
44
50
|
|
45
|
-
describe
|
51
|
+
describe "invalid transitions" do
|
46
52
|
let(:state_class2) do
|
47
53
|
Class.new(described_class) do
|
48
|
-
def self.state_name
|
54
|
+
def self.state_name
|
55
|
+
:test2
|
56
|
+
end
|
49
57
|
end
|
50
58
|
end
|
51
59
|
|
52
|
-
before
|
60
|
+
before do
|
53
61
|
state_class.event(:event1) { transition to: :test2 }
|
54
62
|
workflow_class.refresh_state_methods!
|
55
63
|
workflow_class.state state_class2
|
56
64
|
end
|
57
65
|
|
58
|
-
it
|
66
|
+
it "returns false when trying to transition to the current state" do
|
59
67
|
expect(object).to receive(:state).and_return :test2
|
60
68
|
expect(workflow.event1).to be false
|
61
69
|
end
|
62
70
|
|
63
|
-
it
|
71
|
+
it "sets an invalid_event error when trying to transition to the curent state" do
|
64
72
|
expect(object).to receive(:state).and_return :test2
|
65
73
|
workflow.event1
|
66
74
|
expect(workflow.guard_errors).to eq([:invalid_event])
|
67
75
|
end
|
68
76
|
|
69
|
-
it
|
77
|
+
it "sets an invalid_event error when trying to may to the current state" do
|
70
78
|
expect(object).to receive(:state).and_return :test2
|
71
79
|
expect(workflow).not_to be_may_event1
|
72
80
|
expect(workflow.guard_errors).to eq([:invalid_event])
|
73
81
|
end
|
74
82
|
end
|
75
83
|
|
76
|
-
describe
|
84
|
+
describe "guards" do
|
77
85
|
subject(:state) { workflow.current_state }
|
78
86
|
|
79
|
-
context
|
87
|
+
context "a single guard" do
|
80
88
|
before { state_class.event(:event1, guard: :guard1?) {} }
|
81
|
-
|
89
|
+
|
90
|
+
it "calls the guard" do
|
82
91
|
expect(state).to receive(:guard1?).and_return true
|
83
92
|
state.event1
|
84
93
|
end
|
85
94
|
|
86
|
-
describe
|
87
|
-
it
|
95
|
+
describe "may?" do
|
96
|
+
it "is able to transition if the guard returns true" do
|
88
97
|
expect(state).to receive(:guard1?).and_return true
|
89
98
|
expect(state.may_event1?).to be true
|
90
99
|
end
|
91
100
|
|
92
|
-
it
|
101
|
+
it "is not able to transition if the guard returns false" do
|
93
102
|
expect(state).to receive(:guard1?).and_return false
|
94
103
|
expect(state.may_event1?).to be false
|
95
104
|
end
|
96
105
|
end
|
97
106
|
end
|
98
107
|
|
99
|
-
context
|
108
|
+
context "the guard method is on the workflow instead" do
|
100
109
|
before { state_class.event(:event1, guard: :workflow_guard?) {} }
|
101
|
-
|
110
|
+
|
111
|
+
it "calls the guard" do
|
102
112
|
expect(workflow).to receive(:workflow_guard?).and_return true
|
103
113
|
state.event1
|
104
114
|
end
|
105
115
|
end
|
106
116
|
|
107
|
-
context
|
117
|
+
context "the guard method is on the object" do
|
108
118
|
before { state_class.event(:event1, guard: :object_guard?) {} }
|
109
|
-
|
119
|
+
|
120
|
+
it "calls the guard" do
|
110
121
|
expect(object).to receive(:object_guard?).and_return true
|
111
122
|
state.event1
|
112
123
|
end
|
113
124
|
end
|
114
125
|
|
115
|
-
context
|
126
|
+
context "multiple guards" do
|
116
127
|
before do
|
117
|
-
state_class.event(:event1, guard: [
|
128
|
+
state_class.event(:event1, guard: %i[guard1? guard2?]) {}
|
118
129
|
workflow_class.refresh_state_methods!
|
119
130
|
end
|
120
|
-
|
131
|
+
|
132
|
+
it "calls all the guards" do
|
121
133
|
expect(state).to receive(:guard1?).and_return true
|
122
134
|
expect(state).to receive(:guard2?).and_return true
|
123
135
|
state.event1
|
124
136
|
end
|
125
137
|
|
126
|
-
context
|
127
|
-
before
|
138
|
+
context "one guard returns false" do
|
139
|
+
before do
|
128
140
|
expect(state).to receive(:guard1?).and_return false
|
129
141
|
expect(state).to receive(:guard2?).and_return true
|
130
142
|
end
|
131
143
|
|
132
|
-
it
|
144
|
+
it "gets the guard errors on may_event" do
|
133
145
|
workflow.may_event1?
|
134
146
|
expect(workflow.guard_errors).to eq([:guard1?])
|
135
147
|
end
|
136
148
|
|
137
|
-
it
|
149
|
+
it "gets the guard errors on transition" do
|
138
150
|
workflow.event1
|
139
151
|
expect(workflow.guard_errors).to eq([:guard1?])
|
140
152
|
end
|
141
153
|
end
|
142
154
|
end
|
143
155
|
|
144
|
-
it
|
145
|
-
state_class.event(:event1, guard: [:guard1?]) { raise
|
156
|
+
it "does not call the event block if the guard fails" do
|
157
|
+
state_class.event(:event1, guard: [:guard1?]) { raise "Should not call block" }
|
146
158
|
expect(state).to receive(:guard1?).and_return false
|
147
159
|
state.event1
|
148
160
|
end
|
149
161
|
end
|
150
162
|
|
151
|
-
describe
|
163
|
+
describe "triggering workflow after_transition hook" do
|
152
164
|
let(:state) { workflow.current_state }
|
165
|
+
|
153
166
|
before do
|
154
167
|
workflow_class.after_transition :workflow_hook
|
155
168
|
|
@@ -160,9 +173,9 @@ RSpec.describe FlowMachine::WorkflowState do
|
|
160
173
|
workflow_class.refresh_state_methods!
|
161
174
|
end
|
162
175
|
|
163
|
-
it
|
176
|
+
it "calls the hook on transition" do
|
164
177
|
# allow the transition, and make it think it's a different state
|
165
|
-
expect(workflow).to receive(:current_state_name=).with(
|
178
|
+
expect(workflow).to receive(:current_state_name=).with("state2")
|
166
179
|
expect(state).to receive(:==).and_return false
|
167
180
|
|
168
181
|
expect(state).to receive(:guard1?).and_return(true)
|
@@ -170,48 +183,77 @@ RSpec.describe FlowMachine::WorkflowState do
|
|
170
183
|
workflow.event1
|
171
184
|
end
|
172
185
|
|
173
|
-
it
|
186
|
+
it "does not call the hook on failure" do
|
174
187
|
expect(state).to receive(:guard1?).and_return(false)
|
175
188
|
expect(workflow).not_to receive(:workflow_hook)
|
176
189
|
workflow.event1
|
177
190
|
end
|
178
191
|
end
|
179
192
|
|
180
|
-
describe
|
193
|
+
describe "after transition hooks" do
|
181
194
|
let(:state) { workflow.current_state }
|
182
|
-
|
195
|
+
|
196
|
+
before do
|
183
197
|
state_class.event :event1 do
|
184
198
|
transition to: :state2, after: :after_hook
|
185
199
|
end
|
186
200
|
|
187
201
|
workflow_class.refresh_state_methods!
|
188
202
|
|
189
|
-
expect(workflow).to receive(:current_state_name=).with(
|
203
|
+
expect(workflow).to receive(:current_state_name=).with("state2")
|
190
204
|
end
|
191
205
|
|
192
|
-
it
|
193
|
-
expect(state).
|
206
|
+
it "does not call the hook before saving" do
|
207
|
+
expect(state).not_to receive(:after_hook)
|
194
208
|
workflow.event1
|
195
209
|
end
|
196
210
|
|
197
|
-
it
|
211
|
+
it "calls the hook after saving the transition" do
|
198
212
|
expect(state).to receive(:after_hook).once
|
199
213
|
workflow.event1
|
200
214
|
workflow.persist
|
201
215
|
end
|
202
216
|
end
|
203
217
|
|
204
|
-
describe
|
218
|
+
describe "on_enter" do
|
205
219
|
let(:state) { state_class.new(workflow) }
|
206
|
-
|
220
|
+
|
221
|
+
context "the method is on the workflow" do
|
222
|
+
before { state_class.on_enter :workflow_hook }
|
223
|
+
|
224
|
+
it "can call the method" do
|
225
|
+
expect(workflow).to receive(:workflow_hook)
|
226
|
+
state.fire_callbacks(:on_enter, {})
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "on_exit" do
|
232
|
+
let(:state) { state_class.new(workflow) }
|
233
|
+
|
234
|
+
context "the method is on the workflow" do
|
235
|
+
before { state_class.on_exit :workflow_hook }
|
236
|
+
|
237
|
+
it "can call the method" do
|
238
|
+
expect(workflow).to receive(:workflow_hook)
|
239
|
+
state.fire_callbacks(:on_exit, {})
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
describe "after_enter" do
|
245
|
+
let(:state) { state_class.new(workflow) }
|
246
|
+
|
247
|
+
context "the method is on the workflow" do
|
207
248
|
before { state_class.after_enter :workflow_hook }
|
249
|
+
|
208
250
|
it "can call the method" do
|
209
251
|
expect(workflow).to receive(:workflow_hook)
|
210
252
|
state.fire_callbacks(:after_enter, {})
|
211
253
|
end
|
212
254
|
end
|
213
255
|
|
214
|
-
context
|
256
|
+
context "the method is on the object" do
|
215
257
|
before { state_class.after_enter :object_hook }
|
216
258
|
|
217
259
|
it "can call the method" do
|
@@ -221,41 +263,41 @@ RSpec.describe FlowMachine::WorkflowState do
|
|
221
263
|
end
|
222
264
|
end
|
223
265
|
|
224
|
-
describe
|
266
|
+
describe "#run_workflow_method" do
|
225
267
|
let(:state) { state_class.new(workflow) }
|
226
268
|
|
227
|
-
context
|
228
|
-
it
|
269
|
+
context "nothing in the chain has the method" do
|
270
|
+
it "raises a NoMethodError" do
|
229
271
|
expect { state.run_workflow_method :some_method }.to raise_error(NoMethodError)
|
230
272
|
end
|
231
273
|
end
|
232
274
|
|
233
|
-
context
|
234
|
-
before
|
275
|
+
context "the object defines the method" do
|
276
|
+
before do
|
235
277
|
allow(object).to receive(:some_method)
|
236
278
|
end
|
237
279
|
|
238
|
-
it
|
280
|
+
it "calls the method on object" do
|
239
281
|
expect(object).to receive(:some_method)
|
240
282
|
state.run_workflow_method :some_method
|
241
283
|
end
|
242
284
|
|
243
|
-
context
|
244
|
-
before
|
285
|
+
context "and the workflow defines the method" do
|
286
|
+
before do
|
245
287
|
workflow.singleton_class.send(:define_method, :some_method) {}
|
246
288
|
end
|
247
289
|
|
248
|
-
it
|
290
|
+
it "calls the method on workflow" do
|
249
291
|
expect(workflow).to receive(:some_method)
|
250
292
|
state.run_workflow_method :some_method
|
251
293
|
end
|
252
294
|
|
253
|
-
context
|
254
|
-
before
|
295
|
+
context "and the state defines the method" do
|
296
|
+
before do
|
255
297
|
state.singleton_class.send(:define_method, :some_method) {}
|
256
298
|
end
|
257
299
|
|
258
|
-
it
|
300
|
+
it "calls the method on the state" do
|
259
301
|
expect(state).to receive(:some_method)
|
260
302
|
state.run_workflow_method :some_method
|
261
303
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
GEM_ROOT = File.expand_path(
|
2
|
-
$LOAD_PATH.unshift File.join(GEM_ROOT,
|
1
|
+
GEM_ROOT = File.expand_path("..", __dir__)
|
2
|
+
$LOAD_PATH.unshift File.join(GEM_ROOT, "lib")
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require "rspec/its"
|
5
|
+
require "flow_machine"
|
6
6
|
|
7
7
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
8
8
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
@@ -44,8 +44,8 @@ RSpec.configure do |config|
|
|
44
44
|
mocks.verify_partial_doubles = true
|
45
45
|
end
|
46
46
|
|
47
|
-
# The settings below are suggested to provide a good initial experience
|
48
|
-
# with RSpec, but feel free to customize to your heart's content.
|
47
|
+
# The settings below are suggested to provide a good initial experience
|
48
|
+
# with RSpec, but feel free to customize to your heart's content.
|
49
49
|
|
50
50
|
# These two settings work together to allow you to limit a spec run
|
51
51
|
# to individual examples or groups you care about by tagging them with
|
@@ -72,7 +72,7 @@ RSpec.configure do |config|
|
|
72
72
|
# Use the documentation formatter for detailed output,
|
73
73
|
# unless a formatter has already been configured
|
74
74
|
# (e.g. via a command-line flag).
|
75
|
-
config.default_formatter =
|
75
|
+
config.default_formatter = "doc"
|
76
76
|
end
|
77
77
|
|
78
78
|
# Print the 10 slowest examples and example groups at the
|