state_jacket 0.1.1 → 1.0.0
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 +4 -4
- data/Gemfile +1 -2
- data/Gemfile.lock +48 -28
- data/README.md +60 -114
- data/Rakefile +6 -2
- data/lib/state_jacket.rb +5 -2
- data/lib/state_jacket/state_machine.rb +73 -0
- data/lib/state_jacket/state_transition_system.rb +75 -0
- data/lib/state_jacket/version.rb +1 -1
- data/test/state_machine_test.rb +171 -0
- data/test/state_transition_sytem_test.rb +123 -0
- data/test/test_helper.rb +5 -0
- metadata +27 -9
- data/lib/state_jacket/catalog.rb +0 -72
- data/test/catalog_test.rb +0 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ec65a9c784df47f148a60d0a809321249a7293c
|
4
|
+
data.tar.gz: 5146e1e29bf9613094983f5df0648f7804d018ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af0b443f0965da9896fbbd8928926d0fe8742531c2c0043f92e73f184df5c18d5ddb5d3d35c12e5ea79548d13687e8af04fdb3739224972ac43c42475c8764bf
|
7
|
+
data.tar.gz: 03a364ceb0dcbe9a4b0c1b3c3d9a5fd4efbe3569430ca063f4ec762a3ad9f759f71066154f545307e42f7e3f589698926a4d90e69cc3dac576bca911cf85ea9c
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,52 +1,70 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
state_jacket (0.
|
4
|
+
state_jacket (1.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
+
ast (2.3.0)
|
9
10
|
binding_of_caller (0.7.2)
|
10
11
|
debug_inspector (>= 0.0.1)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
simplecov (
|
16
|
-
term-ansicolor (
|
17
|
-
thor (
|
12
|
+
byebug (9.0.6)
|
13
|
+
coderay (1.1.1)
|
14
|
+
coveralls (0.8.21)
|
15
|
+
json (>= 1.8, < 3)
|
16
|
+
simplecov (~> 0.14.1)
|
17
|
+
term-ansicolor (~> 1.3)
|
18
|
+
thor (~> 0.19.4)
|
19
|
+
tins (~> 1.6)
|
18
20
|
debug_inspector (0.0.2)
|
19
21
|
docile (1.1.5)
|
20
22
|
interception (0.5)
|
23
|
+
json (2.1.0)
|
21
24
|
method_source (0.8.2)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
os (1.0.0)
|
26
|
+
parser (2.4.0.0)
|
27
|
+
ast (~> 2.2)
|
28
|
+
powerpack (0.1.1)
|
29
|
+
pry (0.10.4)
|
26
30
|
coderay (~> 1.1.0)
|
27
31
|
method_source (~> 0.8.1)
|
28
32
|
slop (~> 3.4)
|
29
|
-
pry-
|
33
|
+
pry-byebug (3.4.2)
|
34
|
+
byebug (~> 9.0)
|
35
|
+
pry (~> 0.10)
|
36
|
+
pry-rescue (1.4.5)
|
30
37
|
interception (>= 0.5)
|
31
38
|
pry
|
32
|
-
pry-stack_explorer (0.4.9.
|
39
|
+
pry-stack_explorer (0.4.9.2)
|
33
40
|
binding_of_caller (>= 0.7)
|
34
41
|
pry (>= 0.9.11)
|
35
|
-
pry-test (0.
|
42
|
+
pry-test (0.6.4)
|
36
43
|
os
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
44
|
+
pry
|
45
|
+
pry-byebug
|
46
|
+
pry-rescue
|
47
|
+
pry-stack_explorer
|
48
|
+
rainbow (2.2.1)
|
49
|
+
rake (12.0.0)
|
50
|
+
rubocop (0.48.1)
|
51
|
+
parser (>= 2.3.3.1, < 3.0)
|
52
|
+
powerpack (~> 0.1)
|
53
|
+
rainbow (>= 1.99.1, < 3.0)
|
54
|
+
ruby-progressbar (~> 1.7)
|
55
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
56
|
+
ruby-progressbar (1.8.1)
|
57
|
+
simplecov (0.14.1)
|
41
58
|
docile (~> 1.1.0)
|
42
|
-
|
43
|
-
simplecov-html (~> 0.
|
44
|
-
simplecov-html (0.
|
59
|
+
json (>= 1.8, < 3)
|
60
|
+
simplecov-html (~> 0.10.0)
|
61
|
+
simplecov-html (0.10.0)
|
45
62
|
slop (3.6.0)
|
46
|
-
term-ansicolor (1.
|
47
|
-
tins (~> 0
|
48
|
-
thor (0.
|
49
|
-
tins (
|
63
|
+
term-ansicolor (1.6.0)
|
64
|
+
tins (~> 1.0)
|
65
|
+
thor (0.19.4)
|
66
|
+
tins (1.13.2)
|
67
|
+
unicode-display_width (1.2.1)
|
50
68
|
|
51
69
|
PLATFORMS
|
52
70
|
ruby
|
@@ -54,8 +72,10 @@ PLATFORMS
|
|
54
72
|
DEPENDENCIES
|
55
73
|
coveralls
|
56
74
|
pry
|
57
|
-
pry-rescue
|
58
|
-
pry-stack_explorer
|
59
75
|
pry-test
|
60
76
|
rake
|
77
|
+
rubocop
|
61
78
|
state_jacket!
|
79
|
+
|
80
|
+
BUNDLED WITH
|
81
|
+
1.14.6
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
|
2
2
|
[](https://codeclimate.com/github/hopsoft/state_jacket)
|
3
3
|
[](https://gemnasium.com/hopsoft/state_jacket)
|
4
4
|
[](https://travis-ci.org/hopsoft/state_jacket)
|
@@ -7,140 +7,86 @@
|
|
7
7
|
|
8
8
|
# StateJacket
|
9
9
|
|
10
|
-
## An Intuitive [State Transition System](http://en.wikipedia.org/wiki/State_transition_system)
|
10
|
+
## An Intuitive [State Transition System](http://en.wikipedia.org/wiki/State_transition_system) & [State Machine](https://en.wikipedia.org/wiki/Finite-state_machine)
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
Keeping states, transitions, & events straight can be tricky.
|
15
|
-
StateJacket simplifies things by isolating the management of states & transitions.
|
16
|
-
Events are left out, making it much easier to reason about what states exist
|
17
|
-
and how they transition to other states.
|
12
|
+
StateJacket provides an intuitive approach to building complex state machines
|
13
|
+
by isolating the concerns of the state transition system & state machine.
|
18
14
|
|
19
|
-
|
20
|
-
|
21
|
-
## The Basics
|
22
|
-
|
23
|
-
#### Install
|
15
|
+
## Install
|
24
16
|
|
25
17
|
```sh
|
26
18
|
gem install state_jacket
|
27
19
|
```
|
28
20
|
|
29
|
-
|
30
|
-
|
31
|
-

|
32
|
-
|
33
|
-
```ruby
|
34
|
-
require "state_jacket"
|
35
|
-
|
36
|
-
states = StateJacket::Catalog.new
|
37
|
-
states.add :open => [:closed, :error]
|
38
|
-
states.add :closed => [:open, :error]
|
39
|
-
states.add :error
|
40
|
-
states.lock
|
41
|
-
|
42
|
-
states.inspect # => {:open=>[:closed, :error], :closed=>[:open, :error], :error=>nil}
|
43
|
-
states.transitioners # => [:open, :closed]
|
44
|
-
states.terminators # => [:error]
|
45
|
-
|
46
|
-
states.can_transition? :open => :closed # => true
|
47
|
-
states.can_transition? :closed => :open # => true
|
48
|
-
states.can_transition? :error => :open # => false
|
49
|
-
states.can_transition? :error => :closed # => false
|
50
|
-
```
|
51
|
-
|
52
|
-
## Next Steps
|
21
|
+
## Example
|
53
22
|
|
54
|
-
|
23
|
+
Let's define states & transitions (i.e. the state transition system) & a state machine for a [turnstyle](http://en.wikipedia.org/wiki/Finite-state_machine#Example:_a_turnstile).
|
55
24
|
|
56
|
-
|
25
|
+

|
57
26
|
|
58
|
-
|
27
|
+
### State Transition System
|
59
28
|
|
60
29
|
```ruby
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
states.can_transition? :idle => :dialing # => true
|
75
|
-
states.can_transition? :dialing => [:idle, :connecting] # => true
|
76
|
-
states.can_transition? :connecting => [:idle, :busy, :connected] # => true
|
77
|
-
states.can_transition? :busy => :idle # => true
|
78
|
-
states.can_transition? :connected => :idle # => true
|
79
|
-
states.can_transition? :idle => [:dialing, :connected] # => false
|
30
|
+
system = StateJacket::StateTransitionSystem.new
|
31
|
+
system.add :opened => [:closed, :errored]
|
32
|
+
system.add :closed => [:opened, :errored]
|
33
|
+
system.lock # prevent further changes
|
34
|
+
|
35
|
+
system.to_h.inspect # => {"opened"=>["closed", "errored"], "closed"=>["opened", "errored"], "errored"=>nil}
|
36
|
+
system.transitioners # => ["opened", "closed"]
|
37
|
+
system.terminators # => ["errored"]
|
38
|
+
|
39
|
+
system.can_transition? :opened => :closed # => true
|
40
|
+
system.can_transition? :closed => :opened # => true
|
41
|
+
system.can_transition? :errored => :opened # => false
|
42
|
+
system.can_transition? :errored => :closed # => false
|
80
43
|
```
|
81
44
|
|
82
|
-
|
45
|
+
### State Machine
|
83
46
|
|
84
|
-
|
85
|
-
We'll reuse the turnstyle states from the example from above.
|
47
|
+
Define the events that trigger transitions defined by the state transition system (i.e. the state machine).
|
86
48
|
|
87
49
|
```ruby
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
50
|
+
machine = StateJacket::StateMachine.new(system, state: "closed")
|
51
|
+
machine.on :open, :closed => :opened
|
52
|
+
machine.on :close, :opened => :closed
|
53
|
+
machine.lock # prevent further changes
|
54
|
+
|
55
|
+
machine.to_h.inspect # => {"open"=>[{"closed"=>"opened"}], "close"=>[{"opened"=>"closed"}]}
|
56
|
+
machine.events # => ["open", "close"]
|
57
|
+
|
58
|
+
machine.state # => "closed"
|
59
|
+
machine.is_event? :open # => true
|
60
|
+
machine.is_event? :close # => true
|
61
|
+
machine.is_event? :other # => false
|
62
|
+
|
63
|
+
machine.can_trigger? :open # => true
|
64
|
+
machine.can_trigger? :close # => false
|
65
|
+
|
66
|
+
machine.state # => "closed"
|
67
|
+
machine.trigger :open # => "opened"
|
68
|
+
machine.state # => "opened"
|
69
|
+
|
70
|
+
# you can also pass a block when triggering events
|
71
|
+
machine.trigger :close do |from_state, to_state|
|
72
|
+
# custom logic can be placed here
|
73
|
+
from_state # => "opened"
|
74
|
+
to_state # => "closed"
|
75
|
+
end
|
101
76
|
|
102
|
-
|
103
|
-
if states.can_transition? current_state => :open
|
104
|
-
@current_state = :open
|
105
|
-
else
|
106
|
-
raise "Can't transition from #{@current_state} to :open"
|
107
|
-
end
|
108
|
-
end
|
77
|
+
machine.state # => "closed"
|
109
78
|
|
110
|
-
|
111
|
-
|
112
|
-
@current_state = :closed
|
113
|
-
else
|
114
|
-
raise "Can't transition from #{@current_state} to :closed"
|
115
|
-
end
|
116
|
-
end
|
79
|
+
# this is a noop because can_trigger?(:close) is false
|
80
|
+
machine.trigger :close # => nil
|
117
81
|
|
118
|
-
|
119
|
-
|
82
|
+
machine.state # => "closed"
|
83
|
+
|
84
|
+
begin
|
85
|
+
machine.trigger :open do |from_state, to_state|
|
86
|
+
raise # the transition isn't performed if an error occurs in the block
|
120
87
|
end
|
88
|
+
rescue
|
121
89
|
end
|
122
90
|
|
123
|
-
#
|
124
|
-
turnstyle = Turnstyle.new
|
125
|
-
turnstyle.current_state # => :closed
|
126
|
-
turnstyle.open
|
127
|
-
turnstyle.current_state # => :open
|
128
|
-
turnstyle.close
|
129
|
-
turnstyle.current_state # => :closed
|
130
|
-
turnstyle.close # => RuntimeError: Can't transition from closed to :closed
|
131
|
-
turnstyle.open
|
132
|
-
turnstyle.current_state # => :open
|
133
|
-
turnstyle.open # => RuntimeError: Can't transition from open to :open
|
134
|
-
turnstyle.break
|
135
|
-
turnstyle.open # => RuntimeError: Can't transition from error to :open
|
136
|
-
turnstyle.close # => RuntimeError: Can't transition from error to :closed
|
137
|
-
```
|
138
|
-
|
139
|
-
## Running the Tests
|
140
|
-
|
141
|
-
```
|
142
|
-
gem install state_jacket
|
143
|
-
gem unpack state_jacket
|
144
|
-
cd state_jacket-VERSION
|
145
|
-
rake
|
91
|
+
machine.state # => "closed"
|
146
92
|
```
|
data/Rakefile
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
|
3
|
-
task :
|
3
|
+
task default: [:test]
|
4
|
+
|
5
|
+
desc "Runs rubocop."
|
6
|
+
task :rubocop do
|
7
|
+
exec "bundle exec rubocop -c .rubocop.yml"
|
8
|
+
end
|
4
9
|
|
5
10
|
desc "Runs the test suite."
|
6
11
|
task :test do
|
7
12
|
exec "bundle exec pry-test --disable-pry"
|
8
13
|
end
|
9
|
-
|
data/lib/state_jacket.rb
CHANGED
@@ -0,0 +1,73 @@
|
|
1
|
+
module StateJacket
|
2
|
+
class StateMachine
|
3
|
+
attr_reader :state
|
4
|
+
|
5
|
+
def initialize(transition_system, state:)
|
6
|
+
transition_system.lock
|
7
|
+
raise ArgumentError.new("illegal state") unless transition_system.is_state?(state)
|
8
|
+
@transition_system = transition_system
|
9
|
+
@state = state.to_s
|
10
|
+
@triggers = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
triggers.dup
|
15
|
+
end
|
16
|
+
|
17
|
+
def events
|
18
|
+
triggers.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def on(event, transitions={})
|
22
|
+
raise "events cannot be added after locking" if is_locked?
|
23
|
+
raise ArgumentError.new("event has already been added") if is_event?(event)
|
24
|
+
transitions.each do |from, to|
|
25
|
+
raise ArgumentError.new("illegal transition") unless transition_system.can_transition?(from => to)
|
26
|
+
triggers[event.to_s] ||= []
|
27
|
+
triggers[event.to_s] << { from.to_s => to.to_s }
|
28
|
+
triggers[event.to_s].uniq!
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def trigger(event)
|
33
|
+
raise "must be locked before triggering events" unless is_locked?
|
34
|
+
raise ArgumentError.new("event not defined") unless is_event?(event)
|
35
|
+
transition = transition_for(event)
|
36
|
+
return nil unless transition
|
37
|
+
from = @state
|
38
|
+
to = transition.values.first
|
39
|
+
raise "current state doesn't match transition state" unless from == transition.keys.first
|
40
|
+
yield from, to if block_given?
|
41
|
+
@state = to
|
42
|
+
end
|
43
|
+
|
44
|
+
def lock
|
45
|
+
return true if is_locked?
|
46
|
+
triggers.freeze
|
47
|
+
triggers.values.map(&:freeze)
|
48
|
+
triggers.values.freeze
|
49
|
+
@locked = true
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_locked?
|
53
|
+
!!@locked
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_event?(event)
|
57
|
+
triggers.has_key? event.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def can_trigger?(event)
|
61
|
+
return false unless is_locked?
|
62
|
+
!!transition_for(event)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
attr_reader :transition_system, :triggers
|
68
|
+
|
69
|
+
def transition_for(event)
|
70
|
+
triggers[event.to_s].find { |entry| entry.keys.first == state }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module StateJacket
|
2
|
+
class StateTransitionSystem
|
3
|
+
def initialize
|
4
|
+
@transitions = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_h
|
8
|
+
transitions.dup
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(state)
|
12
|
+
raise "states cannot be added after locking" if is_locked?
|
13
|
+
if state.is_a?(Hash)
|
14
|
+
from = state.keys.first.to_s
|
15
|
+
transitions[from] = make_states(state.values.first)
|
16
|
+
else
|
17
|
+
transitions[state.to_s] = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def lock
|
22
|
+
return true if is_locked?
|
23
|
+
transitions.freeze
|
24
|
+
transitions.values.each { |value| value.freeze unless value.nil? }
|
25
|
+
@locked = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def is_locked?
|
29
|
+
!!@locked
|
30
|
+
end
|
31
|
+
|
32
|
+
def can_transition?(from_to)
|
33
|
+
raise ArgumentError.new("from_to should contain a single transition") unless from_to.size == 1
|
34
|
+
from = from_to.keys.first.to_s
|
35
|
+
to = make_states(from_to.values.first)
|
36
|
+
allowed_states = transitions[from] || []
|
37
|
+
(to & allowed_states).length == to.length
|
38
|
+
end
|
39
|
+
|
40
|
+
def states
|
41
|
+
transitions.keys
|
42
|
+
end
|
43
|
+
|
44
|
+
def transitioners
|
45
|
+
transitions.keys.select { |state| transitions[state] != nil }
|
46
|
+
end
|
47
|
+
|
48
|
+
def terminators
|
49
|
+
transitions.keys.select { |state| transitions[state] == nil }
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_state?(state)
|
53
|
+
transitions.keys.include?(state.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_terminator?(state)
|
57
|
+
terminators.include?(state.to_s)
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_transitioner?(state)
|
61
|
+
transitioners.include?(state.to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :transitions
|
67
|
+
|
68
|
+
def make_states(values)
|
69
|
+
values = [values.to_s] unless values.respond_to?(:map)
|
70
|
+
values = values.map(&:to_s)
|
71
|
+
values.each { |value| transitions[value] ||= nil } unless transitions.frozen?
|
72
|
+
values
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/state_jacket/version.rb
CHANGED
@@ -0,0 +1,171 @@
|
|
1
|
+
require_relative "./test_helper"
|
2
|
+
|
3
|
+
class StateMachineTest < PryTest::Test
|
4
|
+
before do
|
5
|
+
@transitions = StateJacket::StateTransitionSystem.new
|
6
|
+
end
|
7
|
+
|
8
|
+
test "new raises with invalid state" do
|
9
|
+
@transitions.add opened: [:closed, :errored]
|
10
|
+
@transitions.add closed: [:opened, :errored]
|
11
|
+
begin
|
12
|
+
StateJacket::StateMachine.new(@transitions, state: :foo)
|
13
|
+
rescue ArgumentError => e
|
14
|
+
end
|
15
|
+
assert e
|
16
|
+
end
|
17
|
+
|
18
|
+
test "new assigns state" do
|
19
|
+
@transitions.add opened: [:closed, :errored]
|
20
|
+
@transitions.add closed: [:opened, :errored]
|
21
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :opened)
|
22
|
+
assert machine.state == "opened"
|
23
|
+
end
|
24
|
+
|
25
|
+
test "new locks the jacket" do
|
26
|
+
@transitions.add opened: [:closed, :errored]
|
27
|
+
@transitions.add closed: [:opened, :errored]
|
28
|
+
StateJacket::StateMachine.new(@transitions, state: :closed)
|
29
|
+
assert @transitions.is_locked?
|
30
|
+
end
|
31
|
+
|
32
|
+
test "creating an event that has an illegal transition fails" do
|
33
|
+
@transitions.add opened: [:closed, :errored]
|
34
|
+
@transitions.add closed: [:opened, :errored]
|
35
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
36
|
+
begin
|
37
|
+
machine.on :reopen, errored: :open
|
38
|
+
rescue StandardError => e
|
39
|
+
end
|
40
|
+
assert e
|
41
|
+
end
|
42
|
+
|
43
|
+
test "to_h" do
|
44
|
+
@transitions.add opened: [:closed, :errored]
|
45
|
+
@transitions.add closed: [:opened, :errored]
|
46
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
47
|
+
machine.on :open, closed: :opened
|
48
|
+
machine.on :close, opened: :closed
|
49
|
+
assert machine.to_h == {"open"=>[{"closed"=>"opened"}], "close"=>[{"opened"=>"closed"}]}
|
50
|
+
end
|
51
|
+
|
52
|
+
test "lock prevents future mutations" do
|
53
|
+
@transitions.add opened: [:closed, :errored]
|
54
|
+
@transitions.add closed: [:opened, :errored]
|
55
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
56
|
+
machine.on :open, closed: :opened
|
57
|
+
machine.on :close, opened: :closed
|
58
|
+
assert machine.lock
|
59
|
+
assert machine.is_locked?
|
60
|
+
begin
|
61
|
+
machine.on :error, closed: :opened
|
62
|
+
rescue StandardError => e
|
63
|
+
end
|
64
|
+
assert e
|
65
|
+
end
|
66
|
+
|
67
|
+
test "can't trigger events unless locked" do
|
68
|
+
@transitions.add opened: [:closed]
|
69
|
+
@transitions.add closed: [:opened]
|
70
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
71
|
+
machine.on :open, closed: :opened
|
72
|
+
machine.on :close, opened: :closed
|
73
|
+
begin
|
74
|
+
machine.trigger :open
|
75
|
+
rescue StandardError => e
|
76
|
+
end
|
77
|
+
assert e
|
78
|
+
end
|
79
|
+
|
80
|
+
test "trigger event sets matching state" do
|
81
|
+
@transitions.add opened: [:closed]
|
82
|
+
@transitions.add closed: [:opened]
|
83
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
84
|
+
machine.on :open, closed: :opened
|
85
|
+
machine.on :close, opened: :closed
|
86
|
+
machine.lock
|
87
|
+
machine.trigger :open
|
88
|
+
assert machine.state == "opened"
|
89
|
+
machine.trigger :close
|
90
|
+
assert machine.state == "closed"
|
91
|
+
end
|
92
|
+
|
93
|
+
test "trigger noop" do
|
94
|
+
@transitions.add opened: [:closed]
|
95
|
+
@transitions.add closed: [:opened]
|
96
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
97
|
+
machine.on :open, closed: :opened
|
98
|
+
machine.on :close, opened: :closed
|
99
|
+
machine.lock
|
100
|
+
assert machine.trigger(:open) == "opened"
|
101
|
+
assert machine.trigger(:open).nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
test "trigger event sets matching state with block" do
|
105
|
+
@transitions.add opened: [:closed]
|
106
|
+
@transitions.add closed: [:opened]
|
107
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
108
|
+
machine.on :open, closed: :opened
|
109
|
+
machine.on :close, opened: :closed
|
110
|
+
machine.lock
|
111
|
+
machine.trigger(:open) { |from, to| "consumer logic goes here..." }
|
112
|
+
assert machine.state == "opened"
|
113
|
+
machine.trigger(:close) { |from, to| "consumer logic goes here..." }
|
114
|
+
assert machine.state == "closed"
|
115
|
+
end
|
116
|
+
|
117
|
+
test "trigger event does not set state if error in block" do
|
118
|
+
@transitions.add opened: [:closed]
|
119
|
+
@transitions.add closed: [:opened]
|
120
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
121
|
+
machine.on :open, closed: :opened
|
122
|
+
machine.on :close, opened: :closed
|
123
|
+
machine.lock
|
124
|
+
machine.trigger(:open) { |from, to| raise } rescue nil
|
125
|
+
assert machine.state == "closed"
|
126
|
+
end
|
127
|
+
|
128
|
+
test "trigger event passes from/to states to block" do
|
129
|
+
@transitions.add opened: [:closed]
|
130
|
+
@transitions.add closed: [:opened]
|
131
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
132
|
+
machine.on :open, closed: :opened
|
133
|
+
machine.on :close, opened: :closed
|
134
|
+
machine.lock
|
135
|
+
states = { from: nil, to: nil }
|
136
|
+
machine.trigger :open do |from, to|
|
137
|
+
states[:from] = from
|
138
|
+
states[:to] = to
|
139
|
+
end
|
140
|
+
assert states == { from: "closed", to: "opened" }
|
141
|
+
end
|
142
|
+
|
143
|
+
test "can_trigger? false unless locked" do
|
144
|
+
@transitions.add opened: [:closed, :errored]
|
145
|
+
@transitions.add closed: [:opened, :errored]
|
146
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
147
|
+
machine.on :open, closed: :opened
|
148
|
+
machine.on :close, opened: :closed
|
149
|
+
assert !machine.can_trigger?(:open)
|
150
|
+
end
|
151
|
+
|
152
|
+
test "can_trigger?" do
|
153
|
+
@transitions.add opened: [:closed, :errored]
|
154
|
+
@transitions.add closed: [:opened, :errored]
|
155
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
156
|
+
machine.on :open, closed: :opened
|
157
|
+
machine.on :close, opened: :closed
|
158
|
+
machine.lock
|
159
|
+
assert machine.can_trigger?(:open)
|
160
|
+
end
|
161
|
+
|
162
|
+
test "can_trigger? false" do
|
163
|
+
@transitions.add opened: [:closed, :errored]
|
164
|
+
@transitions.add closed: [:opened, :errored]
|
165
|
+
machine = StateJacket::StateMachine.new(@transitions, state: :closed)
|
166
|
+
machine.on :open, closed: :opened
|
167
|
+
machine.on :close, opened: :closed
|
168
|
+
machine.lock
|
169
|
+
assert !machine.can_trigger?(:close)
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require_relative "./test_helper"
|
2
|
+
|
3
|
+
class StateJacketTest < PryTest::Test
|
4
|
+
before do
|
5
|
+
@transitions = StateJacket::StateTransitionSystem.new
|
6
|
+
end
|
7
|
+
|
8
|
+
test "add state" do
|
9
|
+
@transitions.add :started
|
10
|
+
assert @transitions.to_h.has_key?("started")
|
11
|
+
end
|
12
|
+
|
13
|
+
test "terminators" do
|
14
|
+
@transitions.add started: [:finished]
|
15
|
+
@transitions.lock
|
16
|
+
assert @transitions.terminators == ["finished"]
|
17
|
+
end
|
18
|
+
|
19
|
+
test "is_terminator?" do
|
20
|
+
@transitions.add started: [:finished]
|
21
|
+
@transitions.lock
|
22
|
+
assert @transitions.is_terminator?(:finished)
|
23
|
+
end
|
24
|
+
|
25
|
+
test "transitioners" do
|
26
|
+
@transitions.add started: [:finished]
|
27
|
+
@transitions.lock
|
28
|
+
assert @transitions.transitioners == ["started"]
|
29
|
+
end
|
30
|
+
|
31
|
+
test "is_transitioner?" do
|
32
|
+
@transitions.add started: [:finished]
|
33
|
+
@transitions.lock
|
34
|
+
assert @transitions.is_transitioner?(:started)
|
35
|
+
end
|
36
|
+
|
37
|
+
test "can_transition?" do
|
38
|
+
@transitions.add started: [:finished]
|
39
|
+
@transitions.lock
|
40
|
+
assert @transitions.can_transition?(started: :finished)
|
41
|
+
end
|
42
|
+
|
43
|
+
test "is_state?" do
|
44
|
+
@transitions.add started: [:finished]
|
45
|
+
@transitions.lock
|
46
|
+
assert @transitions.is_state?(:started)
|
47
|
+
assert @transitions.is_state?(:finished)
|
48
|
+
end
|
49
|
+
|
50
|
+
test "lock success" do
|
51
|
+
@transitions.add started: [:finished]
|
52
|
+
begin
|
53
|
+
@transitions.lock
|
54
|
+
rescue Exception => e
|
55
|
+
end
|
56
|
+
assert e.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
test "states" do
|
60
|
+
@transitions.add started: [:finished]
|
61
|
+
@transitions.lock
|
62
|
+
assert @transitions.states == %w(finished started)
|
63
|
+
end
|
64
|
+
|
65
|
+
test "symbol state" do
|
66
|
+
@transitions.add started: [:finished]
|
67
|
+
assert @transitions.to_h.keys.include?("started")
|
68
|
+
assert @transitions.can_transition?(started: :finished)
|
69
|
+
end
|
70
|
+
|
71
|
+
test "string state" do
|
72
|
+
@transitions.add "started" => ["finished"]
|
73
|
+
assert @transitions.to_h.keys.include?("started")
|
74
|
+
assert @transitions.can_transition?("started" => "finished")
|
75
|
+
end
|
76
|
+
|
77
|
+
test "number state" do
|
78
|
+
@transitions.add 1 => [2]
|
79
|
+
assert @transitions.to_h.keys.include?("1")
|
80
|
+
assert @transitions.can_transition?(1 => 2)
|
81
|
+
end
|
82
|
+
|
83
|
+
test "turnstyle example" do
|
84
|
+
@transitions.add opened: [:closed, :errored]
|
85
|
+
@transitions.add closed: [:opened, :errored]
|
86
|
+
@transitions.lock
|
87
|
+
assert @transitions.transitioners.sort == ["closed", "opened"]
|
88
|
+
assert @transitions.terminators == ["errored"]
|
89
|
+
assert @transitions.can_transition?(opened: :closed)
|
90
|
+
assert @transitions.can_transition?(closed: :opened)
|
91
|
+
assert @transitions.can_transition?(errored: :opened) == false
|
92
|
+
assert @transitions.can_transition?(errored: :closeded) == false
|
93
|
+
end
|
94
|
+
|
95
|
+
test "phone call example" do
|
96
|
+
@transitions = StateJacket::StateTransitionSystem.new
|
97
|
+
@transitions.add idle: [:dialing]
|
98
|
+
@transitions.add dialing: [:idle, :connecting]
|
99
|
+
@transitions.add connecting: [:idle, :busy, :connected]
|
100
|
+
@transitions.add busy: [:idle]
|
101
|
+
@transitions.add connected: [:idle]
|
102
|
+
@transitions.lock
|
103
|
+
assert @transitions.transitioners.sort == ["busy", "connected", "connecting", "dialing", "idle"]
|
104
|
+
assert @transitions.terminators == []
|
105
|
+
assert @transitions.can_transition?(idle: :dialing)
|
106
|
+
assert @transitions.can_transition?(dialing: [:idle, :connecting])
|
107
|
+
assert @transitions.can_transition?(connecting: [:idle, :busy, :connected])
|
108
|
+
assert @transitions.can_transition?(busy: :idle)
|
109
|
+
assert @transitions.can_transition?(connected: :idle)
|
110
|
+
assert @transitions.can_transition?(idle: [:dialing, :connected]) == false
|
111
|
+
end
|
112
|
+
|
113
|
+
test "to_h" do
|
114
|
+
@transitions.add opened: [:closed, :errored]
|
115
|
+
@transitions.add closed: [:opened, :errored]
|
116
|
+
@transitions.lock
|
117
|
+
assert @transitions.to_h == {
|
118
|
+
"closed" => ["opened", "errored"],
|
119
|
+
"errored" => nil,
|
120
|
+
"opened" => ["closed", "errored"]
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: state_jacket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Hopkins
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: coveralls
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,7 +80,7 @@ dependencies:
|
|
66
80
|
- - ">="
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '0'
|
69
|
-
description:
|
83
|
+
description:
|
70
84
|
email:
|
71
85
|
- natehop@gmail.com
|
72
86
|
executables: []
|
@@ -79,9 +93,12 @@ files:
|
|
79
93
|
- README.md
|
80
94
|
- Rakefile
|
81
95
|
- lib/state_jacket.rb
|
82
|
-
- lib/state_jacket/
|
96
|
+
- lib/state_jacket/state_machine.rb
|
97
|
+
- lib/state_jacket/state_transition_system.rb
|
83
98
|
- lib/state_jacket/version.rb
|
84
|
-
- test/
|
99
|
+
- test/state_machine_test.rb
|
100
|
+
- test/state_transition_sytem_test.rb
|
101
|
+
- test/test_helper.rb
|
85
102
|
homepage: https://github.com/hopsoft/state_jacket
|
86
103
|
licenses:
|
87
104
|
- MIT
|
@@ -102,10 +119,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
119
|
version: '0'
|
103
120
|
requirements: []
|
104
121
|
rubyforge_project:
|
105
|
-
rubygems_version: 2.
|
122
|
+
rubygems_version: 2.6.11
|
106
123
|
signing_key:
|
107
124
|
specification_version: 4
|
108
|
-
summary:
|
125
|
+
summary: A simple & intuitive state machine
|
109
126
|
test_files:
|
110
|
-
- test/
|
111
|
-
|
127
|
+
- test/state_machine_test.rb
|
128
|
+
- test/state_transition_sytem_test.rb
|
129
|
+
- test/test_helper.rb
|
data/lib/state_jacket/catalog.rb
DELETED
@@ -1,72 +0,0 @@
|
|
1
|
-
require "delegate"
|
2
|
-
|
3
|
-
module StateJacket
|
4
|
-
|
5
|
-
# A simple class that allows users to intuitively define states and transitions.
|
6
|
-
class Catalog < SimpleDelegator
|
7
|
-
def initialize
|
8
|
-
@inner_hash = {}
|
9
|
-
super inner_hash
|
10
|
-
end
|
11
|
-
|
12
|
-
def add(state)
|
13
|
-
if state.is_a?(Hash)
|
14
|
-
self[state.keys.first.to_s] = state.values.first.map(&:to_s)
|
15
|
-
else
|
16
|
-
self[state.to_s] = nil
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def can_transition?(from_to)
|
21
|
-
from = from_to.keys.first.to_s
|
22
|
-
to = from_to.values.first
|
23
|
-
to = [to] unless to.is_a?(Array)
|
24
|
-
to = to.map(&:to_s)
|
25
|
-
transitions = self[from] || []
|
26
|
-
(to & transitions).length == to.length
|
27
|
-
end
|
28
|
-
|
29
|
-
def transitioners
|
30
|
-
keys.select do |state|
|
31
|
-
self[state] != nil
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def transitioner?(state)
|
36
|
-
transitioners.include?(state.to_s)
|
37
|
-
end
|
38
|
-
|
39
|
-
def terminators
|
40
|
-
keys.select do |state|
|
41
|
-
self[state] == nil
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def terminator?(state)
|
46
|
-
terminators.include?(state.to_s)
|
47
|
-
end
|
48
|
-
|
49
|
-
def lock
|
50
|
-
values.flatten.each do |value|
|
51
|
-
next if value.nil?
|
52
|
-
if !keys.include?(value)
|
53
|
-
raise "Invalid StateJacket::Catalog! [#{value}] is not a first class state."
|
54
|
-
end
|
55
|
-
end
|
56
|
-
inner_hash.freeze
|
57
|
-
values.each { |value| value.freeze unless value.nil? }
|
58
|
-
end
|
59
|
-
|
60
|
-
def supports_state?(state)
|
61
|
-
keys.include?(state.to_s)
|
62
|
-
end
|
63
|
-
|
64
|
-
protected
|
65
|
-
|
66
|
-
attr_reader :inner_hash
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
|
data/test/catalog_test.rb
DELETED
@@ -1,131 +0,0 @@
|
|
1
|
-
require "pry-test"
|
2
|
-
require "coveralls"
|
3
|
-
Coveralls.wear!
|
4
|
-
SimpleCov.command_name "pry-test"
|
5
|
-
require_relative "../lib/state_jacket/catalog"
|
6
|
-
|
7
|
-
class CatalogTest < PryTest::Test
|
8
|
-
before do
|
9
|
-
@catalog = StateJacket::Catalog.new
|
10
|
-
end
|
11
|
-
|
12
|
-
test "add state" do
|
13
|
-
@catalog.add :start
|
14
|
-
assert @catalog.has_key?("start")
|
15
|
-
end
|
16
|
-
|
17
|
-
test "terminators" do
|
18
|
-
@catalog.add :start => [:finish]
|
19
|
-
@catalog.add :finish
|
20
|
-
@catalog.lock
|
21
|
-
assert @catalog.terminators == ["finish"]
|
22
|
-
end
|
23
|
-
|
24
|
-
test "terminator" do
|
25
|
-
@catalog.add :start => [:finish]
|
26
|
-
@catalog.add :finish
|
27
|
-
@catalog.lock
|
28
|
-
assert @catalog.terminator?(:finish)
|
29
|
-
end
|
30
|
-
|
31
|
-
test "transitioners" do
|
32
|
-
@catalog.add :start => [:finish]
|
33
|
-
@catalog.add :finish
|
34
|
-
@catalog.lock
|
35
|
-
assert @catalog.transitioners == ["start"]
|
36
|
-
end
|
37
|
-
|
38
|
-
test "transitioner" do
|
39
|
-
@catalog.add :start => [:finish]
|
40
|
-
@catalog.add :finish
|
41
|
-
@catalog.lock
|
42
|
-
assert @catalog.transitioner?(:start)
|
43
|
-
end
|
44
|
-
|
45
|
-
test "can transition" do
|
46
|
-
@catalog.add :start => [:finish]
|
47
|
-
@catalog.add :finish
|
48
|
-
@catalog.lock
|
49
|
-
assert @catalog.can_transition?(:start => :finish)
|
50
|
-
end
|
51
|
-
|
52
|
-
test "supports state" do
|
53
|
-
@catalog.add :start => [:finish]
|
54
|
-
@catalog.add :finish
|
55
|
-
@catalog.lock
|
56
|
-
assert @catalog.supports_state?(:start)
|
57
|
-
assert @catalog.supports_state?(:finish)
|
58
|
-
end
|
59
|
-
|
60
|
-
test "lock failure" do
|
61
|
-
@catalog.add :start => [:finish]
|
62
|
-
begin
|
63
|
-
@catalog.lock
|
64
|
-
rescue Exception => e
|
65
|
-
end
|
66
|
-
assert e.message.start_with?("Invalid StateJacket::Catalog!")
|
67
|
-
end
|
68
|
-
|
69
|
-
test "lock success" do
|
70
|
-
@catalog.add :start => [:finish]
|
71
|
-
@catalog.add :finish
|
72
|
-
begin
|
73
|
-
@catalog.lock
|
74
|
-
rescue Exception => e
|
75
|
-
end
|
76
|
-
assert e.nil?
|
77
|
-
end
|
78
|
-
|
79
|
-
test "symbol state" do
|
80
|
-
@catalog.add :start => [:finish]
|
81
|
-
@catalog.add :finish
|
82
|
-
assert @catalog.keys.include?("start")
|
83
|
-
assert @catalog.can_transition?(:start => :finish)
|
84
|
-
end
|
85
|
-
|
86
|
-
test "string state" do
|
87
|
-
@catalog.add "start" => ["finish"]
|
88
|
-
@catalog.add "finish"
|
89
|
-
assert @catalog.keys.include?("start")
|
90
|
-
assert @catalog.can_transition?("start" => "finish")
|
91
|
-
end
|
92
|
-
|
93
|
-
test "number state" do
|
94
|
-
@catalog.add 1 => [2]
|
95
|
-
@catalog.add 2
|
96
|
-
assert @catalog.keys.include?("1")
|
97
|
-
assert @catalog.can_transition?(1 => 2)
|
98
|
-
end
|
99
|
-
|
100
|
-
test "turnstyle example" do
|
101
|
-
@catalog.add :open => [:closed, :error]
|
102
|
-
@catalog.add :closed => [:open, :error]
|
103
|
-
@catalog.add :error
|
104
|
-
@catalog.lock
|
105
|
-
assert @catalog.transitioners == ["open", "closed"]
|
106
|
-
assert @catalog.terminators == ["error"]
|
107
|
-
assert @catalog.can_transition?(:open => :closed)
|
108
|
-
assert @catalog.can_transition?(:closed => :open)
|
109
|
-
assert @catalog.can_transition?(:error => :open) == false
|
110
|
-
assert @catalog.can_transition?(:error => :closed) == false
|
111
|
-
end
|
112
|
-
|
113
|
-
test "phone call example" do
|
114
|
-
@catalog = StateJacket::Catalog.new
|
115
|
-
@catalog.add :idle => [:dialing]
|
116
|
-
@catalog.add :dialing => [:idle, :connecting]
|
117
|
-
@catalog.add :connecting => [:idle, :busy, :connected]
|
118
|
-
@catalog.add :busy => [:idle]
|
119
|
-
@catalog.add :connected => [:idle]
|
120
|
-
@catalog.lock
|
121
|
-
assert @catalog.transitioners == ["idle", "dialing", "connecting", "busy", "connected"]
|
122
|
-
assert @catalog.terminators == []
|
123
|
-
assert @catalog.can_transition?(:idle => :dialing)
|
124
|
-
assert @catalog.can_transition?(:dialing => [:idle, :connecting])
|
125
|
-
assert @catalog.can_transition?(:connecting => [:idle, :busy, :connected])
|
126
|
-
assert @catalog.can_transition?(:busy => :idle)
|
127
|
-
assert @catalog.can_transition?(:connected => :idle)
|
128
|
-
assert @catalog.can_transition?(:idle => [:dialing, :connected]) == false
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|