composable_state_machine 1.0.2
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 +15 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.simplecov +4 -0
- data/.travis.yml +8 -0
- data/.yardopts +4 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +352 -0
- data/Rakefile +19 -0
- data/assets/class-diagram.yuml +24 -0
- data/assets/uml-class-diagram.png +0 -0
- data/composable_state_machine.gemspec +35 -0
- data/lib/composable_state_machine.rb +45 -0
- data/lib/composable_state_machine/behaviors.rb +48 -0
- data/lib/composable_state_machine/callback_runner.rb +19 -0
- data/lib/composable_state_machine/callbacks.rb +56 -0
- data/lib/composable_state_machine/default_callback_runner.rb +16 -0
- data/lib/composable_state_machine/invalid_event.rb +7 -0
- data/lib/composable_state_machine/invalid_transition.rb +7 -0
- data/lib/composable_state_machine/invalid_trigger.rb +7 -0
- data/lib/composable_state_machine/machine.rb +21 -0
- data/lib/composable_state_machine/machine_with_external_state.rb +41 -0
- data/lib/composable_state_machine/model.rb +55 -0
- data/lib/composable_state_machine/transitions.rb +73 -0
- data/lib/composable_state_machine/version.rb +3 -0
- data/spec/integration/auto_update_state_spec.rb +38 -0
- data/spec/integration/instance_callbacks_spec.rb +47 -0
- data/spec/integration/leave_callbacks_spec.rb +60 -0
- data/spec/integration/leave_callbacks_with_composition_spec.rb +68 -0
- data/spec/lib/composable_state_machine/behaviors_spec.rb +83 -0
- data/spec/lib/composable_state_machine/callback_runner_spec.rb +54 -0
- data/spec/lib/composable_state_machine/callbacks_spec.rb +106 -0
- data/spec/lib/composable_state_machine/machine_spec.rb +25 -0
- data/spec/lib/composable_state_machine/machine_with_external_state_spec.rb +97 -0
- data/spec/lib/composable_state_machine/model_spec.rb +76 -0
- data/spec/lib/composable_state_machine/transitions_spec.rb +77 -0
- data/spec/lib/composable_state_machine_spec.rb +53 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/delegation.rb +196 -0
- metadata +218 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'automatic state updates' do
|
4
|
+
|
5
|
+
class Room
|
6
|
+
MACHINE_MODEL = ComposableStateMachine.model(
|
7
|
+
transitions: {
|
8
|
+
heat: {cold: :warm, warm: :hot},
|
9
|
+
cool: {warm: :cold, hot: :warm},
|
10
|
+
}
|
11
|
+
)
|
12
|
+
|
13
|
+
attr_accessor :temp
|
14
|
+
|
15
|
+
def initialize(temp)
|
16
|
+
@machine = ComposableStateMachine::MachineWithExternalState.new(
|
17
|
+
MACHINE_MODEL, method(:temp), method(:temp=), state: temp)
|
18
|
+
end
|
19
|
+
|
20
|
+
def heat(periods = 1)
|
21
|
+
periods.times { @machine.trigger(:heat) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def cool(periods = 1)
|
25
|
+
periods.times { @machine.trigger(:cool) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'updates the room temperature automatically' do
|
30
|
+
Room.new(:cold).tap do |room|
|
31
|
+
room.heat(5)
|
32
|
+
room.temp.should eq :hot
|
33
|
+
room.cool
|
34
|
+
room.temp.should eq :warm
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'callbacks binding to class instances' do
|
4
|
+
|
5
|
+
class Person1
|
6
|
+
include ComposableStateMachine::CallbackRunner
|
7
|
+
|
8
|
+
MACHINE_MODEL = ComposableStateMachine.model(
|
9
|
+
transitions: {
|
10
|
+
hire: {candidate: :hired, departed: :hired, fired: :hired},
|
11
|
+
leave: {hired: :departed},
|
12
|
+
fire: {hired: :fired},
|
13
|
+
},
|
14
|
+
behaviors: {
|
15
|
+
enter: {
|
16
|
+
hired: proc { puts "Welcome, #{@name}!" },
|
17
|
+
fired: proc { puts "Gee, #{@name}..." },
|
18
|
+
}
|
19
|
+
}
|
20
|
+
)
|
21
|
+
|
22
|
+
def initialize(name, state)
|
23
|
+
@name = name
|
24
|
+
@machine = ComposableStateMachine.machine(
|
25
|
+
MACHINE_MODEL,
|
26
|
+
state: state, callback_runner: self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def hire!
|
30
|
+
@machine.trigger(:hire)
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def fire!
|
35
|
+
@machine.trigger(:fire)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'runs callbacks in the context of the Person instance' do
|
41
|
+
STDOUT.should_receive(:puts).with('Welcome, Bob!')
|
42
|
+
STDOUT.should_receive(:puts).with('Gee, Bob...')
|
43
|
+
|
44
|
+
Person1.new('Bob', :candidate).hire!.fire!
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'extending state machines with leave callbacks' do
|
4
|
+
|
5
|
+
class ModelWithLeaveCallbacks < ComposableStateMachine::Model
|
6
|
+
def run_callbacks(callback_runner, current_state, event, new_state, arguments)
|
7
|
+
run_callbacks_for(callback_runner, :leave, current_state,
|
8
|
+
current_state, event, new_state, *arguments)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Person2
|
14
|
+
include ComposableStateMachine::CallbackRunner
|
15
|
+
|
16
|
+
MACHINE_MODEL = ComposableStateMachine.model(
|
17
|
+
transitions: {
|
18
|
+
hire: {candidate: :hired, departed: :hired, fired: :hired},
|
19
|
+
leave: {hired: :departed},
|
20
|
+
fire: {hired: :fired},
|
21
|
+
},
|
22
|
+
behaviors: {
|
23
|
+
enter: {
|
24
|
+
hired: proc { puts "Welcome, #{@name}!" },
|
25
|
+
fired: proc { puts "Gee, #{@name}..." },
|
26
|
+
},
|
27
|
+
leave: {
|
28
|
+
fired: proc { puts 'Is this a good idea?' }
|
29
|
+
}
|
30
|
+
},
|
31
|
+
model_factory: ModelWithLeaveCallbacks
|
32
|
+
)
|
33
|
+
|
34
|
+
def initialize(name, state)
|
35
|
+
@name = name
|
36
|
+
@machine = ComposableStateMachine.machine(
|
37
|
+
MACHINE_MODEL,
|
38
|
+
state: state, callback_runner: self)
|
39
|
+
end
|
40
|
+
|
41
|
+
def hire!
|
42
|
+
@machine.trigger(:hire)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def fire!
|
47
|
+
@machine.trigger(:fire)
|
48
|
+
self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'runs callbacks in the context of the Person instance' do
|
53
|
+
STDOUT.should_receive(:puts).with('Welcome, Bob!').twice
|
54
|
+
STDOUT.should_receive(:puts).with('Gee, Bob...')
|
55
|
+
STDOUT.should_receive(:puts).with('Is this a good idea?')
|
56
|
+
|
57
|
+
Person2.new('Bob', :candidate).hire!.fire!.hire!
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'extending state machines with leave callbacks through composition' do
|
4
|
+
|
5
|
+
class ComposableModelWithLeaveCallbacks
|
6
|
+
delegate :initial_state, :callback_runner, to: :@model
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
@model = ComposableStateMachine::Model.new(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def transition(current_state, event, arguments = [], callback_runner = nil)
|
13
|
+
@model.transition(current_state, event, arguments, callback_runner) do |new_state|
|
14
|
+
@model.run_callbacks_for(callback_runner, :leave, current_state,
|
15
|
+
current_state, event, new_state, *arguments)
|
16
|
+
yield new_state if block_given?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Person3
|
22
|
+
include ComposableStateMachine::CallbackRunner
|
23
|
+
|
24
|
+
MACHINE_MODEL = ComposableStateMachine.model(
|
25
|
+
transitions: {
|
26
|
+
hire: {candidate: :hired, departed: :hired, fired: :hired},
|
27
|
+
leave: {hired: :departed},
|
28
|
+
fire: {hired: :fired},
|
29
|
+
},
|
30
|
+
behaviors: {
|
31
|
+
enter: {
|
32
|
+
hired: proc { puts "Welcome, #{@name}!" },
|
33
|
+
fired: proc { puts "Gee, #{@name}..." },
|
34
|
+
},
|
35
|
+
leave: {
|
36
|
+
fired: proc { puts 'Is this a good idea?' }
|
37
|
+
}
|
38
|
+
},
|
39
|
+
model_factory: ComposableModelWithLeaveCallbacks
|
40
|
+
)
|
41
|
+
|
42
|
+
def initialize(name, state)
|
43
|
+
@name = name
|
44
|
+
@machine = ComposableStateMachine.machine(
|
45
|
+
MACHINE_MODEL,
|
46
|
+
state: state, callback_runner: self)
|
47
|
+
end
|
48
|
+
|
49
|
+
def hire!
|
50
|
+
@machine.trigger(:hire)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def fire!
|
55
|
+
@machine.trigger(:fire)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'runs callbacks in the context of the Person instance' do
|
61
|
+
STDOUT.should_receive(:puts).with('Welcome, Bob!').twice
|
62
|
+
STDOUT.should_receive(:puts).with('Gee, Bob...')
|
63
|
+
STDOUT.should_receive(:puts).with('Is this a good idea?')
|
64
|
+
|
65
|
+
Person3.new('Bob', :candidate).hire!.fire!.hire!
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::Behaviors do
|
4
|
+
|
5
|
+
describe 'initialization' do
|
6
|
+
it 'accepts callables for different behaviors' do
|
7
|
+
callable = proc {}
|
8
|
+
behaviors = described_class.new(enter: callable, leave: callable)
|
9
|
+
|
10
|
+
callable.should_receive(:call).twice
|
11
|
+
|
12
|
+
behaviors.call(nil, :enter)
|
13
|
+
behaviors.call(nil, :leave)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'creates callbacks from non-callables by default' do
|
17
|
+
func = proc{}
|
18
|
+
|
19
|
+
ComposableStateMachine::Callbacks.should_receive(:new).with({x: func})
|
20
|
+
|
21
|
+
described_class.new(enter: {x: func})
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'accepts an alternative callbacks factory' do
|
25
|
+
func = proc{}
|
26
|
+
callbacks_factory = double(new: double)
|
27
|
+
|
28
|
+
callbacks_factory.should_receive(:new).with({x: func})
|
29
|
+
|
30
|
+
described_class.new({enter: {x: func}}, callbacks_factory)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#on' do
|
35
|
+
it 'forwards to callbacks' do
|
36
|
+
func = proc{}
|
37
|
+
callbacks = ComposableStateMachine::Callbacks.new
|
38
|
+
behaviors = described_class.new(enter: callbacks)
|
39
|
+
|
40
|
+
callbacks.should_receive(:on).with(:a, func).once
|
41
|
+
|
42
|
+
behaviors.on(:enter, :a, func)
|
43
|
+
behaviors.on(:leave, :a, proc {})
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'creates new callbacks for new behaviors' do
|
47
|
+
func = proc{}
|
48
|
+
ComposableStateMachine::Callbacks.tap do |callback_factory|
|
49
|
+
callback_factory.should_receive(:new).and_call_original
|
50
|
+
callback_factory.any_instance.should_receive(:on).with(:a, func)
|
51
|
+
end
|
52
|
+
|
53
|
+
behaviors = described_class.new
|
54
|
+
behaviors.on(:enter, :a, func)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#call' do
|
59
|
+
it 'accepts unknown triggers' do
|
60
|
+
behaviors = described_class.new
|
61
|
+
|
62
|
+
expect do
|
63
|
+
behaviors.call(nil, :enter, :a)
|
64
|
+
end.not_to raise_error
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'returns self' do
|
68
|
+
behaviors = described_class.new
|
69
|
+
|
70
|
+
behaviors.call(nil, :enter, :a).should eq behaviors
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'passes any arguments to the callback' do
|
74
|
+
func = proc {}
|
75
|
+
behaviors = described_class.new(enter: func)
|
76
|
+
|
77
|
+
func.should_receive(:call).with(nil, :a, 1, 2, 3)
|
78
|
+
|
79
|
+
behaviors.call(nil, :enter, :a, 1, 2, 3)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::CallbackRunner do
|
4
|
+
|
5
|
+
context 'with callables' do
|
6
|
+
class TestClass
|
7
|
+
include ComposableStateMachine::CallbackRunner
|
8
|
+
|
9
|
+
def initialize(name)
|
10
|
+
@name = name
|
11
|
+
@callback = lambda { |greeting| "#{greeting}, #{@name}!" }
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_callback(greeting)
|
15
|
+
run_state_machine_callback(@callback, greeting)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'runs callbacks in the context of the object' do
|
20
|
+
obj = TestClass.new('Bob')
|
21
|
+
|
22
|
+
obj.test_callback('Hello').should eq 'Hello, Bob!'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with unbound methods' do
|
27
|
+
class TestClass
|
28
|
+
include ComposableStateMachine::CallbackRunner
|
29
|
+
|
30
|
+
def initialize(name)
|
31
|
+
@name = name
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_callback(greeting)
|
35
|
+
run_state_machine_callback(CALLBACK_METHOD, greeting)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def greet(greeting)
|
41
|
+
"#{greeting}, #{@name}!"
|
42
|
+
end
|
43
|
+
|
44
|
+
CALLBACK_METHOD = instance_method(:greet)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'runs callbacks in the context of the object' do
|
48
|
+
obj = TestClass.new('Bob')
|
49
|
+
|
50
|
+
obj.test_callback('Hello').should eq 'Hello, Bob!'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ComposableStateMachine::Callbacks do
|
4
|
+
|
5
|
+
let(:runner) { ComposableStateMachine::DefaultCallbackRunner }
|
6
|
+
|
7
|
+
describe 'initialization' do
|
8
|
+
it 'accepts triggers and actions during initialization' do
|
9
|
+
func = proc {}
|
10
|
+
callbacks = described_class.new(a: func)
|
11
|
+
|
12
|
+
func.should_receive(:call)
|
13
|
+
|
14
|
+
callbacks.call(runner, :a)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'accepts more than one callback per trigger' do
|
18
|
+
func = proc {}
|
19
|
+
callbacks = described_class.new(a: [func, func, func])
|
20
|
+
|
21
|
+
func.should_receive(:call).exactly(3)
|
22
|
+
|
23
|
+
callbacks.call(runner, :a)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#on' do
|
28
|
+
it 'accepts callables' do
|
29
|
+
func = proc {}
|
30
|
+
callbacks = described_class.new.
|
31
|
+
on(:a, func).
|
32
|
+
on(:b, func)
|
33
|
+
|
34
|
+
func.should_receive(:call).twice
|
35
|
+
|
36
|
+
callbacks.call(runner, :a)
|
37
|
+
callbacks.call(runner, :b)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'accepts blocks' do
|
41
|
+
func = proc {}
|
42
|
+
callbacks = described_class.new.
|
43
|
+
on(:a, &func)
|
44
|
+
|
45
|
+
func.should_receive(:call)
|
46
|
+
|
47
|
+
callbacks.call(runner, :a)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#call' do
|
52
|
+
it 'accepts unknown triggers' do
|
53
|
+
callbacks = described_class.new
|
54
|
+
|
55
|
+
expect do
|
56
|
+
callbacks.call(runner, :unknown)
|
57
|
+
end.not_to raise_error
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns self' do
|
61
|
+
callbacks = described_class.new
|
62
|
+
|
63
|
+
callbacks.call(runner, :unknown).should eq callbacks
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'passes callbacks and their arguments to a runner' do
|
67
|
+
func = proc {}
|
68
|
+
callbacks = described_class.new.
|
69
|
+
on(:a, func)
|
70
|
+
|
71
|
+
runner.should_receive(:run_state_machine_callback).with(func, 1, 2, 3)
|
72
|
+
|
73
|
+
callbacks.call(runner, :a, 1, 2, 3)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'passes any arguments to the callback' do
|
77
|
+
func = proc {}
|
78
|
+
callbacks = described_class.new.
|
79
|
+
on(:a, func)
|
80
|
+
|
81
|
+
func.should_receive(:call).with(1, 2, 3)
|
82
|
+
|
83
|
+
callbacks.call(runner, :a, 1, 2, 3)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'calls the :any callbacks for every trigger' do
|
87
|
+
func = proc {}
|
88
|
+
callbacks = described_class.new.
|
89
|
+
on(:any, func).
|
90
|
+
on(:a, func)
|
91
|
+
|
92
|
+
func.should_receive(:call).with(1, 2, 3).twice
|
93
|
+
|
94
|
+
callbacks.call(runner, :a, 1, 2, 3)
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'does not accept the :any trigger' do
|
98
|
+
callbacks = described_class.new
|
99
|
+
|
100
|
+
expect do
|
101
|
+
callbacks.call(runner, :any)
|
102
|
+
end.to raise_error ComposableStateMachine::InvalidTrigger
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|