end_state 0.2.0 → 0.3.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 +4 -4
- data/README.md +47 -0
- data/lib/end_state/action.rb +1 -1
- data/lib/end_state/graph.rb +10 -3
- data/lib/end_state/state_machine.rb +36 -17
- data/lib/end_state/transition.rb +5 -1
- data/lib/end_state/version.rb +1 -1
- data/lib/tasks/end_state.rake +3 -2
- data/spec/end_state/action_spec.rb +2 -0
- data/spec/end_state/finalizer_spec.rb +1 -1
- data/spec/end_state/guard_spec.rb +1 -1
- data/spec/end_state/state_machine_spec.rb +43 -4
- data/spec/end_state/transition_spec.rb +11 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a553419c7a74efd09772b41ad579948834f8c190
|
4
|
+
data.tar.gz: 6ab1ee4bffc24168b8bf31320cb85aa632384a72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9360ba21fe5b2ce44808682ef5501d6cb8542bc9e227b4510e47cc0a812f35cd5923c0b46550a687714b3f09431ee60894d556f3fa8c00b2c442b2248f09e0bb
|
7
|
+
data.tar.gz: 571df2a52aa71250e347e608f05fad04bd04b32628cec368c19f76492af033061fe2c673959410c35015730de329aeddaf66f468ddde5434833eb58f37be07ac
|
data/README.md
CHANGED
@@ -196,6 +196,48 @@ class Machine < EndState::StateMachine
|
|
196
196
|
end
|
197
197
|
```
|
198
198
|
|
199
|
+
## Events
|
200
|
+
|
201
|
+
By using the `as` option in a transition definition you are creating an event representing that transition.
|
202
|
+
This can allow you to exercise the machine in a more natural "verb" style interaction. When using `as` event
|
203
|
+
definitions you can optionally set a `blocked` message on the transition. When the event is executed, if the
|
204
|
+
machine is not in a state maching the initial state of the event, the message is added to the `failure_messages`
|
205
|
+
array on the machine.
|
206
|
+
|
207
|
+
```
|
208
|
+
class Machine < EndState::StateMachine
|
209
|
+
transition a: :b, as: :go do |t|
|
210
|
+
t.blocked 'Cannot go!'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
machine = Machine.new(StatefulObject.new(:a))
|
215
|
+
|
216
|
+
machine.go! # => true
|
217
|
+
machine.state # => :b
|
218
|
+
machine.go! # => false
|
219
|
+
machine.failure_messages # => ['Cannot go!']
|
220
|
+
```
|
221
|
+
|
222
|
+
## State storage
|
223
|
+
|
224
|
+
You may want to use an attribute other than `state` to track the state of the machine.
|
225
|
+
|
226
|
+
```
|
227
|
+
class Machine < EndState::StateMachine
|
228
|
+
state_attribute :status
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
Depending on how you persist the `state` (if at all) you may want what is stored in `state` to be a string instead
|
233
|
+
of a symbol. You can tell the machine this preference.
|
234
|
+
|
235
|
+
```
|
236
|
+
class Machine < EndState::StateMachine
|
237
|
+
store_states_as_strings!
|
238
|
+
end
|
239
|
+
```
|
240
|
+
|
199
241
|
## Exceptions for failing Transitions
|
200
242
|
|
201
243
|
By default `transition` will only raise an exception, `EndState::UnknownState`, if called with a state that doesn't exist.
|
@@ -211,6 +253,11 @@ If you install `GraphViz` and the gem `ruby-graphviz` you can create images repr
|
|
211
253
|
|
212
254
|
`EndState::Graph.new(MyMachine).draw.output png: 'my_machine.png'`
|
213
255
|
|
256
|
+
If you use events in your machine, it will add the events along the arrow representing the transition. If you don't want this,
|
257
|
+
pass in false when contructing the Graph.
|
258
|
+
|
259
|
+
`EndState::Graph.new(MyMachine, false).draw.output png: 'my_machine.png'`
|
260
|
+
|
214
261
|
## Testing
|
215
262
|
|
216
263
|
Included is a custom RSpec matcher for testing your machines.
|
data/lib/end_state/action.rb
CHANGED
data/lib/end_state/graph.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module EndState
|
2
2
|
class Graph < GraphViz
|
3
|
-
attr_reader :machine, :nodes
|
3
|
+
attr_reader :machine, :nodes, :event_labels
|
4
4
|
|
5
|
-
def initialize(machine)
|
5
|
+
def initialize(machine, event_labels=true)
|
6
6
|
@machine = machine
|
7
7
|
@nodes = {}
|
8
|
+
@event_labels = event_labels
|
8
9
|
super machine.name.to_sym
|
9
10
|
end
|
10
11
|
|
@@ -13,7 +14,13 @@ module EndState
|
|
13
14
|
left, right = t.to_a.flatten
|
14
15
|
nodes[left] ||= add_node(left.to_s)
|
15
16
|
nodes[right] ||= add_node(right.to_s)
|
16
|
-
add_edge nodes[left], nodes[right]
|
17
|
+
edge = add_edge nodes[left], nodes[right]
|
18
|
+
if event_labels
|
19
|
+
event = machine.events.detect do |event, transition|
|
20
|
+
transition.include? t
|
21
|
+
end
|
22
|
+
edge[:label] = event.first.to_s if event
|
23
|
+
end
|
17
24
|
end
|
18
25
|
self
|
19
26
|
end
|
@@ -2,16 +2,24 @@ module EndState
|
|
2
2
|
class StateMachine < SimpleDelegator
|
3
3
|
attr_accessor :failure_messages, :success_messages
|
4
4
|
|
5
|
+
def self.store_states_as_strings!
|
6
|
+
@store_states_as_strings = true
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.store_states_as_strings
|
10
|
+
!!@store_states_as_strings
|
11
|
+
end
|
12
|
+
|
5
13
|
def self.transition(state_map)
|
6
14
|
initial_states = Array(state_map.keys.first)
|
7
15
|
final_state = state_map.values.first
|
8
16
|
transition_alias = state_map[:as] if state_map.keys.length > 1
|
9
17
|
transition = Transition.new(final_state)
|
10
18
|
initial_states.each do |state|
|
11
|
-
transitions[{ state => final_state }] = transition
|
19
|
+
transitions[{ state.to_sym => final_state.to_sym }] = transition
|
12
20
|
end
|
13
21
|
unless transition_alias.nil?
|
14
|
-
|
22
|
+
events[transition_alias.to_sym] = initial_states.map { |s| { s.to_sym => final_state.to_sym } }
|
15
23
|
end
|
16
24
|
yield transition if block_given?
|
17
25
|
end
|
@@ -20,8 +28,8 @@ module EndState
|
|
20
28
|
@transitions ||= {}
|
21
29
|
end
|
22
30
|
|
23
|
-
def self.
|
24
|
-
@
|
31
|
+
def self.events
|
32
|
+
@events ||= {}
|
25
33
|
end
|
26
34
|
|
27
35
|
def self.state_attribute(attribute)
|
@@ -41,26 +49,23 @@ module EndState
|
|
41
49
|
transitions.keys.map { |state_map| state_map.values.first }.uniq
|
42
50
|
end
|
43
51
|
|
44
|
-
def self.transition_state_for(check_state)
|
45
|
-
return check_state if states.include? check_state
|
46
|
-
return aliases[check_state] if aliases.keys.include? check_state
|
47
|
-
end
|
48
|
-
|
49
52
|
def object
|
50
53
|
__getobj__
|
51
54
|
end
|
52
55
|
|
53
|
-
def can_transition?(state)
|
54
|
-
previous_state = self.state
|
56
|
+
def can_transition?(state, params = {})
|
57
|
+
previous_state = self.state.to_sym
|
58
|
+
state = state.to_sym
|
55
59
|
transition = self.class.transitions[{ previous_state => state }]
|
56
60
|
return block_transistion(transition, state, :soft) unless transition
|
57
|
-
transition.will_allow? state
|
61
|
+
transition.will_allow? state, params
|
58
62
|
end
|
59
63
|
|
60
64
|
def transition(state, params = {}, mode = :soft)
|
61
65
|
@failure_messages = []
|
62
66
|
@success_messages = []
|
63
|
-
previous_state = self.state
|
67
|
+
previous_state = self.state ? self.state.to_sym : self.state
|
68
|
+
state = state.to_sym
|
64
69
|
transition = self.class.transitions[{ previous_state => state }]
|
65
70
|
return block_transistion(transition, state, mode) unless transition
|
66
71
|
return guard_failed(state, mode) unless transition.allowed?(self, params)
|
@@ -75,10 +80,11 @@ module EndState
|
|
75
80
|
|
76
81
|
def method_missing(method, *args, &block)
|
77
82
|
check_state = method.to_s[0..-2].to_sym
|
78
|
-
check_state =
|
79
|
-
return
|
83
|
+
check_state = state_for_event(check_state) || check_state
|
84
|
+
return false if check_state == :__invalid_event__
|
85
|
+
return super unless self.class.states.include?(check_state)
|
80
86
|
if method.to_s.end_with?('?')
|
81
|
-
state == check_state
|
87
|
+
state.to_sym == check_state
|
82
88
|
elsif method.to_s.end_with?('!')
|
83
89
|
transition check_state, args[0]
|
84
90
|
else
|
@@ -88,12 +94,25 @@ module EndState
|
|
88
94
|
|
89
95
|
private
|
90
96
|
|
97
|
+
def state_for_event(event)
|
98
|
+
transitions = self.class.events[event]
|
99
|
+
return false unless transitions
|
100
|
+
return invalid_event(event) unless transitions.map { |t| t.keys.first }.include?(state)
|
101
|
+
transitions.first.values.first
|
102
|
+
end
|
103
|
+
|
104
|
+
def invalid_event(event)
|
105
|
+
message = self.class.transitions[self.class.events[event].first].blocked_event_message
|
106
|
+
@failure_messages = [message] if message
|
107
|
+
:__invalid_event__
|
108
|
+
end
|
109
|
+
|
91
110
|
def block_transistion(transition, state, mode)
|
92
111
|
if self.class.end_states.include? state
|
93
112
|
fail UnknownTransition, "The transition: #{object.state} => #{state} is unknown." if mode == :hard
|
94
113
|
return false
|
95
114
|
end
|
96
|
-
fail UnknownState, "The state: #{state} is unknown."
|
115
|
+
fail UnknownState, "The state: #{state} is unknown."
|
97
116
|
end
|
98
117
|
|
99
118
|
def guard_failed(state, mode)
|
data/lib/end_state/transition.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module EndState
|
2
2
|
class Transition
|
3
|
-
attr_reader :state
|
3
|
+
attr_reader :state, :blocked_event_message
|
4
4
|
attr_accessor :action, :guards, :finalizers
|
5
5
|
|
6
6
|
def initialize(state)
|
@@ -42,6 +42,10 @@ module EndState
|
|
42
42
|
finalizer Finalizers::Persistence
|
43
43
|
end
|
44
44
|
|
45
|
+
def blocked(message)
|
46
|
+
@blocked_event_message = message
|
47
|
+
end
|
48
|
+
|
45
49
|
private
|
46
50
|
|
47
51
|
def rollback(finalized, object, previous_state, params)
|
data/lib/end_state/version.rb
CHANGED
data/lib/tasks/end_state.rake
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
namespace :end_state do
|
2
|
-
desc 'Draw the statemachine using GraphViz (options: machine=MyMachine, format=png, output=machine.png'
|
2
|
+
desc 'Draw the statemachine using GraphViz (options: machine=MyMachine, format=png, output=machine.png, event_labels=false)'
|
3
3
|
task :draw do
|
4
4
|
options = {}
|
5
5
|
options[:machine] = ENV['machine']
|
6
6
|
options[:format] = ENV['format'] || :png
|
7
7
|
options[:output] = ENV['output'] || "#{options[:machine].to_s}.#{options[:format].to_s}"
|
8
|
+
options[:event_labels] = !(ENV['event_labels'] == 'false')
|
8
9
|
if options[:machine]
|
9
10
|
EndState::Graph.new(Object.const_get(options[:machine])).draw.output options[:format] => options[:output]
|
10
11
|
else
|
11
12
|
puts 'A machine is required'
|
12
13
|
end
|
13
14
|
end
|
14
|
-
end
|
15
|
+
end
|
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
module EndState
|
4
4
|
describe Finalizer do
|
5
5
|
subject(:finalizer) { Finalizer.new(object, state, params) }
|
6
|
-
let(:object) { Struct.new('Machine', :failure_messages, :success_messages, :state).new }
|
6
|
+
let(:object) { Struct.new('Machine', :failure_messages, :success_messages, :state, :store_states_as_strings).new }
|
7
7
|
let(:state) { :a }
|
8
8
|
let(:params) { {} }
|
9
9
|
before do
|
@@ -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).new }
|
6
|
+
let(:object) { Struct.new('Machine', :failure_messages, :success_messages, :state, :store_states_as_strings).new }
|
7
7
|
let(:state) { :a }
|
8
8
|
let(:params) { {} }
|
9
9
|
before do
|
@@ -7,7 +7,8 @@ module EndState
|
|
7
7
|
let(:object) { OpenStruct.new(state: nil) }
|
8
8
|
before do
|
9
9
|
StateMachine.instance_variable_set '@transitions'.to_sym, nil
|
10
|
-
StateMachine.instance_variable_set '@
|
10
|
+
StateMachine.instance_variable_set '@events'.to_sym, nil
|
11
|
+
StateMachine.instance_variable_set '@store_states_as_strings'.to_sym, nil
|
11
12
|
end
|
12
13
|
|
13
14
|
describe '.transition' do
|
@@ -30,7 +31,7 @@ module EndState
|
|
30
31
|
context 'when the :as option is used' do
|
31
32
|
it 'creates an alias' do
|
32
33
|
StateMachine.transition(state_map.merge(as: :go))
|
33
|
-
expect(StateMachine.
|
34
|
+
expect(StateMachine.events[:go]).to eq [{ a: :b }]
|
34
35
|
end
|
35
36
|
end
|
36
37
|
end
|
@@ -83,6 +84,19 @@ module EndState
|
|
83
84
|
specify { expect(StateMachine.end_states).to eq [:b, :c] }
|
84
85
|
end
|
85
86
|
|
87
|
+
describe '.store_states_as_strings!' do
|
88
|
+
it 'sets the flag' do
|
89
|
+
StateMachine.store_states_as_strings!
|
90
|
+
expect(StateMachine.store_states_as_strings).to be_true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#store_states_as_strings' do
|
95
|
+
it 'is false by default' do
|
96
|
+
expect(StateMachine.store_states_as_strings).to be_false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
86
100
|
describe '#state' do
|
87
101
|
context 'when the object has state :a' do
|
88
102
|
let(:object) { OpenStruct.new(state: :a) }
|
@@ -121,7 +135,9 @@ module EndState
|
|
121
135
|
describe '#{state}!' do
|
122
136
|
let(:object) { OpenStruct.new(state: :a) }
|
123
137
|
before do
|
124
|
-
StateMachine.transition a: :b, as: :go
|
138
|
+
StateMachine.transition a: :b, as: :go do |t|
|
139
|
+
t.blocked 'Invalid event!'
|
140
|
+
end
|
125
141
|
end
|
126
142
|
|
127
143
|
it 'transitions the state' do
|
@@ -135,10 +151,24 @@ module EndState
|
|
135
151
|
expect(machine).to have_received(:transition).with(:b, { foo: 'bar', bar: 'foo' })
|
136
152
|
end
|
137
153
|
|
138
|
-
it 'works with an
|
154
|
+
it 'works with an event' do
|
139
155
|
machine.go!
|
140
156
|
expect(machine.state).to eq :b
|
141
157
|
end
|
158
|
+
|
159
|
+
context 'when the intial state is :c' do
|
160
|
+
let(:object) { OpenStruct.new(state: :c) }
|
161
|
+
|
162
|
+
it 'blocks invalid events' do
|
163
|
+
machine.go!
|
164
|
+
expect(machine.state).to eq :c
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'adds a failure message specified by blocked' do
|
168
|
+
machine.go!
|
169
|
+
expect(machine.failure_messages).to eq ['Invalid event!']
|
170
|
+
end
|
171
|
+
end
|
142
172
|
end
|
143
173
|
|
144
174
|
describe '#can_transition?' do
|
@@ -182,6 +212,15 @@ module EndState
|
|
182
212
|
machine.transition :b
|
183
213
|
expect(object.state).to eq :b
|
184
214
|
end
|
215
|
+
|
216
|
+
context 'and the machine is set to store_states_as_strings' do
|
217
|
+
before { StateMachine.store_states_as_strings! }
|
218
|
+
|
219
|
+
it 'transitions the state stored as a string' do
|
220
|
+
machine.transition :b
|
221
|
+
expect(object.state).to eq 'b'
|
222
|
+
end
|
223
|
+
end
|
185
224
|
end
|
186
225
|
|
187
226
|
context 'and a guard is configured' do
|
@@ -15,6 +15,13 @@ module EndState
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
describe '#blocked' do
|
19
|
+
it 'sets the blocked event message' do
|
20
|
+
transition.blocked 'This is blocked.'
|
21
|
+
expect(transition.blocked_event_message).to eq 'This is blocked.'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
18
25
|
describe '#guard' do
|
19
26
|
let(:guard) { double :guard }
|
20
27
|
|
@@ -93,7 +100,10 @@ module EndState
|
|
93
100
|
let(:finalizer) { double :finalizer, new: finalizer_instance }
|
94
101
|
let(:finalizer_instance) { double :finalizer_instance, call: nil, rollback: nil }
|
95
102
|
let(:object) { OpenStruct.new(state: :b) }
|
96
|
-
before
|
103
|
+
before do
|
104
|
+
object.stub_chain(:class, :store_states_as_strings).and_return(false)
|
105
|
+
transition.finalizers << finalizer
|
106
|
+
end
|
97
107
|
|
98
108
|
context 'when all finalizers succeed' do
|
99
109
|
before { finalizer_instance.stub(:call).and_return(true) }
|
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.
|
4
|
+
version: 0.3.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-
|
11
|
+
date: 2014-06-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|