end_state 0.12.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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