statesman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ require "spec_helper"
2
+ require "statesman/adapters/shared_examples"
3
+
4
+ describe Statesman::Adapters::Memory do
5
+ let(:model) { Class.new { attr_accessor :current_state }.new }
6
+ it_behaves_like "an adapter", described_class, Statesman::Transition
7
+ end
@@ -0,0 +1,111 @@
1
+ require "spec_helper"
2
+
3
+ # All adpators must define seven methods:
4
+ # initialize: Accepts a transition class, parent model and state_attr.
5
+ # transition_class: Returns the transition class object passed to initialize.
6
+ # parent_model: Returns the model class object passed to initialize.
7
+ # state_attr: Returns the state attribute to set on the parent.
8
+ # create: Accepts to_state, before callbacks, after callbacks and
9
+ # optional metadata. Creates a new transition class
10
+ # instance and saves metadata to it.
11
+ # history: Returns the full transition history
12
+ # last: Returns the latest transition history item
13
+ #
14
+ shared_examples_for "an adapter" do |adapter_class, transition_class|
15
+ let(:adapter) { adapter_class.new(transition_class, model) }
16
+ let(:before_cbs) { [] }
17
+ let(:after_cbs) { [] }
18
+
19
+ describe "#initialize" do
20
+ subject { adapter }
21
+ its(:transition_class) { should be(transition_class) }
22
+ its(:parent_model) { should be(model) }
23
+ its(:history) { should eq([]) }
24
+ end
25
+
26
+ describe "#create" do
27
+ let(:to) { :y }
28
+ let(:create) { adapter.create(to, before_cbs, after_cbs) }
29
+ subject { -> { create } }
30
+
31
+ it { should change(adapter.history, :count).by(1) }
32
+
33
+ context "the new transition" do
34
+ subject { create }
35
+ it { should be_a(transition_class) }
36
+ its(:to_state) { should be(to) }
37
+
38
+ context "with no previous transition" do
39
+ its(:sort_key) { should be(0) }
40
+ end
41
+
42
+ context "with a previous transition" do
43
+ before { adapter.create(:x, before_cbs, after_cbs) }
44
+ its(:sort_key) { should be(10) }
45
+ end
46
+ end
47
+
48
+ context "with before callbacks" do
49
+ let(:callback) { double(call: true) }
50
+ let(:before_cbs) { [callback] }
51
+
52
+ it "is called before the state transition" do
53
+ callback.should_receive(:call).with do |passed_model, transition|
54
+ expect(passed_model).to be(model)
55
+ expect(transition).to be_a(adapter.transition_class)
56
+ expect(adapter.history.length).to eq(0)
57
+ end.once
58
+
59
+ adapter.create(:x, before_cbs, after_cbs)
60
+ expect(adapter.history.length).to eq(1)
61
+ end
62
+ end
63
+
64
+ context "with after callbacks" do
65
+ let(:callback) { double(call: true) }
66
+ let(:after_cbs) { [callback] }
67
+
68
+ it "is called after the state transition" do
69
+ callback.should_receive(:call).with do |passed_model, transition|
70
+ expect(passed_model).to be(model)
71
+ expect(adapter.last).to eq(transition)
72
+ end.once
73
+ adapter.create(:x, before_cbs, after_cbs)
74
+ end
75
+ end
76
+
77
+ context "with metadata" do
78
+ let(:metadata) { { "some" => "hash" } }
79
+ subject { adapter.create(to, before_cbs, after_cbs, metadata) }
80
+ its(:metadata) { should eq(metadata) }
81
+ end
82
+ end
83
+
84
+ describe "#history" do
85
+ subject { adapter.history }
86
+ it { should eq([]) }
87
+
88
+ context "with transitions" do
89
+ let!(:transition) { adapter.create(:y, before_cbs, after_cbs) }
90
+ it { should eq([transition]) }
91
+
92
+ context "sorting" do
93
+ let!(:transition) { adapter.create(:y, before_cbs, after_cbs) }
94
+ let!(:transition2) { adapter.create(:y, before_cbs, after_cbs) }
95
+ subject { adapter.history }
96
+ it { should eq(adapter.history.sort_by(&:sort_key)) }
97
+ end
98
+ end
99
+ end
100
+
101
+ describe "#last" do
102
+ before do
103
+ adapter.create(:y, before_cbs, after_cbs)
104
+ adapter.create(:z, before_cbs, after_cbs)
105
+ end
106
+ subject { adapter.last }
107
+
108
+ it { should be_a(transition_class) }
109
+ specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
110
+ end
111
+ end
@@ -0,0 +1,119 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesman::Callback do
4
+ let(:cb_lambda) { -> { } }
5
+ let(:callback) do
6
+ Statesman::Callback.new(from: nil, to: nil, callback: cb_lambda)
7
+ end
8
+
9
+ describe "#initialize" do
10
+ context "with no callback" do
11
+ let(:cb_lambda) { nil }
12
+
13
+ it "raises an error" do
14
+ expect { callback }.to raise_error(Statesman::InvalidCallbackError)
15
+ end
16
+ end
17
+ end
18
+
19
+ describe "#call" do
20
+ let(:spy) { double.as_null_object }
21
+ let(:cb_lambda) { -> { spy.call } }
22
+
23
+ it "delegates to callback" do
24
+ callback.call
25
+ expect(spy).to have_received(:call)
26
+ end
27
+ end
28
+
29
+ describe "#applies_to" do
30
+ let(:callback) do
31
+ Statesman::Callback.new(from: :x, to: :y, callback: cb_lambda)
32
+ end
33
+ subject { callback.applies_to?(from: from, to: to) }
34
+
35
+ context "with any from value" do
36
+ let(:from) { nil }
37
+
38
+ context "and an allowed to value" do
39
+ let(:to) { :y }
40
+ it { should be_true }
41
+ end
42
+
43
+ context "and a disallowed to value" do
44
+ let(:to) { :a }
45
+ it { should be_false }
46
+ end
47
+ end
48
+
49
+ context "with any to value" do
50
+ let(:to) { nil }
51
+
52
+ context "and an allowed 'from' value" do
53
+ let(:from) { :x }
54
+ it { should be_true }
55
+ end
56
+
57
+ context "and a disallowed 'from' value" do
58
+ let(:from) { :a }
59
+ it { should be_false }
60
+ end
61
+ end
62
+
63
+ context "with any to and any from value on the callback" do
64
+ let(:callback) do
65
+ Statesman::Callback.new(callback: cb_lambda)
66
+ end
67
+ let(:from) { :x }
68
+ let(:to) { :y }
69
+
70
+ it { should be_true }
71
+ end
72
+
73
+ context "with any from value on the callback" do
74
+ let(:callback) do
75
+ Statesman::Callback.new(to: :y, callback: cb_lambda)
76
+ end
77
+ let(:from) { :x }
78
+
79
+ context "and an allowed to value" do
80
+ let(:to) { :y }
81
+ it { should be_true }
82
+ end
83
+
84
+ context "and a disallowed to value" do
85
+ let(:to) { :a }
86
+ it { should be_false }
87
+ end
88
+ end
89
+
90
+ context "with any to value on the callback" do
91
+ let(:callback) do
92
+ Statesman::Callback.new(from: :x, callback: cb_lambda)
93
+ end
94
+ let(:to) { :y }
95
+
96
+ context "and an allowed to value" do
97
+ let(:from) { :x }
98
+ it { should be_true }
99
+ end
100
+
101
+ context "and a disallowed to value" do
102
+ let(:from) { :a }
103
+ it { should be_false }
104
+ end
105
+ end
106
+
107
+ context "with allowed 'from' and 'to' values" do
108
+ let(:from) { :x }
109
+ let(:to) { :y }
110
+ it { should be_true }
111
+ end
112
+
113
+ context "with disallowed 'from' and 'to' values" do
114
+ let(:from) { :a }
115
+ let(:to) { :b }
116
+ it { should be_false }
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,34 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesman::Config do
4
+ let(:instance) { Statesman::Config.new }
5
+
6
+ after do
7
+ # Don't leak global config changes into other specs
8
+ Statesman.configure { storage_adapter(Statesman::Adapters::Memory) }
9
+ end
10
+
11
+ describe "#storage_adapter" do
12
+ let(:adapter) { Class.new }
13
+ before { instance.storage_adapter(adapter) }
14
+ subject { instance.adapter_class }
15
+ it { should be(adapter) }
16
+
17
+ it "is DSL configurable" do
18
+ new_adapter = adapter
19
+ Statesman.configure { storage_adapter(new_adapter) }
20
+
21
+ defined_adapter = nil
22
+ Statesman.instance_eval { defined_adapter = @storage_adapter }
23
+ expect(defined_adapter).to be(new_adapter)
24
+ end
25
+ end
26
+
27
+ describe "#transition_class" do
28
+ it "serializes metadata to JSON" do
29
+ klass = Class.new
30
+ klass.should_receive(:serialize).once.with(:metadata, JSON)
31
+ Statesman.configure { transition_class(klass) }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesman::Guard do
4
+ let(:callback) { -> { } }
5
+ let(:guard) { Statesman::Guard.new(from: nil, to: nil, callback: callback) }
6
+
7
+ specify { expect(guard).to be_a(Statesman::Callback) }
8
+
9
+ describe "#call" do
10
+ subject { guard.call }
11
+
12
+ context "success" do
13
+ let(:callback) { -> { true } }
14
+
15
+ it "does not raise an error" do
16
+ expect { guard.call }.to_not raise_error
17
+ end
18
+ end
19
+
20
+ context "error" do
21
+ let(:callback) { -> { false } }
22
+
23
+ it "raises an error" do
24
+ expect { guard.call }.to raise_error(Statesman::GuardFailedError)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,459 @@
1
+ require "spec_helper"
2
+
3
+ describe Statesman::Machine do
4
+ let(:machine) { Class.new { include Statesman::Machine } }
5
+ let(:my_model) { Class.new { attr_accessor :current_state }.new }
6
+
7
+ describe ".state" do
8
+ before { machine.state(:x) }
9
+ before { machine.state(:y) }
10
+ specify { expect(machine.states).to eq(%w(x y)) }
11
+
12
+ context "initial" do
13
+ before { machine.state(:x, initial: true) }
14
+ specify { expect(machine.initial_state).to eq("x") }
15
+
16
+ context "when an initial state is already defined" do
17
+ it "raises an error" do
18
+ expect do
19
+ machine.state(:y, initial: true)
20
+ end.to raise_error(Statesman::InvalidStateError)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ describe ".transition" do
27
+ before do
28
+ machine.class_eval do
29
+ state :x
30
+ state :y
31
+ state :z
32
+ end
33
+ end
34
+
35
+ context "given neither a 'from' nor a 'to' state" do
36
+ it "raises an error" do
37
+ expect do
38
+ machine.transition
39
+ end.to raise_error(Statesman::InvalidStateError)
40
+ end
41
+ end
42
+
43
+ context "given an invalid 'from' state" do
44
+ it "raises an error" do
45
+ expect do
46
+ machine.transition(from: :a, to: :x)
47
+ end.to raise_error(Statesman::InvalidStateError)
48
+ end
49
+ end
50
+
51
+ context "given an invalid 'to' state" do
52
+ it "raises an error" do
53
+ expect do
54
+ machine.transition(from: :x, to: :a)
55
+ end.to raise_error(Statesman::InvalidStateError)
56
+ end
57
+ end
58
+
59
+ context "valid 'from' and 'to' states" do
60
+ it "records the transition" do
61
+ machine.transition(from: :x, to: :y)
62
+ machine.transition(from: :x, to: :z)
63
+ expect(machine.successors).to eq("x" => %w(y z))
64
+ end
65
+ end
66
+ end
67
+
68
+ describe ".validate_callback_condition" do
69
+ before do
70
+ machine.class_eval do
71
+ state :x
72
+ state :y
73
+ state :z
74
+ transition from: :x, to: :y
75
+ transition from: :y, to: :z
76
+ end
77
+ end
78
+
79
+ context "with a terminal 'from' state" do
80
+ it "raises an exception" do
81
+ expect do
82
+ machine.validate_callback_condition(from: :z, to: :y)
83
+ end.to raise_error(Statesman::InvalidTransitionError)
84
+ end
85
+ end
86
+
87
+ context "with an initial 'to' state" do
88
+ it "raises an exception" do
89
+ expect do
90
+ machine.validate_callback_condition(from: :y, to: :x)
91
+ end.to raise_error(Statesman::InvalidTransitionError)
92
+ end
93
+ end
94
+
95
+ context "with an invalid transition" do
96
+ it "raises an exception" do
97
+ expect do
98
+ machine.validate_callback_condition(from: :x, to: :z)
99
+ end.to raise_error(Statesman::InvalidTransitionError)
100
+ end
101
+ end
102
+
103
+ context "with any states" do
104
+ it "does not raise an exception" do
105
+ expect { machine.validate_callback_condition }.to_not raise_error
106
+ end
107
+ end
108
+
109
+ context "with a valid transition" do
110
+ it "does not raise an exception" do
111
+ expect do
112
+ machine.validate_callback_condition(from: :x, to: :y)
113
+ end.to_not raise_error
114
+ end
115
+ end
116
+ end
117
+
118
+ shared_examples "a callback store" do |assignment_method, callback_store|
119
+ before do
120
+ machine.class_eval do
121
+ state :x, initial: true
122
+ state :y
123
+ transition from: :x, to: :y
124
+ end
125
+ end
126
+
127
+ it "stores callbacks" do
128
+ expect do
129
+ machine.send(assignment_method) {}
130
+ end.to change(machine.send(callback_store), :count).by(1)
131
+ end
132
+
133
+ it "stores callback instances" do
134
+ machine.send(assignment_method) {}
135
+
136
+ machine.send(callback_store).each do |callback|
137
+ expect(callback).to be_a(Statesman::Callback)
138
+ end
139
+ end
140
+
141
+ context "with invalid states" do
142
+ it "raises an exception when both are invalid" do
143
+ expect do
144
+ machine.send(assignment_method, from: :foo, to: :bar) {}
145
+ end.to raise_error(Statesman::InvalidStateError)
146
+ end
147
+
148
+ it "raises an exception with a terminal from state and nil to state" do
149
+ expect do
150
+ machine.send(assignment_method, from: :y) {}
151
+ end.to raise_error(Statesman::InvalidTransitionError)
152
+ end
153
+
154
+ it "raises an exception with an initial to state and nil from state" do
155
+ expect do
156
+ machine.send(assignment_method, to: :x) {}
157
+ end.to raise_error(Statesman::InvalidTransitionError)
158
+ end
159
+ end
160
+
161
+ context "with validate_states" do
162
+ it "allows a nil from state" do
163
+ expect do
164
+ machine.send(assignment_method, to: :y) {}
165
+ end.to_not raise_error
166
+ end
167
+
168
+ it "allows a nil to state" do
169
+ expect do
170
+ machine.send(assignment_method, from: :x) {}
171
+ end.to_not raise_error
172
+ end
173
+ end
174
+ end
175
+
176
+ describe ".before_transition" do
177
+ it_behaves_like "a callback store", :before_transition, :before_callbacks
178
+ end
179
+
180
+ describe ".after_transition" do
181
+ it_behaves_like "a callback store", :after_transition, :after_callbacks
182
+ end
183
+
184
+ describe ".guard_transition" do
185
+ it_behaves_like "a callback store", :guard_transition, :guards
186
+ end
187
+
188
+ describe "#initialize" do
189
+ it "accepts an object to manipulate" do
190
+ machine_instance = machine.new(my_model)
191
+ expect(machine_instance.object).to be(my_model)
192
+ end
193
+
194
+ context "transition class" do
195
+ it "sets a default" do
196
+ Statesman.storage_adapter.should_receive(:new).once
197
+ .with(Statesman::Transition, my_model)
198
+ machine.new(my_model)
199
+ end
200
+
201
+ it "sets the passed class" do
202
+ my_transition_class = Class.new
203
+ Statesman.storage_adapter.should_receive(:new).once
204
+ .with(my_transition_class, my_model)
205
+ machine.new(my_model, transition_class: my_transition_class)
206
+ end
207
+ end
208
+ end
209
+
210
+ describe "#current_state" do
211
+ before do
212
+ machine.class_eval do
213
+ state :x, initial: true
214
+ state :y
215
+ state :z
216
+ transition from: :x, to: :y
217
+ transition from: :y, to: :z
218
+ end
219
+ end
220
+
221
+ let(:instance) { machine.new(my_model) }
222
+ subject { instance.current_state }
223
+
224
+ context "with no transitions" do
225
+ it { should eq(machine.initial_state) }
226
+ end
227
+
228
+ context "with multiple transitions" do
229
+ before do
230
+ instance.transition_to!(:y)
231
+ instance.transition_to!(:z)
232
+ end
233
+
234
+ it { should eq("z") }
235
+ end
236
+ end
237
+
238
+ describe "#last_transition" do
239
+ let(:instance) { machine.new(my_model) }
240
+ let(:last_action) { "Whatever" }
241
+
242
+ it "delegates to the storage adapter" do
243
+ Statesman.storage_adapter.any_instance.should_receive(:last).once
244
+ .and_return(last_action)
245
+ expect(instance.last_transition).to be(last_action)
246
+ end
247
+ end
248
+
249
+ describe "#can_transition_to?" do
250
+ before do
251
+ machine.class_eval do
252
+ state :x, initial: true
253
+ state :y
254
+ transition from: :x, to: :y
255
+ end
256
+ end
257
+
258
+ let(:instance) { machine.new(my_model) }
259
+ subject { instance.can_transition_to?(new_state) }
260
+
261
+ context "when the transition is invalid" do
262
+ context "with an initial to state" do
263
+ let(:new_state) { :x }
264
+ it { should be_false }
265
+ end
266
+
267
+ context "with a terminal from state" do
268
+ before { instance.transition_to!(:y) }
269
+ let(:new_state) { :y }
270
+ it { should be_false }
271
+ end
272
+ end
273
+
274
+ context "when the transition valid" do
275
+ let(:new_state) { :y }
276
+ it { should be_true }
277
+
278
+ context "but it has a failing guard" do
279
+ before { machine.guard_transition(to: :y) { false } }
280
+ it { should be_false }
281
+ end
282
+ end
283
+ end
284
+
285
+ describe "#transition_to!" do
286
+ before do
287
+ machine.class_eval do
288
+ state :x, initial: true
289
+ state :y
290
+ state :z
291
+ transition from: :x, to: :y
292
+ transition from: :y, to: :z
293
+ end
294
+ end
295
+
296
+ let(:instance) { machine.new(my_model) }
297
+
298
+ context "when the state cannot be transitioned to" do
299
+ it "raises an error" do
300
+ expect do
301
+ instance.transition_to!(:z)
302
+ end.to raise_error(Statesman::TransitionFailedError)
303
+ end
304
+ end
305
+
306
+ context "when the state can be transitioned to" do
307
+ it "changes state" do
308
+ instance.transition_to!(:y)
309
+ expect(instance.current_state).to eq("y")
310
+ end
311
+
312
+ it "creates a new transition object" do
313
+ expect do
314
+ instance.transition_to!(:y)
315
+ end.to change(instance.history, :count).by(1)
316
+
317
+ expect(instance.history.first).to be_a(Statesman::Transition)
318
+ expect(instance.history.first.to_state).to eq("y")
319
+ end
320
+
321
+ it "sends metadata to the transition object" do
322
+ meta = { "my" => "hash" }
323
+ instance.transition_to!(:y, meta)
324
+ expect(instance.history.first.metadata).to eq(meta)
325
+ end
326
+
327
+ it "returns true" do
328
+ expect(instance.transition_to!(:y)).to be_true
329
+ end
330
+
331
+ context "with a guard" do
332
+ let(:result) { true }
333
+ let(:guard_cb) { -> (*args) { result } }
334
+ before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
335
+
336
+ context "and an object to act on" do
337
+ let(:instance) { machine.new(my_model) }
338
+
339
+ it "passes the object to the guard" do
340
+ guard_cb.should_receive(:call).once
341
+ .with(my_model, instance.last_transition, nil).and_return(true)
342
+ instance.transition_to!(:y)
343
+ end
344
+ end
345
+
346
+ context "which passes" do
347
+ it "changes state" do
348
+ instance.transition_to!(:y)
349
+ expect(instance.current_state).to eq("y")
350
+ end
351
+ end
352
+
353
+ context "which fails" do
354
+ let(:result) { false }
355
+
356
+ it "raises an exception" do
357
+ expect do
358
+ instance.transition_to!(:y)
359
+ end.to raise_error(Statesman::GuardFailedError)
360
+ end
361
+ end
362
+ end
363
+
364
+ context "with a before callback" do
365
+ let(:callbacks) { [] }
366
+ before { instance.stub(:before_callbacks_for).and_return(callbacks) }
367
+
368
+ it "is passed to the adapter" do
369
+ Statesman::Adapters::Memory.any_instance.should_receive(:create)
370
+ .with("y", callbacks, anything, anything)
371
+ instance.transition_to!(:y)
372
+ end
373
+ end
374
+
375
+ context "with an after callback" do
376
+ let(:callbacks) { [] }
377
+ before { instance.stub(:after_callbacks_for).and_return(callbacks) }
378
+
379
+ it "is passed to the adapter" do
380
+ Statesman::Adapters::Memory.any_instance.should_receive(:create)
381
+ .with("y", anything, callbacks, anything)
382
+ instance.transition_to!(:y)
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ describe "#transition_to" do
389
+ let(:instance) { machine.new(my_model) }
390
+ let(:metadata) { { some: :metadata } }
391
+ subject { instance.transition_to(:some_state, metadata) }
392
+
393
+ context "when it is succesful" do
394
+ before do
395
+ instance.should_receive(:transition_to!).once
396
+ .with(:some_state, metadata).and_return(:some_state)
397
+ end
398
+ it { should be(:some_state) }
399
+ end
400
+
401
+ context "when it is unsuccesful" do
402
+ before do
403
+ instance.stub(:transition_to!).and_raise(Statesman::GuardFailedError)
404
+ end
405
+ it { should be_false }
406
+ end
407
+ end
408
+
409
+ shared_examples "a callback filter" do |definer, getter|
410
+ before do
411
+ machine.class_eval do
412
+ state :x
413
+ state :y
414
+ state :z
415
+ transition from: :x, to: :y
416
+ transition from: :y, to: :z
417
+ end
418
+ end
419
+
420
+ let(:instance) { machine.new(my_model) }
421
+ let(:callbacks) { instance.send(getter, from: :x, to: :y) }
422
+
423
+ context "with no defined callbacks" do
424
+ specify { expect(callbacks).to eq([]) }
425
+ end
426
+
427
+ context "with defined callbacks" do
428
+ let(:callback_1) { -> { "Hi" } }
429
+ let(:callback_2) { -> { "Bye" } }
430
+
431
+ before do
432
+ machine.send(definer, from: :x, to: :y, &callback_1)
433
+ machine.send(definer, from: :y, to: :z, &callback_2)
434
+ end
435
+
436
+ it "contains the relevant callback" do
437
+ expect(callbacks.map(&:callback)).to include(callback_1)
438
+ end
439
+
440
+ it "does not contain the irrelevant callback" do
441
+ expect(callbacks.map(&:callback)).to_not include(callback_2)
442
+ end
443
+ end
444
+ end
445
+
446
+ describe "#guards_for" do
447
+ it_behaves_like "a callback filter", :guard_transition, :guards_for
448
+ end
449
+
450
+ describe "#before_callbacks_for" do
451
+ it_behaves_like "a callback filter", :before_transition,
452
+ :before_callbacks_for
453
+ end
454
+
455
+ describe "#after_callbacks_for" do
456
+ it_behaves_like "a callback filter", :after_transition,
457
+ :after_callbacks_for
458
+ end
459
+ end