hsume2-state_machine 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. data/CHANGELOG.rdoc +413 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +717 -0
  4. data/Rakefile +77 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +448 -0
  28. data/lib/state_machine/alternate_machine.rb +79 -0
  29. data/lib/state_machine/assertions.rb +36 -0
  30. data/lib/state_machine/branch.rb +224 -0
  31. data/lib/state_machine/callback.rb +236 -0
  32. data/lib/state_machine/condition_proxy.rb +94 -0
  33. data/lib/state_machine/error.rb +13 -0
  34. data/lib/state_machine/eval_helpers.rb +86 -0
  35. data/lib/state_machine/event.rb +304 -0
  36. data/lib/state_machine/event_collection.rb +139 -0
  37. data/lib/state_machine/extensions.rb +149 -0
  38. data/lib/state_machine/initializers.rb +4 -0
  39. data/lib/state_machine/initializers/merb.rb +1 -0
  40. data/lib/state_machine/initializers/rails.rb +25 -0
  41. data/lib/state_machine/integrations.rb +110 -0
  42. data/lib/state_machine/integrations/active_model.rb +502 -0
  43. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  44. data/lib/state_machine/integrations/active_model/observer.rb +45 -0
  45. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  46. data/lib/state_machine/integrations/active_record.rb +424 -0
  47. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  48. data/lib/state_machine/integrations/active_record/versions.rb +143 -0
  49. data/lib/state_machine/integrations/base.rb +91 -0
  50. data/lib/state_machine/integrations/data_mapper.rb +392 -0
  51. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  52. data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
  53. data/lib/state_machine/integrations/mongo_mapper.rb +272 -0
  54. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  55. data/lib/state_machine/integrations/mongo_mapper/versions.rb +110 -0
  56. data/lib/state_machine/integrations/mongoid.rb +357 -0
  57. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  58. data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
  59. data/lib/state_machine/integrations/sequel.rb +428 -0
  60. data/lib/state_machine/integrations/sequel/versions.rb +36 -0
  61. data/lib/state_machine/machine.rb +1873 -0
  62. data/lib/state_machine/machine_collection.rb +87 -0
  63. data/lib/state_machine/matcher.rb +123 -0
  64. data/lib/state_machine/matcher_helpers.rb +54 -0
  65. data/lib/state_machine/node_collection.rb +157 -0
  66. data/lib/state_machine/path.rb +120 -0
  67. data/lib/state_machine/path_collection.rb +90 -0
  68. data/lib/state_machine/state.rb +271 -0
  69. data/lib/state_machine/state_collection.rb +112 -0
  70. data/lib/state_machine/transition.rb +458 -0
  71. data/lib/state_machine/transition_collection.rb +244 -0
  72. data/lib/tasks/state_machine.rake +1 -0
  73. data/lib/tasks/state_machine.rb +27 -0
  74. data/test/files/en.yml +17 -0
  75. data/test/files/switch.rb +11 -0
  76. data/test/functional/alternate_state_machine_test.rb +122 -0
  77. data/test/functional/state_machine_test.rb +993 -0
  78. data/test/test_helper.rb +4 -0
  79. data/test/unit/assertions_test.rb +40 -0
  80. data/test/unit/branch_test.rb +890 -0
  81. data/test/unit/callback_test.rb +701 -0
  82. data/test/unit/condition_proxy_test.rb +328 -0
  83. data/test/unit/error_test.rb +43 -0
  84. data/test/unit/eval_helpers_test.rb +222 -0
  85. data/test/unit/event_collection_test.rb +358 -0
  86. data/test/unit/event_test.rb +985 -0
  87. data/test/unit/integrations/active_model_test.rb +1097 -0
  88. data/test/unit/integrations/active_record_test.rb +2021 -0
  89. data/test/unit/integrations/base_test.rb +99 -0
  90. data/test/unit/integrations/data_mapper_test.rb +1909 -0
  91. data/test/unit/integrations/mongo_mapper_test.rb +1611 -0
  92. data/test/unit/integrations/mongoid_test.rb +1591 -0
  93. data/test/unit/integrations/sequel_test.rb +1523 -0
  94. data/test/unit/integrations_test.rb +61 -0
  95. data/test/unit/invalid_event_test.rb +20 -0
  96. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  97. data/test/unit/invalid_transition_test.rb +77 -0
  98. data/test/unit/machine_collection_test.rb +599 -0
  99. data/test/unit/machine_test.rb +3043 -0
  100. data/test/unit/matcher_helpers_test.rb +37 -0
  101. data/test/unit/matcher_test.rb +155 -0
  102. data/test/unit/node_collection_test.rb +217 -0
  103. data/test/unit/path_collection_test.rb +266 -0
  104. data/test/unit/path_test.rb +485 -0
  105. data/test/unit/state_collection_test.rb +310 -0
  106. data/test/unit/state_machine_test.rb +31 -0
  107. data/test/unit/state_test.rb +924 -0
  108. data/test/unit/transition_collection_test.rb +2102 -0
  109. data/test/unit/transition_test.rb +1541 -0
  110. metadata +207 -0
@@ -0,0 +1,458 @@
1
+ require 'state_machine/transition_collection'
2
+ require 'state_machine/error'
3
+
4
+ module StateMachine
5
+ # An invalid transition was attempted
6
+ class InvalidTransition < Error
7
+ # The machine attempting to be transitioned
8
+ attr_reader :machine
9
+
10
+ # The current state value for the machine
11
+ attr_reader :from
12
+
13
+ def initialize(object, machine, event) #:nodoc:
14
+ @machine = machine
15
+ @from_state = machine.states.match!(object)
16
+ @from = machine.read(object, :state)
17
+ @event = machine.events.fetch(event)
18
+
19
+ super(object, "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}")
20
+ end
21
+
22
+ # The event that triggered the failed transition
23
+ def event
24
+ @event.name
25
+ end
26
+
27
+ # The fully-qualified name of the event that triggered the failed transition
28
+ def qualified_event
29
+ @event.qualified_name
30
+ end
31
+
32
+ # The name for the current state
33
+ def from_name
34
+ @from_state.name
35
+ end
36
+
37
+ # The fully-qualified name for the current state
38
+ def qualified_from_name
39
+ @from_state.qualified_name
40
+ end
41
+ end
42
+
43
+ # A set of transition failed to run in parallel
44
+ class InvalidParallelTransition < Error
45
+ # The set of events that failed the transition(s)
46
+ attr_reader :events
47
+
48
+ def initialize(object, events) #:nodoc:
49
+ @events = events
50
+
51
+ super(object, "Cannot run events in parallel: #{events * ', '}")
52
+ end
53
+ end
54
+
55
+ # A transition represents a state change for a specific attribute.
56
+ #
57
+ # Transitions consist of:
58
+ # * An event
59
+ # * A starting state
60
+ # * An ending state
61
+ class Transition
62
+ # The object being transitioned
63
+ attr_reader :object
64
+
65
+ # The state machine for which this transition is defined
66
+ attr_reader :machine
67
+
68
+ # The original state value *before* the transition
69
+ attr_reader :from
70
+
71
+ # The new state value *after* the transition
72
+ attr_reader :to
73
+
74
+ # The arguments passed in to the event that triggered the transition
75
+ # (does not include the +run_action+ boolean argument if specified)
76
+ attr_accessor :args
77
+
78
+ # The result of invoking the action associated with the machine
79
+ attr_reader :result
80
+
81
+ # Whether the transition is only existing temporarily for the object
82
+ attr_writer :transient
83
+
84
+ # Creates a new, specific transition
85
+ def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
86
+ @object = object
87
+ @machine = machine
88
+ @args = []
89
+ @transient = false
90
+
91
+ @event = machine.events.fetch(event)
92
+ @from_state = machine.states.fetch(from_name)
93
+ @from = read_state ? machine.read(object, :state) : @from_state.value
94
+ @to_state = machine.states.fetch(to_name)
95
+ @to = @to_state.value
96
+
97
+ reset
98
+ end
99
+
100
+ # The attribute which this transition's machine is defined for
101
+ def attribute
102
+ machine.attribute
103
+ end
104
+
105
+ # The action that will be run when this transition is performed
106
+ def action
107
+ machine.action
108
+ end
109
+
110
+ # The event that triggered the transition
111
+ def event
112
+ @event.name
113
+ end
114
+
115
+ # The fully-qualified name of the event that triggered the transition
116
+ def qualified_event
117
+ @event.qualified_name
118
+ end
119
+
120
+ # The human-readable name of the event that triggered the transition
121
+ def human_event
122
+ @event.human_name(@object.class)
123
+ end
124
+
125
+ # The state name *before* the transition
126
+ def from_name
127
+ @from_state.name
128
+ end
129
+
130
+ # The fully-qualified state name *before* the transition
131
+ def qualified_from_name
132
+ @from_state.qualified_name
133
+ end
134
+
135
+ # The human-readable state name *before* the transition
136
+ def human_from_name
137
+ @from_state.human_name(@object.class)
138
+ end
139
+
140
+ # The new state name *after* the transition
141
+ def to_name
142
+ @to_state.name
143
+ end
144
+
145
+ # The new fully-qualified state name *after* the transition
146
+ def qualified_to_name
147
+ @to_state.qualified_name
148
+ end
149
+
150
+ # The new human-readable state name *after* the transition
151
+ def human_to_name
152
+ @to_state.human_name(@object.class)
153
+ end
154
+
155
+ # Does this transition represent a loopback (i.e. the from and to state
156
+ # are the same)
157
+ #
158
+ # == Example
159
+ #
160
+ # machine = StateMachine.new(Vehicle)
161
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
162
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
163
+ def loopback?
164
+ from_name == to_name
165
+ end
166
+
167
+ # Is this transition existing for a short period only? If this is set, it
168
+ # indicates that the transition (or the event backing it) should not be
169
+ # written to the object if it fails.
170
+ def transient?
171
+ @transient
172
+ end
173
+
174
+ # A hash of all the core attributes defined for this transition with their
175
+ # names as keys and values of the attributes as values.
176
+ #
177
+ # == Example
178
+ #
179
+ # machine = StateMachine.new(Vehicle)
180
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
181
+ # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
182
+ def attributes
183
+ @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
184
+ end
185
+
186
+ # Runs the actual transition and any before/after callbacks associated
187
+ # with the transition. The action associated with the transition/machine
188
+ # can be skipped by passing in +false+.
189
+ #
190
+ # == Examples
191
+ #
192
+ # class Vehicle
193
+ # state_machine :action => :save do
194
+ # ...
195
+ # end
196
+ # end
197
+ #
198
+ # vehicle = Vehicle.new
199
+ # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
200
+ # transition.perform # => Runs the +save+ action after setting the state attribute
201
+ # transition.perform(false) # => Only sets the state attribute
202
+ # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
203
+ # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
204
+ def perform(*args)
205
+ run_action = [true, false].include?(args.last) ? args.pop : true
206
+ self.args = args
207
+
208
+ # Run the transition
209
+ !!TransitionCollection.new([self], :actions => run_action).perform
210
+ end
211
+
212
+ # Runs a block within a transaction for the object being transitioned.
213
+ # By default, transactions are a no-op unless otherwise defined by the
214
+ # machine's integration.
215
+ def within_transaction
216
+ machine.within_transaction(object) do
217
+ yield
218
+ end
219
+ end
220
+
221
+ # Runs the before / after callbacks for this transition. If a block is
222
+ # provided, then it will be executed between the before and after callbacks.
223
+ #
224
+ # Configuration options:
225
+ # * +before+ - Whether to run before callbacks.
226
+ # * +after+ - Whether to run after callbacks. If false, then any around
227
+ # callbacks will be paused until called again with +after+ enabled.
228
+ # Default is true.
229
+ #
230
+ # This will return true if all before callbacks gets executed. After
231
+ # callbacks will not have an effect on the result.
232
+ def run_callbacks(options = {}, &block)
233
+ options = {:before => true, :after => true}.merge(options)
234
+ @success = false
235
+
236
+ halted = pausable { before(options[:after], &block) } if options[:before]
237
+
238
+ # After callbacks are only run if:
239
+ # * An around callback didn't halt after yielding
240
+ # * They're enabled or the run didn't succeed
241
+ after if !(@before_run && halted) && (options[:after] || !@success)
242
+
243
+ @before_run
244
+ end
245
+
246
+ # Transitions the current value of the state to that specified by the
247
+ # transition. Once the state is persisted, it cannot be persisted again
248
+ # until this transition is reset.
249
+ #
250
+ # == Example
251
+ #
252
+ # class Vehicle
253
+ # state_machine do
254
+ # event :ignite do
255
+ # transition :parked => :idling
256
+ # end
257
+ # end
258
+ # end
259
+ #
260
+ # vehicle = Vehicle.new
261
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
262
+ # transition.persist
263
+ #
264
+ # vehicle.state # => 'idling'
265
+ def persist
266
+ unless @persisted
267
+ machine.write(object, :state, to)
268
+ @persisted = true
269
+ end
270
+ end
271
+
272
+ # Rolls back changes made to the object's state via this transition. This
273
+ # will revert the state back to the +from+ value.
274
+ #
275
+ # == Example
276
+ #
277
+ # class Vehicle
278
+ # state_machine :initial => :parked do
279
+ # event :ignite do
280
+ # transition :parked => :idling
281
+ # end
282
+ # end
283
+ # end
284
+ #
285
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
286
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
287
+ #
288
+ # # Persist the new state
289
+ # vehicle.state # => "parked"
290
+ # transition.persist
291
+ # vehicle.state # => "idling"
292
+ #
293
+ # # Roll back to the original state
294
+ # transition.rollback
295
+ # vehicle.state # => "parked"
296
+ def rollback
297
+ reset
298
+ machine.write(object, :state, from)
299
+ end
300
+
301
+ # Resets any tracking of which callbacks have already been run and whether
302
+ # the state has already been persisted
303
+ def reset
304
+ @before_run = @persisted = @after_run = false
305
+ @paused_block = nil
306
+ end
307
+
308
+ # Determines equality of transitions by testing whether the object, states,
309
+ # and event involved in the transition are equal
310
+ def ==(other)
311
+ other.instance_of?(self.class) &&
312
+ other.object == object &&
313
+ other.machine == machine &&
314
+ other.from_name == from_name &&
315
+ other.to_name == to_name &&
316
+ other.event == event
317
+ end
318
+
319
+ # Generates a nicely formatted description of this transitions's contents.
320
+ #
321
+ # For example,
322
+ #
323
+ # transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
324
+ # transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
325
+ def inspect
326
+ "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
327
+ end
328
+
329
+ private
330
+ # Runs a block that may get paused. If the block doesn't pause, then
331
+ # execution will continue as normal. If the block gets paused, then it
332
+ # will take care of switching the execution context when it's resumed.
333
+ #
334
+ # This will return true if the given block halts for a reason other than
335
+ # getting paused.
336
+ def pausable
337
+ begin
338
+ halted = !catch(:halt) { yield; true }
339
+ rescue Exception => error
340
+ raise unless @resume_block
341
+ end
342
+
343
+ if @resume_block
344
+ @resume_block.call(halted, error)
345
+ else
346
+ halted
347
+ end
348
+ end
349
+
350
+ # Pauses the current callback execution. This should only occur within
351
+ # around callbacks when the remainder of the callback will be executed at
352
+ # a later point in time.
353
+ def pause
354
+ unless @resume_block
355
+ require 'continuation' unless defined?(callcc)
356
+ callcc do |block|
357
+ @paused_block = block
358
+ throw :halt, true
359
+ end
360
+ end
361
+ end
362
+
363
+ # Resumes the execution of a previously paused callback execution. Once
364
+ # the paused callbacks complete, the current execution will continue.
365
+ def resume
366
+ if @paused_block
367
+ halted, error = callcc do |block|
368
+ @resume_block = block
369
+ @paused_block.call
370
+ end
371
+
372
+ @resume_block = @paused_block = nil
373
+
374
+ raise error if error
375
+ !halted
376
+ else
377
+ true
378
+ end
379
+ end
380
+
381
+ # Runs the machine's +before+ callbacks for this transition. Only
382
+ # callbacks that are configured to match the event, from state, and to
383
+ # state will be invoked.
384
+ #
385
+ # Once the callbacks are run, they cannot be run again until this transition
386
+ # is reset.
387
+ def before(complete = true, index = 0, &block)
388
+ unless @before_run
389
+ while callback = machine.callbacks[:before][index]
390
+ index += 1
391
+
392
+ if callback.type == :around
393
+ # Around callback: need to handle recursively. Execution only gets
394
+ # paused if:
395
+ # * The block fails and the callback doesn't run on failures OR
396
+ # * The block succeeds, but after callbacks are disabled (in which
397
+ # case a continuation is stored for later execution)
398
+ return if catch(:cancel) do
399
+ callback.call(object, context, self) do
400
+ before(complete, index, &block)
401
+
402
+ pause if @success && !complete
403
+ throw :cancel, true unless @success
404
+ end
405
+ end
406
+ else
407
+ # Normal before callback
408
+ callback.call(object, context, self)
409
+ end
410
+ end
411
+
412
+ @before_run = true
413
+ end
414
+
415
+ action = {:success => true}.merge(block_given? ? yield : {})
416
+ @result, @success = action[:result], action[:success]
417
+ end
418
+
419
+ # Runs the machine's +after+ callbacks for this transition. Only
420
+ # callbacks that are configured to match the event, from state, and to
421
+ # state will be invoked.
422
+ #
423
+ # Once the callbacks are run, they cannot be run again until this transition
424
+ # is reset.
425
+ #
426
+ # == Halting
427
+ #
428
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
429
+ # and the callback chain will be automatically stopped. However, this
430
+ # exception will not bubble up to the caller since +after+ callbacks
431
+ # should never halt the execution of a +perform+.
432
+ def after
433
+ unless @after_run
434
+ # First resume previously paused callbacks
435
+ if resume
436
+ catch(:halt) do
437
+ type = @success ? :after : :failure
438
+ machine.callbacks[type].each {|callback| callback.call(object, context, self)}
439
+ end
440
+ end
441
+
442
+ @after_run = true
443
+ end
444
+ end
445
+
446
+ # Gets a hash of the context defining this unique transition (including
447
+ # event, from state, and to state).
448
+ #
449
+ # == Example
450
+ #
451
+ # machine = StateMachine.new(Vehicle)
452
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
453
+ # transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
454
+ def context
455
+ @context ||= {:on => event, :from => from_name, :to => to_name}
456
+ end
457
+ end
458
+ end