hsume2-state_machine 1.0.1

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