statesman 0.0.1

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.
@@ -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