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