end_state 0.2.0 → 0.3.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: 04263dcaee90aaa630e9e16fd1648a71717b5c49
4
- data.tar.gz: 1e5cba77cd2f71faa5792ba9374f7c14c941dda9
3
+ metadata.gz: a553419c7a74efd09772b41ad579948834f8c190
4
+ data.tar.gz: 6ab1ee4bffc24168b8bf31320cb85aa632384a72
5
5
  SHA512:
6
- metadata.gz: f9e25308af4b71252f6e3709949077ffe7dee67312aea2eff0e310848b8f1215c2fa84f6259901ceee0a0560ee6e5dd6b6064c81720c87fc19cb1ce5a9962dd2
7
- data.tar.gz: 4d9db32fc84ae115f6f6ae02e0a901f1f366351bc6291ae6c535f5633b12347fab002fb67322f627bb152f8084ff4c1e7adf773221fd4150b2f0fd01b5a7f979
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.
@@ -8,7 +8,7 @@ module EndState
8
8
  end
9
9
 
10
10
  def call
11
- object.state = state
11
+ object.state = object.class.store_states_as_strings ? state.to_s : state.to_sym
12
12
  true
13
13
  end
14
14
 
@@ -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
- aliases[transition_alias] = final_state
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.aliases
24
- @aliases ||= {}
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 = self.class.transition_state_for(check_state)
79
- return super if check_state.nil?
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." unless transition
115
+ fail UnknownState, "The state: #{state} is unknown."
97
116
  end
98
117
 
99
118
  def guard_failed(state, mode)
@@ -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)
@@ -1,3 +1,3 @@
1
1
  module EndState
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -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
@@ -6,6 +6,8 @@ module EndState
6
6
  let(:object) { OpenStruct.new(state: nil) }
7
7
  let(:state) { :a }
8
8
 
9
+ before { object.stub_chain(:class, :store_states_as_strings).and_return(false) }
10
+
9
11
  describe '#call' do
10
12
  it 'changes the state to the new state' do
11
13
  action.call
@@ -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 '@aliases'.to_sym, nil
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.aliases[:go]).to eq :b
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 alias' do
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 { transition.finalizers << finalizer }
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.2.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-05-30 00:00:00.000000000 Z
11
+ date: 2014-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler