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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a1c5c0ff06fe58828c8984d5fad9dd05037efe01
4
- data.tar.gz: 920fc4959884ca82671059d467ea32bf7dfdb6fa
3
+ metadata.gz: 2ec65a9c784df47f148a60d0a809321249a7293c
4
+ data.tar.gz: 5146e1e29bf9613094983f5df0648f7804d018ce
5
5
  SHA512:
6
- metadata.gz: 6773afef2dd5fce13ac220f6b412f378a23346dda5794b44e8be28f2ccb28ffa842e9d2daffc8e91c2f105ec0b71ca56dd018db82d5836eb8fcee93416797c1a
7
- data.tar.gz: c3fb9ffb3c98b38f075f4c3d158a6ab547cae5f348f111e48317d971d227d0866441f81cda4d84bad64d2e8b9711986847a3f5af953e466bf515885d0d1ae829
6
+ metadata.gz: af0b443f0965da9896fbbd8928926d0fe8742531c2c0043f92e73f184df5c18d5ddb5d3d35c12e5ea79548d13687e8af04fdb3739224972ac43c42475c8764bf
7
+ data.tar.gz: 03a364ceb0dcbe9a4b0c1b3c3d9a5fd4efbe3569430ca063f4ec762a3ad9f759f71066154f545307e42f7e3f589698926a4d90e69cc3dac576bca911cf85ea9c
data/Gemfile CHANGED
@@ -1,3 +1,2 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
  gemspec
3
-
@@ -1,52 +1,70 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- state_jacket (0.1.1)
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
- coderay (1.1.0)
12
- coveralls (0.7.2)
13
- multi_json (~> 1.3)
14
- rest-client (= 1.6.7)
15
- simplecov (>= 0.7)
16
- term-ansicolor (= 1.2.2)
17
- thor (= 0.18.1)
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
- mime-types (2.4.3)
23
- multi_json (1.10.1)
24
- os (0.9.6)
25
- pry (0.10.1)
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-rescue (1.4.1)
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.1)
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.5.5)
42
+ pry-test (0.6.4)
36
43
  os
37
- rake (10.4.2)
38
- rest-client (1.6.7)
39
- mime-types (>= 1.16)
40
- simplecov (0.9.1)
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
- multi_json (~> 1.0)
43
- simplecov-html (~> 0.8.0)
44
- simplecov-html (0.8.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.2.2)
47
- tins (~> 0.8)
48
- thor (0.18.1)
49
- tins (0.13.2)
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-60-brightgreen.svg?style=flat)](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/)
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
- [State machines](http://en.wikipedia.org/wiki/Finite-state_machine) are awesome
13
- but can be pretty daunting as a system grows.
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
- *The examples below are somewhat contrived, but should clearly illustrate usage.*
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
- #### Define states &amp; transitions for a simple [turnstyle](http://en.wikipedia.org/wiki/Finite-state_machine#Example:_a_turnstile).
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
- Lets model something a bit more complex.
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
- #### Define states &amp; transitions for a phone call.
25
+ ![Turnstyle](https://raw.github.com/hopsoft/state_jacket/master/doc/turnstyle.png)
57
26
 
58
- ![Phone Call](https://raw.github.com/hopsoft/state_jacket/master/doc/phone-call.png)
27
+ ### State Transition System
59
28
 
60
29
  ```ruby
61
- require "state_jacket"
62
-
63
- states = StateJacket::Catalog.new
64
- states.add :idle => [:dialing]
65
- states.add :dialing => [:idle, :connecting]
66
- states.add :connecting => [:idle, :busy, :connected]
67
- states.add :busy => [:idle]
68
- states.add :connected => [:idle]
69
- states.lock
70
-
71
- states.transitioners # => [:idle, :dialing, :connecting, :busy, :connected]
72
- states.terminators # => []
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
- ## Deep Cuts
45
+ ### State Machine
83
46
 
84
- Lets add state awareness and behavior to another class.
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
- require "state_jacket"
89
-
90
- class Turnstyle
91
- attr_reader :states, :current_state
92
-
93
- def initialize
94
- @states = StateJacket::Catalog.new
95
- @states.add :open => [:closed, :error]
96
- @states.add :closed => [:open, :error]
97
- @states.add :error
98
- @states.lock
99
- @current_state = :closed
100
- end
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
- def open
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
- def close
111
- if states.can_transition? current_state => :closed
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
- def break
119
- @current_state = :error
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
- # example usage
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 :default => [:test]
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
-
@@ -1,3 +1,6 @@
1
- require "state_jacket/version"
2
- require "state_jacket/catalog"
1
+ require_relative "./state_jacket/version"
2
+ require_relative "./state_jacket/state_transition_system"
3
+ require_relative "./state_jacket/state_machine"
3
4
 
5
+ module StateJacket
6
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module StateJacket
2
- VERSION = "0.1.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -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
@@ -0,0 +1,5 @@
1
+ require "pry-test"
2
+ require "coveralls"
3
+ Coveralls.wear!
4
+ SimpleCov.command_name "pry-test"
5
+ require_relative "../lib/state_jacket"
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.1.1
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: 2014-12-18 00:00:00.000000000 Z
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: Intuitively define state machine like states and transitions.
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/catalog.rb
96
+ - lib/state_jacket/state_machine.rb
97
+ - lib/state_jacket/state_transition_system.rb
83
98
  - lib/state_jacket/version.rb
84
- - test/catalog_test.rb
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.2.2
122
+ rubygems_version: 2.6.11
106
123
  signing_key:
107
124
  specification_version: 4
108
- summary: Intuitively define state machine like states and transitions.
125
+ summary: A simple & intuitive state machine
109
126
  test_files:
110
- - test/catalog_test.rb
111
- has_rdoc:
127
+ - test/state_machine_test.rb
128
+ - test/state_transition_sytem_test.rb
129
+ - test/test_helper.rb
@@ -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
-
@@ -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