state_jacket 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Lines of Code](http://img.shields.io/badge/lines_of_code-
|
1
|
+
[![Lines of Code](http://img.shields.io/badge/lines_of_code-130-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
|
2
2
|
[![Code Status](http://img.shields.io/codeclimate/github/hopsoft/state_jacket.svg?style=flat)](https://codeclimate.com/github/hopsoft/state_jacket)
|
3
3
|
[![Dependency Status](http://img.shields.io/gemnasium/hopsoft/state_jacket.svg?style=flat)](https://gemnasium.com/hopsoft/state_jacket)
|
4
4
|
[![Build Status](http://img.shields.io/travis/hopsoft/state_jacket.svg?style=flat)](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
|
-
![Turnstyle](https://raw.github.com/hopsoft/state_jacket/master/doc/turnstyle.png)
|
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
|
+
![Turnstyle](https://raw.github.com/hopsoft/state_jacket/master/doc/turnstyle.png)
|
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
|