end_state 0.12.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,83 @@
1
+ module EndState
2
+ class TransitionConfigurationSet
3
+
4
+ def initialize
5
+ @end_state_map = { any_state: {} } # [start_state][event] = end_state
6
+ @configuration_map = { any_state: {} } # [start_state][end_state] = configuration
7
+ end
8
+
9
+ def add(start_state, end_state, configuration, event = nil)
10
+ if event
11
+ end_state_map[start_state] ||= {}
12
+ end_state_map[start_state][event] = end_state
13
+ end
14
+
15
+ configuration_map[start_state] ||= {}
16
+ configuration_map[start_state][end_state] = configuration
17
+ end
18
+
19
+ def get_configuration(start_state, end_state)
20
+ local_map = configuration_map[start_state] || {}
21
+ local_map[end_state] || configuration_map[:any_state][end_state]
22
+ end
23
+
24
+ def get_end_state(start_state, event)
25
+ local_map = end_state_map[start_state] || {}
26
+ local_map[event] || end_state_map[:any_state][event]
27
+ end
28
+
29
+ def start_states
30
+ states = configuration_map.keys
31
+ states.delete(:any_state)
32
+ states += end_states unless configuration_map[:any_state].empty?
33
+ states.uniq
34
+ end
35
+
36
+ def end_states
37
+ configuration_map.map { |_, v| v.keys }.flatten.uniq
38
+ end
39
+
40
+ def events
41
+ end_state_map.map { |_, v| v.keys }.flatten.uniq
42
+ end
43
+
44
+ def event_conflicts?(start_state, event)
45
+ !!get_end_state(start_state, event) || (start_state == :any_state && events.include?(event))
46
+ end
47
+
48
+ def each &block
49
+ all_transitions.each(&block)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :configuration_map, :end_state_map
55
+
56
+ def all_transitions
57
+ all_start_states = start_states
58
+
59
+ configuration_map.map do |start_state, local_config|
60
+ states = (start_state == :any_state) ? all_start_states : [start_state]
61
+ states.map { |s| transitions_for s, local_config }
62
+ end.flatten(2)
63
+ end
64
+
65
+ def transitions_for start_state, local_map
66
+ local_map.map do |end_state, config|
67
+ [start_state, end_state, config, event_for(start_state, end_state)]
68
+ end
69
+ end
70
+
71
+ def event_for start_state, end_state
72
+ (end_state_map[start_state] || {}).each do |k, v|
73
+ return k if v == end_state
74
+ end
75
+
76
+ end_state_map[:any_state].each do |k, v|
77
+ return k if v == end_state
78
+ end
79
+
80
+ nil
81
+ end
82
+ end
83
+ end
@@ -1,3 +1,3 @@
1
1
  module EndState
2
- VERSION = '0.12.0'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/end_state.rb CHANGED
@@ -6,14 +6,17 @@ 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
+ require 'end_state/transition_configuration'
10
+ require 'end_state/transition_configuration_set'
10
11
  require 'end_state/action'
12
+ require 'end_state/state_machine_configuration'
11
13
  require 'end_state/state_machine'
12
14
 
13
15
  begin
14
16
  require 'graphviz'
15
17
  require 'end_state/graph'
16
18
  rescue LoadError
19
+ require 'end_state/no_graph'
17
20
  end
18
21
 
19
22
  module EndState
@@ -4,14 +4,15 @@ module EndStateMatchers
4
4
  end
5
5
 
6
6
  class TransitionMatcher
7
- attr_reader :transition, :machine, :failure_messages, :guards, :concluders, :required_params
7
+ attr_reader :start_state, :end_state, :event, :machine, :failure_messages, :guards, :concluders, :required_params
8
8
 
9
9
  def initialize(transition)
10
- @transition = transition
11
- @failure_messages = []
10
+ @start_state, @end_state = transition.first
11
+ @event = nil
12
12
  @guards = []
13
13
  @concluders = []
14
14
  @required_params = []
15
+ @failure_messages = []
15
16
  end
16
17
 
17
18
  def matches?(actual)
@@ -24,11 +25,11 @@ module EndStateMatchers
24
25
  end
25
26
 
26
27
  def description
27
- "have transition :#{transition.keys.first} => :#{transition.values.first}"
28
+ "have transition #{start_state} => #{end_state}"
28
29
  end
29
30
 
30
- def with_guard(guard)
31
- @guards << guard
31
+ def with_event(event)
32
+ @event = event
32
33
  self
33
34
  end
34
35
 
@@ -37,11 +38,6 @@ module EndStateMatchers
37
38
  self
38
39
  end
39
40
 
40
- def with_concluder(concluder)
41
- @concluders << concluder
42
- self
43
- end
44
-
45
41
  def with_concluders(*concluders)
46
42
  @concluders += Array(concluders).flatten
47
43
  self
@@ -52,6 +48,10 @@ module EndStateMatchers
52
48
  self
53
49
  end
54
50
 
51
+ alias_method :with_guard, :with_guards
52
+ alias_method :with_concluder, :with_concluders
53
+ alias_method :with_required_param, :with_required_params
54
+
55
55
  # Backward compatibility
56
56
  # Finalizer is deprecated
57
57
  alias_method :with_finalizer, :with_concluder
@@ -60,50 +60,63 @@ module EndStateMatchers
60
60
 
61
61
  private
62
62
 
63
+ def transition_configuration
64
+ @tc = machine.transition_configurations.get_configuration(start_state, end_state)
65
+ end
66
+
67
+ def has_event?(event)
68
+ machine.transition_configurations.get_end_state(start_state, event) == end_state
69
+ end
70
+
71
+ def has_guard?(guard)
72
+ transition_configuration.guards.include?(guard)
73
+ end
74
+
75
+ def has_concluder?(concluder)
76
+ transition_configuration.concluders.include?(concluder)
77
+ end
78
+
79
+ def has_required_param?(param)
80
+ transition_configuration.required_params.include?(param)
81
+ end
82
+
83
+ def add_failure(suffix)
84
+ failure_messages << "expected transition #{start_state} => #{end_state} #{suffix}"
85
+ end
86
+
63
87
  def verify
64
- result = true
65
- if machine.transitions.keys.include? transition
66
- result = (result && verify_guards) if guards.any?
67
- result = (result && verify_concluders) if concluders.any?
68
- result = (result && verify_required_params) if required_params.any?
69
- result
88
+ if transition_configuration.nil?
89
+ add_failure('to be defined')
70
90
  else
71
- failure_messages << "expected that #{machine.name} would have transition :#{transition.keys.first} => :#{transition.values.first}"
72
- false
91
+ verify_event if event
92
+ verify_guards
93
+ verify_concluders
94
+ verify_required_params
73
95
  end
96
+
97
+ failure_messages.empty?
98
+ end
99
+
100
+ def verify_event
101
+ add_failure("to have event name: #{event}") unless has_event?(event)
74
102
  end
75
103
 
76
104
  def verify_guards
77
- result = true
78
- guards.each do |guard|
79
- unless machine.transitions[transition].guards.any? { |g| g == guard }
80
- failure_messages << "expected that transition :#{transition.keys.first} => :#{transition.values.first} would have guard #{guard.name}"
81
- result = false
82
- end
105
+ guards.map do |guard|
106
+ add_failure("to have guard #{guard.name}") unless has_guard?(guard)
83
107
  end
84
- result
85
108
  end
86
109
 
87
110
  def verify_concluders
88
- result = true
89
- concluders.each do |concluder|
90
- unless machine.transitions[transition].concluders.any? { |f| f == concluder }
91
- failure_messages << "expected that transition :#{transition.keys.first} => :#{transition.values.first} would have concluder #{concluder.name}"
92
- result = false
93
- end
111
+ concluders.map do |concluder|
112
+ add_failure("to have concluder #{concluder.name}") unless has_concluder?(concluder)
94
113
  end
95
- result
96
114
  end
97
115
 
98
116
  def verify_required_params
99
- result = true
100
117
  required_params.each do |param|
101
- unless machine.transitions[transition].required_params.any? { |p| p == param }
102
- failure_messages << "expected that transition :#{transition.keys.first} => :#{transition.values.first} would have required param #{param}"
103
- result = false
104
- end
118
+ add_failure("to have required param #{param}") unless has_required_param?(param)
105
119
  end
106
- result
107
120
  end
108
121
  end
109
122
  end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  module EndState
4
4
  describe Concluder do
5
5
  subject(:concluder) { Concluder.new(object, state, params) }
6
- let(:object) { Struct.new('Machine', :failure_messages, :success_messages, :state, :store_states_as_strings).new }
6
+ let(:object) { OpenStruct.new(failure_messages: [], success_messages: []) }
7
7
  let(:state) { :a }
8
8
  let(:params) { {} }
9
9
  before do
@@ -20,8 +20,14 @@ module EndState
20
20
 
21
21
  describe '#add_success' do
22
22
  it 'adds an success' do
23
- concluder.add_error('success')
24
- expect(object.failure_messages).to eq ['success']
23
+ concluder.add_success('success')
24
+ expect(object.success_messages).to eq ['success']
25
+ end
26
+ end
27
+
28
+ describe 'call' do
29
+ it 'returns false' do
30
+ expect(concluder.call).to be false
25
31
  end
26
32
  end
27
33
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe EndState::Graph do
4
+ class TestMachine < EndState::StateMachine
5
+ transition a: :b
6
+ transition b: :c
7
+ transition c: :d, as: :go
8
+ transition any_state: :a
9
+ transition any_state: :e, as: :exit
10
+ end
11
+
12
+ subject(:graph) { EndState::Graph.new(TestMachine) }
13
+
14
+ describe '#draw' do
15
+ let(:description) { graph.draw.to_s }
16
+
17
+ it 'contains all the nodes' do
18
+ ['a [label = "a"];', 'b [label = "b"];', 'c [label = "c"];', 'd [label = "d"];', 'e [label = "e"];'].each do |s|
19
+ expect(description).to include(s)
20
+ end
21
+ end
22
+
23
+ it 'contains all the edges without labels' do
24
+ ['a -> b;', 'b -> c;', 'a -> a;', 'b -> a;', 'c -> a;', 'd -> a;', 'e -> a;'].each do |s|
25
+ expect(description).to include(s)
26
+ end
27
+ end
28
+
29
+ it 'contains all the edges with labels' do
30
+ ['c -> d [label = "go"];', 'a -> e [label = "exit"];', 'b -> e [label = "exit"];', 'c -> e [label = "exit"];', 'd -> e [label = "exit"];', 'e -> e [label = "exit"];'].each do |s|
31
+ expect(description).to include(s)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  module EndState
4
4
  describe Guard do
5
5
  subject(:guard) { Guard.new(object, state, params) }
6
- let(:object) { Struct.new('Machine', :failure_messages, :success_messages, :state, :store_states_as_strings).new }
6
+ let(:object) { OpenStruct.new(failure_messages: [], success_messages: []) }
7
7
  let(:state) { :a }
8
8
  let(:params) { {} }
9
9
  before do
@@ -20,8 +20,34 @@ module EndState
20
20
 
21
21
  describe '#add_success' do
22
22
  it 'adds an success' do
23
- guard.add_error('success')
24
- expect(object.failure_messages).to eq ['success']
23
+ guard.add_success('success')
24
+ expect(object.success_messages).to eq ['success']
25
+ end
26
+ end
27
+
28
+ describe 'will_allow?' do
29
+ it 'returns false' do
30
+ expect(guard.will_allow?).to be false
31
+ end
32
+ end
33
+
34
+ describe 'allowed?' do
35
+ context 'will_allow? returns true' do
36
+ before { allow(guard).to receive(:will_allow?).and_return(true) }
37
+
38
+ it 'calls passed and returns true' do
39
+ expect(guard).to receive(:passed)
40
+ expect(guard.allowed?).to be true
41
+ end
42
+ end
43
+
44
+ context 'will_allow? returns false' do
45
+ before { allow(guard).to receive(:will_allow?).and_return(false) }
46
+
47
+ it 'calls failed and returns false' do
48
+ expect(guard).to receive(:failed)
49
+ expect(guard.allowed?).to be false
50
+ end
25
51
  end
26
52
  end
27
53
  end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module EndState
5
+ describe StateMachineConfiguration do
6
+ subject(:machine) { StateMachine.new(object) }
7
+ let(:object) { OpenStruct.new(state: nil) }
8
+ before do
9
+ StateMachine.instance_variable_set '@transition_configurations'.to_sym, nil
10
+ StateMachine.instance_variable_set '@events'.to_sym, nil
11
+ StateMachine.instance_variable_set '@store_states_as_strings'.to_sym, nil
12
+ StateMachine.instance_variable_set '@initial_state'.to_sym, :__nil__
13
+ StateMachine.instance_variable_set '@mode'.to_sym, :soft
14
+ end
15
+
16
+ describe '.transition' do
17
+ let(:options) { { a: :b } }
18
+
19
+ before do
20
+ @transition_configuration = nil
21
+ StateMachine.transition(options) { |tc| @transition_configuration = tc }
22
+ end
23
+
24
+ it 'does not require a block' do
25
+ expect { StateMachine.transition(options) }.not_to raise_error
26
+ end
27
+
28
+ context 'single transition' do
29
+ it 'yields a transition configuraton' do
30
+ expect(@transition_configuration).to be_a TransitionConfiguration
31
+ end
32
+
33
+ context 'with as' do
34
+ let(:options) { { a: :b, as: :go } }
35
+
36
+ it 'creates an alias' do
37
+ expect(StateMachine).to have_transition(a: :b).with_event(:go)
38
+ end
39
+
40
+ context 'another single transition with as' do
41
+ before { StateMachine.transition({c: :d, as: :go}) }
42
+
43
+ it 'appends to the event' do
44
+ expect(StateMachine).to have_transition(a: :b).with_event(:go)
45
+ expect(StateMachine).to have_transition(c: :d).with_event(:go)
46
+ end
47
+ end
48
+
49
+ context 'another single transition with as that conflicts' do
50
+ it 'raises an error' do
51
+ expect{ StateMachine.transition({a: :c, as: :go}) }.to raise_error EventConflict,
52
+ "Attempting to define event 'go' on state 'a', but it is already defined. " \
53
+ "(Check duplicates and use of 'any_state')"
54
+ end
55
+ end
56
+
57
+ context 'another single transition with as that conflicts' do
58
+ it 'raises an error' do
59
+ expect{ StateMachine.transition({any_state: :c, as: :go}) }.to raise_error EventConflict,
60
+ "Attempting to define event 'go' on state 'any_state', but it is already defined. " \
61
+ "(Check duplicates and use of 'any_state')"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ context 'multiple start states' do
68
+ let(:options) { { [:a, :b] => :c } }
69
+
70
+ it 'yields a transition configuraton' do
71
+ expect(@transition_configuration).to be_a TransitionConfiguration
72
+ end
73
+
74
+ it 'has both transitions' do
75
+ expect(StateMachine).to have_transition(a: :c)
76
+ expect(StateMachine).to have_transition(b: :c)
77
+ end
78
+
79
+ context 'with as' do
80
+ let(:options) { { [:a, :b] => :c, as: :go } }
81
+
82
+ it 'creates an alias' do
83
+ expect(StateMachine).to have_transition(a: :c).with_event(:go)
84
+ expect(StateMachine).to have_transition(b: :c).with_event(:go)
85
+ end
86
+ end
87
+ end
88
+
89
+ context 'multiple transitions' do
90
+ let(:options) { { a: :b, c: :d } }
91
+
92
+ it 'yields a transition configuraton' do
93
+ expect(@transition_configuration).to be_a TransitionConfiguration
94
+ end
95
+
96
+ it 'has both transitions' do
97
+ expect(StateMachine).to have_transition(a: :b)
98
+ expect(StateMachine).to have_transition(c: :d)
99
+ end
100
+
101
+ context 'with as' do
102
+ let(:options) { { a: :b, c: :d, as: :go } }
103
+
104
+ it 'creates an alias' do
105
+ expect(StateMachine).to have_transition(a: :b).with_event(:go)
106
+ expect(StateMachine).to have_transition(c: :d).with_event(:go)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '.state_attribute' do
113
+ context 'when set to :foobar' do
114
+ let(:object) { OpenStruct.new(foobar: :a) }
115
+ before { StateMachine.state_attribute :foobar }
116
+
117
+ it 'answers state with foobar' do
118
+ expect(machine.state).to eq object.foobar
119
+ end
120
+
121
+ it 'answers state= with foobar=' do
122
+ machine.state = :b
123
+ expect(object.foobar).to eq :b
124
+ end
125
+
126
+ after do
127
+ StateMachine.send(:remove_method, :state)
128
+ StateMachine.send(:remove_method, :state=)
129
+ end
130
+ end
131
+ end
132
+
133
+ describe '.states' do
134
+ before do
135
+ StateMachine.transition(a: :b)
136
+ StateMachine.transition(b: :c)
137
+ end
138
+
139
+ specify { expect(StateMachine.states).to eq [:a, :b, :c] }
140
+ end
141
+
142
+ describe '.start_states' do
143
+ before do
144
+ StateMachine.transition(a: :b)
145
+ StateMachine.transition(b: :c)
146
+ end
147
+
148
+ specify { expect(StateMachine.start_states).to eq [:a, :b] }
149
+ end
150
+
151
+ describe '.end_states' do
152
+ before do
153
+ StateMachine.transition(a: :b)
154
+ StateMachine.transition(b: :c)
155
+ end
156
+
157
+ specify { expect(StateMachine.end_states).to eq [:b, :c] }
158
+ end
159
+
160
+ describe '.store_states_as_strings!' do
161
+ it 'sets the flag' do
162
+ StateMachine.store_states_as_strings!
163
+ expect(StateMachine.store_states_as_strings).to be true
164
+ end
165
+ end
166
+
167
+ describe '.store_states_as_strings' do
168
+ it 'is false by default' do
169
+ expect(StateMachine.store_states_as_strings).to be false
170
+ end
171
+ end
172
+
173
+ describe '.initial_state' do
174
+ context 'when set to :first' do
175
+ before { StateMachine.set_initial_state :first }
176
+
177
+ it 'has that initial state' do
178
+ expect(machine.state).to eq :first
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end