simple_state_machine 0.6.0.pre → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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