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 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