simple_state_machine 0.6.0.pre → 0.6.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.
- data/Changelog.rdoc +16 -0
- data/README.rdoc +79 -51
- data/lib/simple_state_machine.rb +10 -1
- data/lib/simple_state_machine/.DS_Store +0 -0
- data/lib/simple_state_machine/active_record.rb +1 -75
- data/lib/simple_state_machine/decorator/active_record.rb +74 -0
- data/lib/simple_state_machine/decorator/default.rb +91 -0
- data/lib/simple_state_machine/simple_state_machine.rb +0 -342
- data/lib/simple_state_machine/state_machine.rb +88 -0
- data/lib/simple_state_machine/state_machine_definition.rb +72 -0
- data/lib/simple_state_machine/tools/graphviz.rb +17 -0
- data/lib/simple_state_machine/tools/inspector.rb +44 -0
- data/lib/simple_state_machine/transition.rb +40 -0
- data/lib/simple_state_machine/version.rb +1 -1
- data/simple_state_machine.gemspec +1 -1
- data/spec/.DS_Store +0 -0
- data/spec/active_record_spec.rb +7 -14
- data/spec/{decorator_spec.rb → decorator/default_spec.rb} +8 -8
- data/spec/examples_spec.rb +4 -4
- data/spec/mountable_spec.rb +3 -3
- data/spec/simple_state_machine_spec.rb +1 -2
- data/spec/spec_helper.rb +6 -0
- data/spec/state_machine_definition_spec.rb +1 -76
- data/spec/tools/graphviz_spec.rb +29 -0
- data/spec/tools/inspector_spec.rb +70 -0
- metadata +25 -14
@@ -1,10 +1,5 @@
|
|
1
1
|
module SimpleStateMachine
|
2
2
|
|
3
|
-
require 'cgi'
|
4
|
-
|
5
|
-
class IllegalStateTransitionError < ::RuntimeError
|
6
|
-
end
|
7
|
-
|
8
3
|
##
|
9
4
|
# Allows class to mount a state_machine
|
10
5
|
module Mountable
|
@@ -34,12 +29,10 @@ module SimpleStateMachine
|
|
34
29
|
##
|
35
30
|
# Adds state machine methods to the extended class
|
36
31
|
module Extendable
|
37
|
-
|
38
32
|
# mark the method as an event and specify how the state should transition
|
39
33
|
def event event_name, state_transitions
|
40
34
|
state_machine_definition.define_event event_name, state_transitions
|
41
35
|
end
|
42
|
-
|
43
36
|
end
|
44
37
|
include Extendable
|
45
38
|
|
@@ -56,339 +49,4 @@ module SimpleStateMachine
|
|
56
49
|
end
|
57
50
|
include Inheritable
|
58
51
|
|
59
|
-
##
|
60
|
-
# Defines state machine transitions
|
61
|
-
class StateMachineDefinition
|
62
|
-
|
63
|
-
attr_writer :default_error_state, :state_method, :subject, :decorator,
|
64
|
-
:decorator_class
|
65
|
-
|
66
|
-
def decorator
|
67
|
-
@decorator ||= decorator_class.new(@subject)
|
68
|
-
end
|
69
|
-
|
70
|
-
def decorator_class
|
71
|
-
@decorator_class ||= Decorator
|
72
|
-
end
|
73
|
-
|
74
|
-
def default_error_state
|
75
|
-
@default_error_state && @default_error_state.to_s
|
76
|
-
end
|
77
|
-
|
78
|
-
def transitions
|
79
|
-
@transitions ||= []
|
80
|
-
end
|
81
|
-
|
82
|
-
def define_event event_name, state_transitions
|
83
|
-
state_transitions.each do |froms, to|
|
84
|
-
[froms].flatten.each do |from|
|
85
|
-
add_transition(event_name, from, to)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def add_transition event_name, from, to
|
91
|
-
transition = Transition.new(event_name, from, to)
|
92
|
-
transitions << transition
|
93
|
-
decorator.decorate(transition)
|
94
|
-
end
|
95
|
-
|
96
|
-
def state_method
|
97
|
-
@state_method ||= :state
|
98
|
-
end
|
99
|
-
|
100
|
-
# Human readable format: old_state.event! => new_state
|
101
|
-
def to_s
|
102
|
-
transitions.map(&:to_s).join("\n")
|
103
|
-
end
|
104
|
-
|
105
|
-
module Graphviz
|
106
|
-
# Graphviz dot format for rendering as a directional graph
|
107
|
-
def to_graphviz_dot
|
108
|
-
transitions.map { |t| t.to_graphviz_dot }.sort.join(";")
|
109
|
-
end
|
110
|
-
|
111
|
-
# Generates a url that renders states and events as a directional graph.
|
112
|
-
# See http://code.google.com/apis/chart/docs/gallery/graphviz.html
|
113
|
-
def google_chart_url
|
114
|
-
"http://chart.googleapis.com/chart?cht=gv&chl=digraph{#{::CGI.escape to_graphviz_dot}}"
|
115
|
-
end
|
116
|
-
end
|
117
|
-
include Graphviz
|
118
|
-
|
119
|
-
module Inspector
|
120
|
-
def begin_states
|
121
|
-
from_states - to_states
|
122
|
-
end
|
123
|
-
|
124
|
-
def end_states
|
125
|
-
to_states - from_states
|
126
|
-
end
|
127
|
-
|
128
|
-
def states
|
129
|
-
(to_states + from_states).uniq
|
130
|
-
end
|
131
|
-
|
132
|
-
private
|
133
|
-
|
134
|
-
def from_states
|
135
|
-
to_uniq_sym(sample_transitions.map(&:from))
|
136
|
-
end
|
137
|
-
|
138
|
-
def to_states
|
139
|
-
to_uniq_sym(sample_transitions.map(&:to))
|
140
|
-
end
|
141
|
-
|
142
|
-
def to_uniq_sym(array)
|
143
|
-
array.map { |state| state.is_a?(String) ? state.to_sym : state }.uniq
|
144
|
-
end
|
145
|
-
|
146
|
-
def sample_transitions
|
147
|
-
(@subject || sample_subject).state_machine_definition.send :transitions
|
148
|
-
end
|
149
|
-
|
150
|
-
def sample_subject
|
151
|
-
self_class = self.class
|
152
|
-
sample = Class.new do
|
153
|
-
extend SimpleStateMachine::Mountable
|
154
|
-
mount_state_machine self_class
|
155
|
-
end
|
156
|
-
sample
|
157
|
-
end
|
158
|
-
end
|
159
|
-
include Inspector
|
160
|
-
|
161
|
-
module Mountable
|
162
|
-
def event event_name, state_transitions
|
163
|
-
events << [event_name, state_transitions]
|
164
|
-
end
|
165
|
-
|
166
|
-
def events
|
167
|
-
@events ||= []
|
168
|
-
end
|
169
|
-
|
170
|
-
module InstanceMethods
|
171
|
-
def add_events
|
172
|
-
self.class.events.each do |event_name, state_transitions|
|
173
|
-
define_event event_name, state_transitions
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
extend Mountable
|
179
|
-
include Mountable::InstanceMethods
|
180
|
-
|
181
|
-
end
|
182
|
-
|
183
|
-
##
|
184
|
-
# Defines the state machine used by the instance
|
185
|
-
class StateMachine
|
186
|
-
attr_reader :raised_error
|
187
|
-
|
188
|
-
def initialize(subject)
|
189
|
-
@subject = subject
|
190
|
-
end
|
191
|
-
|
192
|
-
# Returns the next state for the subject for event_name
|
193
|
-
def next_state(event_name)
|
194
|
-
transition = transitions.select{|t| t.is_transition_for?(event_name, @subject.send(state_method))}.first
|
195
|
-
transition ? transition.to : nil
|
196
|
-
end
|
197
|
-
|
198
|
-
# Returns the error state for the subject for event_name and error
|
199
|
-
def error_state(event_name, error)
|
200
|
-
transition = transitions.select{|t| t.is_error_transition_for?(event_name, error) }.first
|
201
|
-
transition ? transition.to : nil
|
202
|
-
end
|
203
|
-
|
204
|
-
# Transitions to the next state if next_state exists.
|
205
|
-
# When an error occurs, it uses the error to determine next state.
|
206
|
-
# If no next state can be determined it transitions to the default error
|
207
|
-
# state if defined, otherwise the error is re-raised.
|
208
|
-
# Calls illegal_event_callback event_name if no next_state is found
|
209
|
-
def transition(event_name)
|
210
|
-
clear_raised_error
|
211
|
-
if to = next_state(event_name)
|
212
|
-
begin
|
213
|
-
result = yield
|
214
|
-
rescue => e
|
215
|
-
error_state = error_state(event_name, e) ||
|
216
|
-
state_machine_definition.default_error_state
|
217
|
-
if error_state
|
218
|
-
@raised_error = e
|
219
|
-
@subject.send("#{state_method}=", error_state)
|
220
|
-
return result
|
221
|
-
else
|
222
|
-
raise
|
223
|
-
end
|
224
|
-
end
|
225
|
-
# TODO refactor out to AR module
|
226
|
-
if defined?(::ActiveRecord) && @subject.is_a?(::ActiveRecord::Base)
|
227
|
-
if @subject.errors.entries.empty?
|
228
|
-
@subject.send("#{state_method}=", to)
|
229
|
-
return true
|
230
|
-
else
|
231
|
-
return false
|
232
|
-
end
|
233
|
-
else
|
234
|
-
@subject.send("#{state_method}=", to)
|
235
|
-
return result
|
236
|
-
end
|
237
|
-
else
|
238
|
-
illegal_event_callback event_name
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
private
|
243
|
-
|
244
|
-
def clear_raised_error
|
245
|
-
@raised_error = nil
|
246
|
-
end
|
247
|
-
|
248
|
-
def state_machine_definition
|
249
|
-
@subject.class.state_machine_definition
|
250
|
-
end
|
251
|
-
|
252
|
-
def transitions
|
253
|
-
state_machine_definition.transitions
|
254
|
-
end
|
255
|
-
|
256
|
-
def state_method
|
257
|
-
state_machine_definition.state_method
|
258
|
-
end
|
259
|
-
|
260
|
-
# override with your own implementation, like setting errors in your model
|
261
|
-
def illegal_event_callback event_name
|
262
|
-
raise IllegalStateTransitionError.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
|
263
|
-
end
|
264
|
-
|
265
|
-
end
|
266
|
-
|
267
|
-
##
|
268
|
-
# Defines transitions for events
|
269
|
-
class Transition
|
270
|
-
attr_reader :event_name, :from, :to
|
271
|
-
def initialize(event_name, from, to)
|
272
|
-
@event_name = event_name.to_s
|
273
|
-
@from = from.is_a?(Class) ? from : from.to_s
|
274
|
-
@to = to.to_s
|
275
|
-
end
|
276
|
-
|
277
|
-
# returns true if it's a transition for event_name
|
278
|
-
def is_transition_for?(event_name, subject_state)
|
279
|
-
is_same_event?(event_name) && is_same_from?(subject_state)
|
280
|
-
end
|
281
|
-
|
282
|
-
# returns true if it's a error transition for event_name and error
|
283
|
-
def is_error_transition_for?(event_name, error)
|
284
|
-
is_same_event?(event_name) && from.is_a?(Class) && error.is_a?(from)
|
285
|
-
end
|
286
|
-
|
287
|
-
def to_s
|
288
|
-
"#{from}.#{event_name}! => #{to}"
|
289
|
-
end
|
290
|
-
|
291
|
-
def to_graphviz_dot
|
292
|
-
%("#{from}"->"#{to}"[label=#{event_name}])
|
293
|
-
end
|
294
|
-
|
295
|
-
private
|
296
|
-
|
297
|
-
def is_same_event?(event_name)
|
298
|
-
self.event_name == event_name.to_s
|
299
|
-
end
|
300
|
-
|
301
|
-
def is_same_from?(subject_from)
|
302
|
-
from.to_s == 'all' || subject_from.to_s == from.to_s
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
##
|
307
|
-
# Decorates @subject with methods to access the state machine
|
308
|
-
class Decorator
|
309
|
-
|
310
|
-
attr_writer :subject
|
311
|
-
def initialize(subject)
|
312
|
-
@subject = subject
|
313
|
-
end
|
314
|
-
|
315
|
-
def decorate transition
|
316
|
-
define_state_machine_method
|
317
|
-
define_state_getter_method
|
318
|
-
define_state_setter_method
|
319
|
-
|
320
|
-
define_state_helper_method(transition.from)
|
321
|
-
define_state_helper_method(transition.to)
|
322
|
-
define_event_method(transition.event_name)
|
323
|
-
decorate_event_method(transition.event_name)
|
324
|
-
end
|
325
|
-
|
326
|
-
private
|
327
|
-
|
328
|
-
def define_state_machine_method
|
329
|
-
unless any_method_defined?("state_machine")
|
330
|
-
@subject.send(:define_method, "state_machine") do
|
331
|
-
@state_machine ||= StateMachine.new(self)
|
332
|
-
end
|
333
|
-
end
|
334
|
-
end
|
335
|
-
|
336
|
-
def define_state_helper_method state
|
337
|
-
unless any_method_defined?("#{state.to_s}?")
|
338
|
-
@subject.send(:define_method, "#{state.to_s}?") do
|
339
|
-
self.send(self.class.state_machine_definition.state_method) == state.to_s
|
340
|
-
end
|
341
|
-
end
|
342
|
-
end
|
343
|
-
|
344
|
-
def define_event_method event_name
|
345
|
-
unless any_method_defined?("#{event_name}")
|
346
|
-
@subject.send(:define_method, "#{event_name}") {}
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
def decorate_event_method event_name
|
351
|
-
# TODO put in transaction for activeRecord?
|
352
|
-
unless @subject.method_defined?("with_managed_state_#{event_name}")
|
353
|
-
@subject.send(:define_method, "with_managed_state_#{event_name}") do |*args|
|
354
|
-
return state_machine.transition(event_name) do
|
355
|
-
send("without_managed_state_#{event_name}", *args)
|
356
|
-
end
|
357
|
-
end
|
358
|
-
alias_event_methods event_name
|
359
|
-
end
|
360
|
-
end
|
361
|
-
|
362
|
-
def define_state_setter_method
|
363
|
-
unless any_method_defined?("#{state_method}=")
|
364
|
-
@subject.send(:define_method, "#{state_method}=") do |new_state|
|
365
|
-
instance_variable_set(:"@#{self.class.state_machine_definition.state_method}", new_state)
|
366
|
-
end
|
367
|
-
end
|
368
|
-
end
|
369
|
-
|
370
|
-
def define_state_getter_method
|
371
|
-
unless any_method_defined?(state_method)
|
372
|
-
@subject.send(:attr_reader, state_method)
|
373
|
-
end
|
374
|
-
end
|
375
|
-
|
376
|
-
def any_method_defined?(method)
|
377
|
-
@subject.method_defined?(method) ||
|
378
|
-
@subject.protected_method_defined?(method) ||
|
379
|
-
@subject.private_method_defined?(method)
|
380
|
-
end
|
381
|
-
|
382
|
-
protected
|
383
|
-
|
384
|
-
def alias_event_methods event_name
|
385
|
-
@subject.send :alias_method, "without_managed_state_#{event_name}", event_name
|
386
|
-
@subject.send :alias_method, event_name, "with_managed_state_#{event_name}"
|
387
|
-
end
|
388
|
-
|
389
|
-
def state_method
|
390
|
-
@subject.state_machine_definition.state_method
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
52
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module SimpleStateMachine
|
2
|
+
|
3
|
+
class IllegalStateTransitionError < ::RuntimeError; end
|
4
|
+
|
5
|
+
##
|
6
|
+
# Defines the state machine used by the instance
|
7
|
+
class StateMachine
|
8
|
+
attr_reader :raised_error
|
9
|
+
|
10
|
+
def initialize(subject)
|
11
|
+
@subject = subject
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the next state for the subject for event_name
|
15
|
+
def next_state(event_name)
|
16
|
+
transition = transitions.select{|t| t.is_transition_for?(event_name, @subject.send(state_method))}.first
|
17
|
+
transition ? transition.to : nil
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the error state for the subject for event_name and error
|
21
|
+
def error_state(event_name, error)
|
22
|
+
transition = transitions.select{|t| t.is_error_transition_for?(event_name, error) }.first
|
23
|
+
transition ? transition.to : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Transitions to the next state if next_state exists.
|
27
|
+
# When an error occurs, it uses the error to determine next state.
|
28
|
+
# If no next state can be determined it transitions to the default error
|
29
|
+
# state if defined, otherwise the error is re-raised.
|
30
|
+
# Calls illegal_event_callback event_name if no next_state is found
|
31
|
+
def transition(event_name)
|
32
|
+
clear_raised_error
|
33
|
+
if to = next_state(event_name)
|
34
|
+
begin
|
35
|
+
result = yield
|
36
|
+
rescue => e
|
37
|
+
error_state = error_state(event_name, e) ||
|
38
|
+
state_machine_definition.default_error_state
|
39
|
+
if error_state
|
40
|
+
@raised_error = e
|
41
|
+
@subject.send("#{state_method}=", error_state)
|
42
|
+
return result
|
43
|
+
else
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# TODO refactor out to AR module
|
48
|
+
if defined?(::ActiveRecord) && @subject.is_a?(::ActiveRecord::Base)
|
49
|
+
if @subject.errors.entries.empty?
|
50
|
+
@subject.send("#{state_method}=", to)
|
51
|
+
return true
|
52
|
+
else
|
53
|
+
return false
|
54
|
+
end
|
55
|
+
else
|
56
|
+
@subject.send("#{state_method}=", to)
|
57
|
+
return result
|
58
|
+
end
|
59
|
+
else
|
60
|
+
illegal_event_callback event_name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def clear_raised_error
|
67
|
+
@raised_error = nil
|
68
|
+
end
|
69
|
+
|
70
|
+
def state_machine_definition
|
71
|
+
@subject.class.state_machine_definition
|
72
|
+
end
|
73
|
+
|
74
|
+
def transitions
|
75
|
+
state_machine_definition.transitions
|
76
|
+
end
|
77
|
+
|
78
|
+
def state_method
|
79
|
+
state_machine_definition.state_method
|
80
|
+
end
|
81
|
+
|
82
|
+
# override with your own implementation, like setting errors in your model
|
83
|
+
def illegal_event_callback event_name
|
84
|
+
raise IllegalStateTransitionError.new("You cannot '#{event_name}' when state is '#{@subject.send(state_method)}'")
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module SimpleStateMachine
|
2
|
+
##
|
3
|
+
# Defines state machine transitions
|
4
|
+
class StateMachineDefinition
|
5
|
+
|
6
|
+
attr_writer :default_error_state, :state_method, :subject, :decorator,
|
7
|
+
:decorator_class
|
8
|
+
|
9
|
+
def decorator
|
10
|
+
@decorator ||= decorator_class.new(@subject)
|
11
|
+
end
|
12
|
+
|
13
|
+
def decorator_class
|
14
|
+
@decorator_class ||= Decorator::Default
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_error_state
|
18
|
+
@default_error_state && @default_error_state.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def transitions
|
22
|
+
@transitions ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def define_event event_name, state_transitions
|
26
|
+
state_transitions.each do |froms, to|
|
27
|
+
[froms].flatten.each do |from|
|
28
|
+
add_transition(event_name, from, to)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_transition event_name, from, to
|
34
|
+
transition = Transition.new(event_name, from, to)
|
35
|
+
transitions << transition
|
36
|
+
decorator.decorate(transition)
|
37
|
+
end
|
38
|
+
|
39
|
+
def state_method
|
40
|
+
@state_method ||= :state
|
41
|
+
end
|
42
|
+
|
43
|
+
# Human readable format: old_state.event! => new_state
|
44
|
+
def to_s
|
45
|
+
transitions.map(&:to_s).join("\n")
|
46
|
+
end
|
47
|
+
|
48
|
+
module Mountable
|
49
|
+
def event event_name, state_transitions
|
50
|
+
events << [event_name, state_transitions]
|
51
|
+
end
|
52
|
+
|
53
|
+
def events
|
54
|
+
@events ||= []
|
55
|
+
end
|
56
|
+
|
57
|
+
module InstanceMethods
|
58
|
+
def add_events
|
59
|
+
self.class.events.each do |event_name, state_transitions|
|
60
|
+
define_event event_name, state_transitions
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
extend Mountable
|
66
|
+
include Mountable::InstanceMethods
|
67
|
+
|
68
|
+
# TODO only include in rake task?
|
69
|
+
include ::SimpleStateMachine::Tools::Graphviz
|
70
|
+
include ::SimpleStateMachine::Tools::Inspector
|
71
|
+
end
|
72
|
+
end
|