simple_state_machine 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/build.yml +46 -0
- data/.gitignore +3 -0
- data/.travis.yml +17 -0
- data/Gemfile +7 -0
- data/README.rdoc +31 -29
- data/examples/conversation.rb +5 -5
- data/examples/lamp.rb +5 -5
- data/examples/relationship.rb +8 -8
- data/examples/traffic_light.rb +3 -3
- data/examples/user.rb +1 -0
- data/gemfiles/Gemfile.activerecord-5.2.x +9 -0
- data/gemfiles/Gemfile.activerecord-6.0.x +9 -0
- data/gemfiles/Gemfile.activerecord-6.1.x +8 -0
- data/gemfiles/Gemfile.activerecord-main.x +9 -0
- data/gemfiles/Gemfile.basic +6 -0
- data/lib/simple_state_machine/decorator/active_record.rb +12 -18
- data/lib/simple_state_machine/tools/graphviz.rb +6 -2
- data/lib/simple_state_machine/version.rb +1 -1
- data/lib/simple_state_machine.rb +5 -4
- data/lib/tasks/graphviz.rake +10 -0
- data/simple_state_machine.gemspec +14 -25
- data/spec/active_record_spec.rb +196 -199
- data/spec/decorator/default_spec.rb +28 -28
- data/spec/examples_spec.rb +13 -13
- data/spec/mountable_spec.rb +18 -16
- data/spec/simple_state_machine_spec.rb +71 -63
- data/spec/spec_helper.rb +16 -9
- data/spec/state_machine_definition_spec.rb +21 -21
- data/spec/state_machine_spec.rb +12 -12
- data/spec/tools/graphviz_spec.rb +3 -2
- data/spec/tools/inspector_spec.rb +3 -3
- metadata +36 -137
- data/autotest/discover.rb +0 -1
data/spec/examples_spec.rb
CHANGED
@@ -4,54 +4,54 @@ describe "Examples" do
|
|
4
4
|
describe "TrafficLight" do
|
5
5
|
it "changes to the next state" do
|
6
6
|
tl = TrafficLight.new
|
7
|
-
tl.
|
7
|
+
expect(tl).to be_green
|
8
8
|
tl.change_state
|
9
|
-
tl.
|
9
|
+
expect(tl).to be_orange
|
10
10
|
tl.change_state
|
11
|
-
tl.
|
11
|
+
expect(tl).to be_red
|
12
12
|
tl.change_state
|
13
|
-
tl.
|
13
|
+
expect(tl).to be_green
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
17
|
describe "Lamp" do
|
18
18
|
it "changes between :on and :off" do
|
19
19
|
lamp = Lamp.new
|
20
|
-
lamp.
|
20
|
+
expect(lamp).to be_off
|
21
21
|
lamp.push_button1
|
22
|
-
lamp.
|
22
|
+
expect(lamp).to be_on
|
23
23
|
lamp.push_button2
|
24
|
-
lamp.
|
24
|
+
expect(lamp).to be_off
|
25
25
|
lamp.push_button2
|
26
|
-
lamp.
|
26
|
+
expect(lamp).to be_on
|
27
27
|
lamp.push_button1
|
28
|
-
lamp.
|
28
|
+
expect(lamp).to be_off
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
describe "Conversation" do
|
33
33
|
it "is :unread by default" do
|
34
34
|
conversation = Conversation.new
|
35
|
-
conversation.
|
35
|
+
expect(conversation).to be_unread
|
36
36
|
end
|
37
37
|
|
38
38
|
it "changes to read on view" do
|
39
39
|
conversation = Conversation.new
|
40
40
|
conversation.view
|
41
|
-
conversation.
|
41
|
+
expect(conversation).to be_read
|
42
42
|
end
|
43
43
|
|
44
44
|
it "changes to closed on close" do
|
45
45
|
conversation = Conversation.new
|
46
46
|
conversation.close
|
47
|
-
conversation.
|
47
|
+
expect(conversation).to be_closed
|
48
48
|
end
|
49
49
|
|
50
50
|
it "changes to closed on close if :read" do
|
51
51
|
conversation = Conversation.new
|
52
52
|
conversation.view
|
53
53
|
conversation.close
|
54
|
-
conversation.
|
54
|
+
expect(conversation).to be_closed
|
55
55
|
end
|
56
56
|
|
57
57
|
end
|
data/spec/mountable_spec.rb
CHANGED
@@ -1,34 +1,36 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe
|
4
|
-
|
5
|
-
|
6
|
-
event(:event, :state1 => :state2)
|
3
|
+
describe SimpleStateMachine::Mountable do
|
4
|
+
class MountableExample < SimpleStateMachine::StateMachineDefinition
|
5
|
+
event(:event, :state1 => :state2)
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
end
|
7
|
+
def decorator_class
|
8
|
+
SimpleStateMachine::Decorator::Default
|
11
9
|
end
|
12
|
-
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:klass) do
|
13
|
+
Class.new do
|
13
14
|
attr_accessor :event_called
|
14
15
|
extend SimpleStateMachine::Mountable
|
15
|
-
mount_state_machine
|
16
|
+
mount_state_machine MountableExample
|
16
17
|
def event_without_managed_state
|
17
18
|
@event_called = true
|
18
19
|
end
|
19
20
|
end
|
20
|
-
|
21
|
-
|
21
|
+
end
|
22
|
+
subject do
|
23
|
+
klass.new.tap{|i| i.state = 'state1' }
|
22
24
|
end
|
23
25
|
|
24
26
|
it "has state_helper methods" do
|
25
|
-
|
26
|
-
|
27
|
+
expect(subject).to be_state1
|
28
|
+
expect(subject).not_to be_state2
|
27
29
|
end
|
28
30
|
|
29
31
|
it "calls existing methods" do
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
subject.event
|
33
|
+
expect(subject).to be_state2
|
34
|
+
expect(subject.event_called).to eq(true)
|
33
35
|
end
|
34
36
|
end
|
@@ -3,22 +3,21 @@ require 'spec_helper'
|
|
3
3
|
describe SimpleStateMachine do
|
4
4
|
|
5
5
|
it "has an error that extends RuntimeError" do
|
6
|
-
SimpleStateMachine::IllegalStateTransitionError.superclass.
|
6
|
+
expect(SimpleStateMachine::IllegalStateTransitionError.superclass).to eq(RuntimeError)
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(state = 'state1')
|
15
|
-
@state = state
|
16
|
-
end
|
9
|
+
let(:klass) do
|
10
|
+
Class.new do
|
11
|
+
extend SimpleStateMachine
|
12
|
+
def initialize(state = 'state1')
|
13
|
+
@state = state
|
17
14
|
end
|
18
15
|
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe ".event" do
|
19
19
|
|
20
20
|
it "returns what the decorated method returns" do
|
21
|
-
klass = Class.new(@klass)
|
22
21
|
klass.instance_eval do
|
23
22
|
event :event1, :state1 => :state2
|
24
23
|
define_method :event2 do
|
@@ -26,13 +25,12 @@ describe SimpleStateMachine do
|
|
26
25
|
end
|
27
26
|
event :event2, :state2 => :state3
|
28
27
|
end
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
subject = klass.new
|
29
|
+
expect(subject.event1).to eq(nil)
|
30
|
+
expect(subject.event2).to eq('event2')
|
32
31
|
end
|
33
32
|
|
34
33
|
it "calls existing methods" do
|
35
|
-
klass = Class.new(@klass)
|
36
34
|
klass.instance_eval do
|
37
35
|
attr_accessor :event_called
|
38
36
|
define_method :event do
|
@@ -40,112 +38,122 @@ describe SimpleStateMachine do
|
|
40
38
|
end
|
41
39
|
event :event, :state1 => :state2
|
42
40
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
41
|
+
subject = klass.new
|
42
|
+
subject.event
|
43
|
+
expect(subject.event_called).to eq(true)
|
46
44
|
end
|
47
45
|
|
48
46
|
context "given an event has multiple transitions" do
|
49
|
-
|
50
|
-
klass = Class.new(@klass)
|
47
|
+
before do
|
51
48
|
klass.instance_eval do
|
52
49
|
event :event, :state1 => :state2, :state2 => :state3
|
53
50
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
51
|
+
end
|
52
|
+
|
53
|
+
it "changes state for all transitions" do
|
54
|
+
subject = klass.new
|
55
|
+
expect(subject).to be_state1
|
56
|
+
subject.event
|
57
|
+
expect(subject).to be_state2
|
58
|
+
subject.event
|
59
|
+
expect(subject).to be_state3
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
63
|
context "given an event has multiple from states" do
|
64
|
-
|
65
|
-
klass = Class.new(@klass)
|
64
|
+
before do
|
66
65
|
klass.instance_eval do
|
67
66
|
event :event, [:state1, :state2] => :state3
|
68
67
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
68
|
+
end
|
69
|
+
|
70
|
+
it "changes state for all from states" do
|
71
|
+
subject = klass.new
|
72
|
+
subject.event
|
73
|
+
expect(subject).to be_state3
|
74
|
+
subject = klass.new 'state2'
|
75
|
+
expect(subject).to be_state2
|
76
|
+
subject.event
|
77
|
+
expect(subject).to be_state3
|
76
78
|
end
|
77
79
|
end
|
78
80
|
|
79
81
|
context "given an event has :all as from state" do
|
80
|
-
|
81
|
-
klass = Class.new(@klass)
|
82
|
+
before do
|
82
83
|
klass.instance_eval do
|
83
84
|
event :other_event, :state1 => :state2
|
84
85
|
event :event, :all => :state3
|
85
86
|
end
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
87
|
+
end
|
88
|
+
|
89
|
+
it "changes state from all states" do
|
90
|
+
subject = klass.new
|
91
|
+
subject.event
|
92
|
+
expect(subject).to be_state3
|
93
|
+
subject = klass.new 'state2'
|
94
|
+
expect(subject).to be_state2
|
95
|
+
subject.event
|
96
|
+
expect(subject).to be_state3
|
93
97
|
end
|
94
98
|
end
|
95
99
|
|
96
100
|
context "given state is a symbol instead of a string" do
|
97
|
-
|
98
|
-
klass = Class.new(@klass)
|
101
|
+
before do
|
99
102
|
klass.instance_eval do
|
100
103
|
event :event, :state1 => :state2
|
101
104
|
end
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
105
|
+
end
|
106
|
+
|
107
|
+
it "changes state" do
|
108
|
+
subject = klass.new :state1
|
109
|
+
expect(subject.state).to eq(:state1)
|
110
|
+
subject.send(:event)
|
111
|
+
expect(subject).to be_state2
|
106
112
|
end
|
107
113
|
end
|
108
114
|
|
109
115
|
context "given an RuntimeError begin state" do
|
110
116
|
it "changes state to error_state when error can be caught" do
|
111
|
-
class_with_error = Class.new(
|
117
|
+
class_with_error = Class.new(klass)
|
112
118
|
class_with_error.instance_eval do
|
113
119
|
define_method :raise_error do
|
114
120
|
raise RuntimeError.new
|
115
121
|
end
|
116
122
|
event :raise_error, :state1 => :state2, RuntimeError => :failed
|
117
123
|
end
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
subject = class_with_error.new
|
125
|
+
expect(subject).to be_state1
|
126
|
+
subject.raise_error
|
127
|
+
expect(subject).to be_failed
|
122
128
|
end
|
123
129
|
|
124
130
|
it "changes state to error_state when error superclass can be caught" do
|
125
131
|
error_subclass = Class.new(RuntimeError)
|
126
|
-
class_with_error = Class.new(
|
132
|
+
class_with_error = Class.new(klass)
|
127
133
|
class_with_error.instance_eval do
|
128
134
|
define_method :raise_error do
|
129
135
|
raise error_subclass.new
|
130
136
|
end
|
131
137
|
event :raise_error, :state1 => :state2, RuntimeError => :failed
|
132
138
|
end
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
139
|
+
subject = class_with_error.new
|
140
|
+
expect(subject).to be_state1
|
141
|
+
subject.raise_error
|
142
|
+
expect(subject).to be_failed
|
137
143
|
end
|
138
144
|
end
|
139
145
|
|
140
146
|
context "given an invalid state_transition is called" do
|
141
|
-
|
142
|
-
klass = Class.new(@klass)
|
147
|
+
before do
|
143
148
|
klass.instance_eval do
|
144
149
|
event :event, :state1 => :state2
|
145
150
|
event :event2, :state2 => :state3
|
146
151
|
end
|
147
|
-
|
148
|
-
|
152
|
+
end
|
153
|
+
|
154
|
+
it "raises an IllegalStateTransitionError" do
|
155
|
+
subject = klass.new
|
156
|
+
expect { subject.event2 }.to raise_error(
|
149
157
|
SimpleStateMachine::IllegalStateTransitionError,
|
150
158
|
"You cannot 'event2' when state is 'state1'")
|
151
159
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,13 +1,20 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "bundler"
|
3
|
-
Bundler.require
|
4
|
-
|
5
|
-
require 'active_record'
|
6
|
-
|
7
|
-
|
3
|
+
Bundler.require :test
|
4
|
+
begin
|
5
|
+
require 'active_record'
|
6
|
+
rescue LoadError
|
7
|
+
puts "Skipping ActiveRecord specs"
|
8
|
+
end
|
9
|
+
|
10
|
+
ROOT = Pathname(File.expand_path(File.join(File.dirname(__FILE__), '..')))
|
11
|
+
$LOAD_PATH << File.join(ROOT, 'lib')
|
12
|
+
$LOAD_PATH << File.join(ROOT, 'spec')
|
13
|
+
$LOAD_PATH << File.join(ROOT, 'examples')
|
14
|
+
|
8
15
|
require 'simple_state_machine'
|
9
|
-
require 'examples
|
10
|
-
require 'examples
|
11
|
-
require 'examples
|
12
|
-
require 'examples
|
16
|
+
require File.join(ROOT, 'examples', 'conversation.rb')
|
17
|
+
require File.join(ROOT, 'examples', 'lamp.rb')
|
18
|
+
require File.join(ROOT, 'examples', 'traffic_light.rb')
|
19
|
+
require File.join(ROOT, 'examples', 'user.rb') if defined? ActiveRecord
|
13
20
|
|
@@ -2,24 +2,24 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe SimpleStateMachine::StateMachineDefinition do
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
let(:klass) do
|
6
|
+
Class.new do
|
7
7
|
extend SimpleStateMachine
|
8
8
|
def initialize(state = 'state1')
|
9
9
|
@state = state
|
10
10
|
end
|
11
11
|
event :event1, :state1 => :state2, :state2 => :state3
|
12
12
|
end
|
13
|
-
@smd = @klass.state_machine_definition
|
14
13
|
end
|
14
|
+
let(:smd) { klass.state_machine_definition }
|
15
15
|
|
16
16
|
it "is inherited by subclasses" do
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
subject = Class.new(klass).new
|
18
|
+
expect(subject).to be_state1
|
19
|
+
subject.event1
|
20
|
+
expect(subject).to be_state2
|
21
|
+
subject.event1
|
22
|
+
expect(subject).to be_state3
|
23
23
|
end
|
24
24
|
|
25
25
|
describe '#state_method' do
|
@@ -39,23 +39,23 @@ describe SimpleStateMachine::StateMachineDefinition do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
it "is used when changing state" do
|
42
|
-
subject.ssm_state.
|
42
|
+
expect(subject.ssm_state).to eq('state1')
|
43
43
|
subject.event1
|
44
|
-
subject.ssm_state.
|
44
|
+
expect(subject.ssm_state).to eq('state2')
|
45
45
|
subject.event2
|
46
|
-
subject.ssm_state.
|
46
|
+
expect(subject.ssm_state).to eq('state3')
|
47
47
|
end
|
48
48
|
|
49
49
|
it "works with state helper methods" do
|
50
|
-
subject.
|
50
|
+
expect(subject).to be_state1
|
51
51
|
subject.event1
|
52
|
-
subject.
|
52
|
+
expect(subject).to be_state2
|
53
53
|
subject.event2
|
54
|
-
subject.
|
54
|
+
expect(subject).to be_state3
|
55
55
|
end
|
56
56
|
|
57
57
|
it "raise an error if an invalid state_transition is called" do
|
58
|
-
|
58
|
+
expect { subject.event2 }.to raise_error(SimpleStateMachine::IllegalStateTransitionError, "You cannot 'event2' when state is 'state1'")
|
59
59
|
end
|
60
60
|
|
61
61
|
end
|
@@ -80,22 +80,22 @@ describe SimpleStateMachine::StateMachineDefinition do
|
|
80
80
|
end
|
81
81
|
|
82
82
|
it "is set when an error occurs during an event" do
|
83
|
-
subject.state.
|
83
|
+
expect(subject.state).to eq('state1')
|
84
84
|
subject.event1
|
85
|
-
subject.state.
|
85
|
+
expect(subject.state).to eq('failed')
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
89
|
describe "#transitions" do
|
90
90
|
it "has a list of transitions" do
|
91
|
-
|
92
|
-
|
91
|
+
expect(smd.transitions).to be_a(Array)
|
92
|
+
expect(smd.transitions.first).to be_a(SimpleStateMachine::Transition)
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
96
|
describe "#to_s" do
|
97
97
|
it "converts to readable string format" do
|
98
|
-
|
98
|
+
expect(smd.to_s).to match(Regexp.new("state1.event1! => state2"))
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
data/spec/state_machine_spec.rb
CHANGED
@@ -14,11 +14,11 @@ describe SimpleStateMachine::StateMachine do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it "returns the next state for the event and current state" do
|
17
|
-
subject.next_state('event1').
|
17
|
+
expect(subject.next_state('event1')).to eq('state2')
|
18
18
|
end
|
19
19
|
|
20
20
|
it "returns nil if no next state for the event and current state exists" do
|
21
|
-
subject.next_state('event2').
|
21
|
+
expect(subject.next_state('event2')).to be_nil
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -37,20 +37,20 @@ describe SimpleStateMachine::StateMachine do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
subject do
|
41
|
+
class_with_error.new.tap{|i| i.raise_error }
|
42
|
+
end
|
43
|
+
|
40
44
|
it "the raised error is accessible" do
|
41
|
-
|
42
|
-
|
43
|
-
raised_error
|
44
|
-
raised_error.should be_a(RuntimeError)
|
45
|
-
raised_error.message.should == "Something went wrong"
|
45
|
+
raised_error = subject.state_machine.raised_error
|
46
|
+
expect(raised_error).to be_a(RuntimeError)
|
47
|
+
expect(raised_error.message).to eq("Something went wrong")
|
46
48
|
end
|
47
49
|
|
48
50
|
it "the raised error is set to nil on the next transition" do
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
example.retry
|
53
|
-
example.state_machine.raised_error.should_not be
|
51
|
+
expect(subject.state_machine.raised_error).to be
|
52
|
+
subject.retry
|
53
|
+
expect(subject.state_machine.raised_error).not_to be
|
54
54
|
end
|
55
55
|
|
56
56
|
end
|
data/spec/tools/graphviz_spec.rb
CHANGED
@@ -15,13 +15,14 @@ describe SimpleStateMachine::Tools::Graphviz do
|
|
15
15
|
|
16
16
|
describe "#to_graphviz_dot" do
|
17
17
|
it "converts to graphviz dot format" do
|
18
|
-
@smd.to_graphviz_dot.
|
18
|
+
expect(@smd.to_graphviz_dot).to eq(%(digraph G {\n"state1"->"state2"[label=event1];\n"state2"->"state3"[label=event1]\n}))
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
describe "#google_chart_url" do
|
23
23
|
it "shows the state and event dependencies as a Google chart" do
|
24
|
-
@smd.
|
24
|
+
graph = @smd.transitions.map { |t| t.to_graphviz_dot }.sort.join(";")
|
25
|
+
expect(@smd.google_chart_url).to eq("http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape graph}}")
|
25
26
|
end
|
26
27
|
end
|
27
28
|
end
|
@@ -19,7 +19,7 @@ describe SimpleStateMachine::Tools::Inspector do
|
|
19
19
|
end
|
20
20
|
|
21
21
|
it "returns all 'from' states that aren't 'to' states" do
|
22
|
-
@klass.new.begin_states.
|
22
|
+
expect(@klass.new.begin_states).to eq([:state1, RuntimeError])
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -40,7 +40,7 @@ describe SimpleStateMachine::Tools::Inspector do
|
|
40
40
|
end
|
41
41
|
|
42
42
|
it "returns all 'to' states that aren't 'from' states" do
|
43
|
-
@klass.new.end_states.
|
43
|
+
expect(@klass.new.end_states).to eq([:state3])
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -61,7 +61,7 @@ describe SimpleStateMachine::Tools::Inspector do
|
|
61
61
|
end
|
62
62
|
|
63
63
|
it "returns all states" do
|
64
|
-
@klass.new.states.map(&:to_s).sort.
|
64
|
+
expect(@klass.new.states.map(&:to_s).sort).to eq(%w{state1 state2 state3})
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|