end_state 0.11.0 → 0.12.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: 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