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 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