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.
Files changed (42) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +8 -0
  6. data/.yardopts +4 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +5 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +352 -0
  11. data/Rakefile +19 -0
  12. data/assets/class-diagram.yuml +24 -0
  13. data/assets/uml-class-diagram.png +0 -0
  14. data/composable_state_machine.gemspec +35 -0
  15. data/lib/composable_state_machine.rb +45 -0
  16. data/lib/composable_state_machine/behaviors.rb +48 -0
  17. data/lib/composable_state_machine/callback_runner.rb +19 -0
  18. data/lib/composable_state_machine/callbacks.rb +56 -0
  19. data/lib/composable_state_machine/default_callback_runner.rb +16 -0
  20. data/lib/composable_state_machine/invalid_event.rb +7 -0
  21. data/lib/composable_state_machine/invalid_transition.rb +7 -0
  22. data/lib/composable_state_machine/invalid_trigger.rb +7 -0
  23. data/lib/composable_state_machine/machine.rb +21 -0
  24. data/lib/composable_state_machine/machine_with_external_state.rb +41 -0
  25. data/lib/composable_state_machine/model.rb +55 -0
  26. data/lib/composable_state_machine/transitions.rb +73 -0
  27. data/lib/composable_state_machine/version.rb +3 -0
  28. data/spec/integration/auto_update_state_spec.rb +38 -0
  29. data/spec/integration/instance_callbacks_spec.rb +47 -0
  30. data/spec/integration/leave_callbacks_spec.rb +60 -0
  31. data/spec/integration/leave_callbacks_with_composition_spec.rb +68 -0
  32. data/spec/lib/composable_state_machine/behaviors_spec.rb +83 -0
  33. data/spec/lib/composable_state_machine/callback_runner_spec.rb +54 -0
  34. data/spec/lib/composable_state_machine/callbacks_spec.rb +106 -0
  35. data/spec/lib/composable_state_machine/machine_spec.rb +25 -0
  36. data/spec/lib/composable_state_machine/machine_with_external_state_spec.rb +97 -0
  37. data/spec/lib/composable_state_machine/model_spec.rb +76 -0
  38. data/spec/lib/composable_state_machine/transitions_spec.rb +77 -0
  39. data/spec/lib/composable_state_machine_spec.rb +53 -0
  40. data/spec/spec_helper.rb +14 -0
  41. data/spec/support/delegation.rb +196 -0
  42. metadata +218 -0
@@ -0,0 +1,3 @@
1
+ module ComposableStateMachine
2
+ VERSION = '1.0.2'
3
+ end
@@ -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