end_state 0.11.0 → 0.12.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: fd8b390f8750c3af0948f56fa4e7471f5d4dfce8
4
- data.tar.gz: ed30307b64e15e69829f25f61dcd883f3ce42fc2
3
+ metadata.gz: b964eec779e3cb4ab2214bfaf0da1b048f4e9b43
4
+ data.tar.gz: c48a186f6bf7229369818e1faf01a46d287dcee3
5
5
  SHA512:
6
- metadata.gz: d00715edd410e6dcffba526a15a5a5ad9475ce70665363423751a08b0a70912024b8c433126c8e11231b045d7278ee00821f3afb8845f1e3b578aaf56f01cad2
7
- data.tar.gz: f4139a6f1fca2ca85e3883885d48106f293d0e7605060863cfd4ca6b7b350f24bfc8d10f6b65786329ebd6338c669e11bd9de2f34bc0f22c359c2eb08f7d3591
6
+ metadata.gz: aca9ba0ae25ef8eb95c988a157ebe487681225feef8785605c2b7414b01725f5b2b733b3fee830b26d65135ccf75e67bb47db6da411b679984ec46dc815f9bc7
7
+ data.tar.gz: 8b92ab95a97116056c9aa2331410ebf0ba1a29e641e73345ce5cc9c33ba83cd5e110b4752a5eaab3cf244a7137a39288f01aaf1df670f76dd837fa9eae92ac7a
data/README.md CHANGED
@@ -26,12 +26,15 @@ Or install it yourself as:
26
26
  ## StateMachine
27
27
 
28
28
  Create a state machine by subclassing `EndState::StateMachine`.
29
+ Transitions can be named by adding an `:as` option.
29
30
 
30
31
  ```ruby
31
32
  class Machine < EndState::StateMachine
32
- transition a: :b, as: :go
33
- transition b: :c
34
- transition [:b, :c] => :a
33
+ transition parked: :idling, as: :start
34
+ transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear, as: :shift_up
35
+ transition third_gear: :second_gear, second_gear: :first_gear, as: shift_down
36
+ transition first_gear: :idling, as: idle
37
+ transition [:idling, :first_gear] => :parked, as: :park
35
38
  end
36
39
  ```
37
40
 
@@ -46,22 +49,24 @@ class StatefulObject
46
49
  end
47
50
  end
48
51
 
49
- machine = Machine.new(StatefulObject.new(:a))
50
-
51
- machine.transition :b # => true
52
- machine.state # => :b
53
- machine.b? # => true
54
- machine.transition :c # => true
55
- machine.state # => :c
56
- machine.can_transition? :b # => false
57
- machine.can_transition? :a # => true
58
- machine.transition :b # => false
59
- machine.transition! :b # => raises InvalidTransition
60
- machine.transition :a # => true
61
- machine.state # => :a
62
- machine.go # => true
63
- machine.state # => :b
64
- machine.go! # => raises InvalidTransition
52
+ machine = Machine.new(StatefulObject.new(:parked))
53
+
54
+ machine.transition :idling # => true
55
+ machine.state # => :idling
56
+ machine.idling? # => true
57
+ machine.transition :first_gear # => true
58
+ machine.transition :second_gear # => true
59
+ machine.transition :third_gear # => true
60
+ machine.state # => :third_gear
61
+ machine.can_transition? :first_gear # => false
62
+ machine.can_transition? :second_gear # => true
63
+ machine.transition :first_gear # => false
64
+ machine.transition! :first_gear # => raises InvalidTransition
65
+ machine.shift_down # => true
66
+ machine.shift_up # => true
67
+ machine.state # => :third_gear
68
+ machine.park # => false
69
+ machine.park! # => raises InvalidTransition
65
70
  ```
66
71
 
67
72
  ## Initial State
@@ -82,19 +87,19 @@ the machine to transition to the new state specified from any actual state.
82
87
 
83
88
  ```ruby
84
89
  class Machine < EndState::StateMachine
85
- transition a: :b
86
- transition b: :c
87
- transition any_state: :d
90
+ transition parked: :idling
91
+ transition idling: :first_gear
92
+ transition any_state: :crashed
88
93
  end
89
94
 
90
- machine = Machine.new(StatefulObject.new(:a))
91
- machine.transition :d # true
92
- machine.state # :d
95
+ machine = Machine.new(StatefulObject.new(:parked))
96
+ machine.transition :crashed # true
97
+ machine.state # :crashed
93
98
 
94
- machine = Machine.new(StatefulObject.new(:a))
95
- machine.transition :b # true
96
- machine.transition :d # true
97
- machine.state # :d
99
+ machine = Machine.new(StatefulObject.new(:parked))
100
+ machine.transition :idling # true
101
+ machine.transition :crashed # true
102
+ machine.state # :crashed
98
103
  ```
99
104
 
100
105
  ## Guards
@@ -4,4 +4,5 @@ module EndState
4
4
  class InvalidTransition < Error; end
5
5
  class GuardFailed < Error; end
6
6
  class ConcluderFailed < Error; end
7
+ class EventConflict < Error; end
7
8
  end
@@ -35,17 +35,20 @@ module EndState
35
35
  end
36
36
 
37
37
  def self.transition(state_map)
38
- initial_states = Array(state_map.keys.first)
39
- final_state = state_map.values.first
40
- transition_alias = state_map[:as] if state_map.keys.length > 1
41
- transition = Transition.new(final_state)
42
- initial_states.each do |state|
43
- transitions[{ state.to_sym => final_state.to_sym }] = transition
44
- end
45
- unless transition_alias.nil?
46
- events[transition_alias.to_sym] = initial_states.map { |s| { s.to_sym => final_state.to_sym } }
38
+ transition_alias = state_map.delete(:as)
39
+ transition_alias = transition_alias.to_sym unless transition_alias.nil?
40
+
41
+ state_map.each do |start_states, end_state|
42
+ transition = Transition.new(end_state)
43
+
44
+ Array(start_states).each do |start_state|
45
+ state_mapping = StateMapping[start_state.to_sym => end_state.to_sym]
46
+ transitions[state_mapping] = transition
47
+ __sm_add_event(transition_alias, state_mapping) unless transition_alias.nil?
48
+ end
49
+
50
+ yield transition if block_given?
47
51
  end
48
- yield transition if block_given?
49
52
  end
50
53
 
51
54
  def self.transitions
@@ -66,11 +69,11 @@ module EndState
66
69
  end
67
70
 
68
71
  def self.start_states
69
- transitions.keys.map { |state_map| state_map.keys.first }.uniq
72
+ transitions.keys.map(&:start_state).uniq
70
73
  end
71
74
 
72
75
  def self.end_states
73
- transitions.keys.map { |state_map| state_map.values.first }.uniq
76
+ transitions.keys.map(&:end_state).uniq
74
77
  end
75
78
 
76
79
  def object
@@ -143,11 +146,12 @@ module EndState
143
146
  end
144
147
 
145
148
  def __sm_state_for_event(event, mode)
146
- transitions = self.class.events[event]
147
- return false unless transitions
148
- start_states = transitions.map { |t| t.keys.first }
149
- return __sm_invalid_event(event, mode) unless start_states.include?(state.to_sym) || start_states.include?(:any_state)
150
- transitions.first.values.first
149
+ state_mappings = self.class.events[event]
150
+ return false unless state_mappings
151
+ state_mappings.each do |state_mapping|
152
+ return state_mapping.end_state if state_mapping.matches_start_state?(state.to_sym)
153
+ end
154
+ return __sm_invalid_event(event, mode)
151
155
  end
152
156
 
153
157
  def __sm_invalid_event(event, mode)
@@ -179,5 +183,20 @@ module EndState
179
183
  return false unless mode == :hard
180
184
  fail ConcluderFailed, "The transition to #{state} was rolled back: #{failure_messages.join(', ')}"
181
185
  end
186
+
187
+ def self.__sm_add_event(event, state_mapping)
188
+ events[event] ||= []
189
+ conflicting_mapping = events[event].find{ |sm| sm.conflicts?(state_mapping) }
190
+ if conflicting_mapping
191
+ message =
192
+ "Attempting to define :#{event} as transitioning from " \
193
+ ":#{state_mapping.start_state} => :#{state_mapping.end_state} when " \
194
+ ":#{conflicting_mapping.start_state} => :#{conflicting_mapping.end_state} already exists. " \
195
+ "You cannot define multiple transitions from a single state with the same event name."
196
+
197
+ fail EventConflict, message
198
+ end
199
+ events[event] << state_mapping
200
+ end
182
201
  end
183
202
  end
@@ -0,0 +1,25 @@
1
+ module EndState
2
+ class StateMapping < Hash
3
+ def start_state
4
+ keys.first
5
+ end
6
+
7
+ def end_state
8
+ values.first
9
+ end
10
+
11
+ def any_start_state?
12
+ start_state == :any_state
13
+ end
14
+
15
+ def matches_start_state?(state)
16
+ start_state == state || any_start_state?
17
+ end
18
+
19
+ def conflicts?(state_mapping)
20
+ start_state == state_mapping.start_state ||
21
+ any_start_state? ||
22
+ state_mapping.any_start_state?
23
+ end
24
+ end
25
+ end
@@ -1,3 +1,3 @@
1
1
  module EndState
2
- VERSION = '0.11.0'
2
+ VERSION = '0.12.0'
3
3
  end
data/lib/end_state.rb CHANGED
@@ -6,8 +6,10 @@ require 'end_state/guard'
6
6
  require 'end_state/concluder'
7
7
  require 'end_state/concluders'
8
8
  require 'end_state/transition'
9
+ require 'end_state/state_mapping'
9
10
  require 'end_state/action'
10
11
  require 'end_state/state_machine'
12
+
11
13
  begin
12
14
  require 'graphviz'
13
15
  require 'end_state/graph'
@@ -14,26 +14,102 @@ module EndState
14
14
  end
15
15
 
16
16
  describe '.transition' do
17
- let(:state_map) { { a: :b } }
18
- let(:yielded) { OpenStruct.new(transition: nil) }
19
- before { StateMachine.transition(state_map) { |transition| yielded.transition = transition } }
17
+ let(:options) { { a: :b } }
20
18
 
21
- it 'yields a transition for the supplied end state' do
22
- expect(yielded.transition.state).to eq :b
19
+ before do
20
+ @transitions = []
21
+ StateMachine.transition(options) { |transition| @transitions << transition }
23
22
  end
24
23
 
25
24
  it 'does not require a block' do
26
- expect { StateMachine.transition(b: :c) }.not_to raise_error
25
+ expect { StateMachine.transition(options) }.not_to raise_error
26
+ end
27
+
28
+ context 'single transition' do
29
+ it 'yields a transition for the supplied end state' do
30
+ expect(@transitions.count).to eq 1
31
+ expect(@transitions[0].state).to eq :b
32
+ end
33
+
34
+ it 'adds the transition to the state machine' do
35
+ expect(StateMachine.transitions[a: :b]).to eq @transitions[0]
36
+ end
37
+
38
+ context 'with as' do
39
+ let(:options) { { a: :b, as: :go } }
40
+
41
+ it 'creates an alias' do
42
+ expect(StateMachine.events[:go]).to eq [{ a: :b }]
43
+ end
44
+
45
+ context 'another single transition with as' do
46
+ before { StateMachine.transition({c: :d, as: :go}) }
47
+
48
+ it 'appends to the event' do
49
+ expect(StateMachine.events[:go]).to eq [{ a: :b }, { c: :d }]
50
+ end
51
+ end
52
+
53
+ context 'another single transition with as that conflicts' do
54
+ it 'raises an error' do
55
+ expect{ StateMachine.transition({a: :c, as: :go}) }.to raise_error EventConflict,
56
+ 'Attempting to define :go as transitioning from :a => :c when :a => :b already exists. ' \
57
+ 'You cannot define multiple transitions from a single state with the same event name.'
58
+ end
59
+ end
60
+
61
+ context 'another single transition with as that conflicts' do
62
+ it 'raises an error' do
63
+ expect{ StateMachine.transition({any_state: :c, as: :go}) }.to raise_error EventConflict,
64
+ 'Attempting to define :go as transitioning from :any_state => :c when :a => :b already exists. ' \
65
+ 'You cannot define multiple transitions from a single state with the same event name.'
66
+ end
67
+ end
68
+ end
27
69
  end
28
70
 
29
- it 'adds the transition to the state machine' do
30
- expect(StateMachine.transitions[state_map]).to eq yielded.transition
71
+ context 'multiple start states' do
72
+ let(:options) { { [:a, :b] => :c } }
73
+
74
+ it 'yields each transition for the supplied end state' do
75
+ expect(@transitions.count).to eq 1
76
+ expect(@transitions[0].state).to eq :c
77
+ end
78
+
79
+ it 'adds the transitions to the state machine' do
80
+ expect(StateMachine.transitions[a: :c]).to eq @transitions[0]
81
+ expect(StateMachine.transitions[b: :c]).to eq @transitions[0]
82
+ end
83
+
84
+ context 'with as' do
85
+ let(:options) { { [:a, :b] => :c, as: :go } }
86
+
87
+ it 'creates an alias' do
88
+ expect(StateMachine.events[:go]).to eq [{ a: :c }, { b: :c }]
89
+ end
90
+ end
31
91
  end
32
92
 
33
- context 'when the :as option is used' do
34
- it 'creates an alias' do
35
- StateMachine.transition(state_map.merge(as: :go))
36
- expect(StateMachine.events[:go]).to eq [{ a: :b }]
93
+ context 'multiple transitions' do
94
+ let(:options) { { a: :b, c: :d } }
95
+
96
+ it 'yields each transition for the supplied end state' do
97
+ expect(@transitions.count).to eq 2
98
+ expect(@transitions[0].state).to eq :b
99
+ expect(@transitions[1].state).to eq :d
100
+ end
101
+
102
+ it 'adds the transitions to the state machine' do
103
+ expect(StateMachine.transitions[a: :b]).to eq @transitions[0]
104
+ expect(StateMachine.transitions[c: :d]).to eq @transitions[1]
105
+ end
106
+
107
+ context 'with as' do
108
+ let(:options) { { a: :b, c: :d, as: :go } }
109
+
110
+ it 'creates an alias' do
111
+ expect(StateMachine.events[:go]).to eq [{ a: :b }, { c: :d }]
112
+ end
37
113
  end
38
114
  end
39
115
  end
@@ -172,59 +248,123 @@ module EndState
172
248
 
173
249
  describe '#{event}' do
174
250
  let(:object) { OpenStruct.new(state: :a) }
175
- before do
176
- StateMachine.transition a: :b, as: :go do |t|
177
- t.blocked 'Invalid event!'
251
+
252
+ context 'single transition' do
253
+ before do
254
+ StateMachine.transition a: :b, as: :go do |t|
255
+ t.blocked 'Invalid event!'
256
+ end
178
257
  end
179
- end
180
258
 
181
- it 'transitions the state' do
182
- machine.go
183
- expect(machine.state).to eq :b
184
- end
259
+ it 'transitions the state' do
260
+ machine.go
261
+ expect(machine.state).to eq :b
262
+ end
185
263
 
186
- it 'accepts params' do
187
- allow(machine).to receive(:transition)
188
- machine.go foo: 'bar', bar: 'foo'
189
- expect(machine).to have_received(:transition).with(:b, { foo: 'bar', bar: 'foo' }, :soft)
190
- end
264
+ it 'accepts params' do
265
+ allow(machine).to receive(:transition)
266
+ machine.go foo: 'bar', bar: 'foo'
267
+ expect(machine).to have_received(:transition).with(:b, { foo: 'bar', bar: 'foo' }, :soft)
268
+ end
191
269
 
192
- it 'defaults params to {}' do
193
- allow(machine).to receive(:transition)
194
- machine.go
195
- expect(machine).to have_received(:transition).with(:b, {}, :soft)
270
+ it 'defaults params to {}' do
271
+ allow(machine).to receive(:transition)
272
+ machine.go
273
+ expect(machine).to have_received(:transition).with(:b, {}, :soft)
274
+ end
275
+
276
+ context 'when the intial state is :c' do
277
+ let(:object) { OpenStruct.new(state: :c) }
278
+
279
+ it 'blocks invalid events' do
280
+ machine.go
281
+ expect(machine.state).to eq :c
282
+ end
283
+
284
+ it 'adds a failure message specified by blocked' do
285
+ machine.go
286
+ expect(machine.failure_messages).to eq ['Invalid event!']
287
+ end
288
+
289
+ context 'and all transitions are forced to run in :hard mode' do
290
+ before { machine.class.treat_all_transitions_as_hard! }
291
+
292
+ it 'raises an InvalidTransition error' do
293
+ expect { machine.go }.to raise_error(InvalidTransition)
294
+ end
295
+ end
296
+ end
297
+
298
+ context 'when using any_state with an event' do
299
+ before do
300
+ StateMachine.transition any_state: :end, as: :jump_to_end
301
+ end
302
+
303
+ it 'transitions the state to :end' do
304
+ machine.jump_to_end
305
+ expect(machine.state).to eq :end
306
+ end
307
+ end
196
308
  end
197
309
 
198
- context 'when the intial state is :c' do
199
- let(:object) { OpenStruct.new(state: :c) }
310
+ context 'multiple start states' do
311
+ before do
312
+ StateMachine.transition [:a, :b] => :c, as: :go do |t|
313
+ t.blocked 'Invalid event!'
314
+ end
315
+ end
200
316
 
201
- it 'blocks invalid events' do
202
- machine.go
203
- expect(machine.state).to eq :c
317
+ context 'initial state is :a' do
318
+ let(:object) { OpenStruct.new(state: :a) }
319
+
320
+ it 'transitions the state' do
321
+ machine.go
322
+ expect(machine.state).to eq :c
323
+ end
204
324
  end
205
325
 
206
- it 'adds a failure message specified by blocked' do
207
- machine.go
208
- expect(machine.failure_messages).to eq ['Invalid event!']
326
+ context 'initial state is :b' do
327
+ let(:object) { OpenStruct.new(state: :b) }
328
+
329
+ it 'transitions the state' do
330
+ machine.go
331
+ expect(machine.state).to eq :c
332
+ end
209
333
  end
210
334
 
211
- context 'and all transitions are forced to run in :hard mode' do
212
- before { machine.class.treat_all_transitions_as_hard! }
335
+ context 'initial state is :d' do
336
+ let(:object) { OpenStruct.new(state: :b) }
213
337
 
214
- it 'raises an InvalidTransition error' do
215
- expect { machine.go }.to raise_error(InvalidTransition)
338
+ it 'transitions the state' do
339
+ machine.go
340
+ expect(machine.state).to eq :c
216
341
  end
217
342
  end
218
343
  end
219
344
 
220
- context 'when using any_state with an event' do
345
+ context 'multiple transitions' do
221
346
  before do
222
- StateMachine.transition any_state: :end, as: :jump_to_end
347
+ StateMachine.transition a: :b, c: :d, as: :go do |t|
348
+ t.blocked 'Invalid event!'
349
+ end
223
350
  end
224
351
 
225
- it 'transitions the state to :end' do
226
- machine.jump_to_end
227
- expect(machine.state).to eq :end
352
+ context 'initial state is :a' do
353
+ let(:object) { OpenStruct.new(state: :a) }
354
+
355
+ it 'transitions the state' do
356
+ machine.go
357
+ expect(machine.state).to eq :b
358
+ end
359
+ end
360
+
361
+ context 'initial state is :b' do
362
+ let(:object) { OpenStruct.new(state: :c) }
363
+
364
+ it 'transitions the state' do
365
+ machine.go
366
+ expect(machine.state).to eq :d
367
+ end
228
368
  end
229
369
  end
230
370
  end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module EndState
5
+ describe StateMapping do
6
+ describe 'simple example' do
7
+ let(:subject) { StateMapping[a: :b] }
8
+
9
+ context '#start_state' do
10
+ it 'returns the first key' do
11
+ expect(subject.start_state).to eq :a
12
+ end
13
+ end
14
+
15
+ context '#end_state' do
16
+ it 'returns the first value' do
17
+ expect(subject.end_state).to eq :b
18
+ end
19
+ end
20
+
21
+ context '#any_start_state?' do
22
+ context 'start_state is :any_state' do
23
+ let(:subject) { StateMapping[any_state: :b] }
24
+ it 'returns true' do
25
+ expect(subject.any_start_state?).to eq true
26
+ end
27
+ end
28
+
29
+ context 'start_state is anything else' do
30
+ it 'returns false' do
31
+ expect(subject.any_start_state?).to eq false
32
+ end
33
+ end
34
+ end
35
+
36
+ context '#matches_start_state?' do
37
+ context 'same start_state' do
38
+ it 'returns true' do
39
+ expect(subject.matches_start_state?(:a)).to eq true
40
+ end
41
+ end
42
+
43
+ context 'different start_state' do
44
+ it 'returns false' do
45
+ expect(subject.matches_start_state?(:b)).to eq false
46
+ end
47
+ end
48
+
49
+ context 'object has a start_state of :any_state' do
50
+ let(:subject) { StateMapping[any_state: :c] }
51
+
52
+ it 'returns true' do
53
+ expect(subject.matches_start_state?(:b)).to eq true
54
+ end
55
+ end
56
+ end
57
+
58
+ context '#conflicts?' do
59
+ context 'same start_state' do
60
+ let(:other) { StateMapping[a: :c] }
61
+
62
+ it 'returns true' do
63
+ expect(subject.conflicts?(other)).to eq true
64
+ end
65
+ end
66
+
67
+ context 'different start_state' do
68
+ let(:other) { StateMapping[c: :d] }
69
+
70
+ it 'returns false' do
71
+ expect(subject.conflicts?(other)).to eq false
72
+ end
73
+ end
74
+
75
+ context 'argument has a start_state of :any_state' do
76
+ let(:other) { StateMapping[any_state: :c] }
77
+
78
+ it 'returns true' do
79
+ expect(subject.conflicts?(other)).to eq true
80
+ end
81
+ end
82
+
83
+ context 'object has a start_state of :any_state' do
84
+ let(:subject) { StateMapping[any_state: :c] }
85
+ let(:other) { StateMapping[a: :c] }
86
+
87
+ it 'returns true' do
88
+ expect(subject.conflicts?(other)).to eq true
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: end_state
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alexpeachey
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-08-21 00:00:00.000000000 Z
11
+ date: 2014-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -95,6 +95,7 @@ files:
95
95
  - lib/end_state/guard.rb
96
96
  - lib/end_state/messages.rb
97
97
  - lib/end_state/state_machine.rb
98
+ - lib/end_state/state_mapping.rb
98
99
  - lib/end_state/transition.rb
99
100
  - lib/end_state/version.rb
100
101
  - lib/end_state_matchers.rb
@@ -104,6 +105,7 @@ files:
104
105
  - spec/end_state/concluders/persistence_spec.rb
105
106
  - spec/end_state/guard_spec.rb
106
107
  - spec/end_state/state_machine_spec.rb
108
+ - spec/end_state/state_mapping_spec.rb
107
109
  - spec/end_state/transition_spec.rb
108
110
  - spec/end_state_spec.rb
109
111
  - spec/spec_helper.rb
@@ -137,6 +139,7 @@ test_files:
137
139
  - spec/end_state/concluders/persistence_spec.rb
138
140
  - spec/end_state/guard_spec.rb
139
141
  - spec/end_state/state_machine_spec.rb
142
+ - spec/end_state/state_mapping_spec.rb
140
143
  - spec/end_state/transition_spec.rb
141
144
  - spec/end_state_spec.rb
142
145
  - spec/spec_helper.rb