state_machine 1.0.2 → 1.0.3

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 (102) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +0 -2
  3. data/.yardopts +3 -2
  4. data/Appraisals +48 -0
  5. data/{CHANGELOG.rdoc → CHANGELOG.md} +63 -46
  6. data/README.md +1029 -0
  7. data/gemfiles/active_model-3.0.0.gemfile.lock +1 -3
  8. data/gemfiles/active_model-3.0.5.gemfile.lock +1 -3
  9. data/gemfiles/active_model-3.1.1.gemfile +7 -0
  10. data/gemfiles/active_model-3.1.1.gemfile.lock +32 -0
  11. data/gemfiles/active_record-2.0.0.gemfile.lock +1 -3
  12. data/gemfiles/active_record-2.0.5.gemfile.lock +1 -3
  13. data/gemfiles/active_record-2.1.0.gemfile.lock +1 -3
  14. data/gemfiles/active_record-2.1.2.gemfile.lock +1 -3
  15. data/gemfiles/active_record-2.2.3.gemfile.lock +1 -3
  16. data/gemfiles/active_record-2.3.12.gemfile.lock +1 -3
  17. data/gemfiles/active_record-3.0.0.gemfile.lock +1 -3
  18. data/gemfiles/active_record-3.0.5.gemfile.lock +1 -3
  19. data/gemfiles/active_record-3.1.1.gemfile +8 -0
  20. data/gemfiles/active_record-3.1.1.gemfile.lock +43 -0
  21. data/gemfiles/data_mapper-0.10.2.gemfile.lock +1 -3
  22. data/gemfiles/data_mapper-0.9.11.gemfile.lock +1 -3
  23. data/gemfiles/data_mapper-0.9.4.gemfile.lock +1 -3
  24. data/gemfiles/data_mapper-0.9.7.gemfile.lock +1 -3
  25. data/gemfiles/data_mapper-1.0.0.gemfile.lock +1 -3
  26. data/gemfiles/data_mapper-1.0.1.gemfile.lock +1 -3
  27. data/gemfiles/data_mapper-1.0.2.gemfile.lock +1 -3
  28. data/gemfiles/data_mapper-1.1.0.gemfile.lock +1 -3
  29. data/gemfiles/data_mapper-1.2.0.gemfile +12 -0
  30. data/gemfiles/data_mapper-1.2.0.gemfile.lock +49 -0
  31. data/gemfiles/default.gemfile.lock +1 -3
  32. data/gemfiles/graphviz-0.9.0.gemfile +7 -0
  33. data/gemfiles/graphviz-0.9.0.gemfile.lock +24 -0
  34. data/gemfiles/graphviz-0.9.21.gemfile +7 -0
  35. data/gemfiles/graphviz-0.9.21.gemfile.lock +24 -0
  36. data/gemfiles/graphviz-1.0.0.gemfile +7 -0
  37. data/gemfiles/graphviz-1.0.0.gemfile.lock +24 -0
  38. data/gemfiles/mongo_mapper-0.10.0.gemfile +7 -0
  39. data/gemfiles/mongo_mapper-0.10.0.gemfile.lock +41 -0
  40. data/gemfiles/mongo_mapper-0.5.5.gemfile.lock +1 -3
  41. data/gemfiles/mongo_mapper-0.5.8.gemfile.lock +1 -3
  42. data/gemfiles/mongo_mapper-0.6.0.gemfile.lock +1 -3
  43. data/gemfiles/mongo_mapper-0.6.10.gemfile.lock +1 -3
  44. data/gemfiles/mongo_mapper-0.7.0.gemfile.lock +1 -3
  45. data/gemfiles/mongo_mapper-0.7.5.gemfile.lock +1 -3
  46. data/gemfiles/mongo_mapper-0.8.0.gemfile.lock +1 -3
  47. data/gemfiles/mongo_mapper-0.8.3.gemfile.lock +1 -3
  48. data/gemfiles/mongo_mapper-0.8.4.gemfile.lock +1 -3
  49. data/gemfiles/mongo_mapper-0.8.6.gemfile.lock +1 -3
  50. data/gemfiles/mongo_mapper-0.9.0.gemfile.lock +1 -3
  51. data/gemfiles/mongoid-2.0.0.gemfile.lock +1 -3
  52. data/gemfiles/mongoid-2.1.4.gemfile.lock +1 -3
  53. data/gemfiles/mongoid-2.2.4.gemfile +7 -0
  54. data/gemfiles/mongoid-2.2.4.gemfile.lock +40 -0
  55. data/gemfiles/mongoid-2.3.3.gemfile +7 -0
  56. data/gemfiles/mongoid-2.3.3.gemfile.lock +40 -0
  57. data/gemfiles/sequel-2.11.0.gemfile.lock +1 -3
  58. data/gemfiles/sequel-2.12.0.gemfile.lock +1 -3
  59. data/gemfiles/sequel-2.8.0.gemfile.lock +1 -3
  60. data/gemfiles/sequel-3.0.0.gemfile.lock +1 -3
  61. data/gemfiles/sequel-3.13.0.gemfile.lock +1 -3
  62. data/gemfiles/sequel-3.14.0.gemfile.lock +1 -3
  63. data/gemfiles/sequel-3.23.0.gemfile.lock +1 -3
  64. data/gemfiles/sequel-3.24.0.gemfile.lock +1 -3
  65. data/gemfiles/sequel-3.29.0.gemfile +8 -0
  66. data/gemfiles/sequel-3.29.0.gemfile.lock +26 -0
  67. data/lib/state_machine.rb +45 -0
  68. data/lib/state_machine/event.rb +18 -3
  69. data/lib/state_machine/event_collection.rb +1 -1
  70. data/lib/state_machine/integrations/active_model.rb +59 -16
  71. data/lib/state_machine/integrations/active_model/observer.rb +3 -15
  72. data/lib/state_machine/integrations/active_record.rb +46 -9
  73. data/lib/state_machine/integrations/data_mapper.rb +42 -2
  74. data/lib/state_machine/integrations/data_mapper/versions.rb +22 -10
  75. data/lib/state_machine/integrations/mongo_mapper.rb +55 -0
  76. data/lib/state_machine/integrations/mongo_mapper/versions.rb +3 -3
  77. data/lib/state_machine/integrations/mongoid.rb +57 -12
  78. data/lib/state_machine/integrations/mongoid/versions.rb +22 -4
  79. data/lib/state_machine/integrations/sequel.rb +45 -0
  80. data/lib/state_machine/integrations/sequel/versions.rb +3 -0
  81. data/lib/state_machine/machine.rb +148 -34
  82. data/lib/state_machine/node_collection.rb +36 -3
  83. data/lib/state_machine/state.rb +6 -3
  84. data/lib/state_machine/state_collection.rb +1 -1
  85. data/lib/state_machine/version.rb +1 -1
  86. data/lib/tasks/state_machine.rb +11 -9
  87. data/state_machine.gemspec +2 -3
  88. data/test/functional/state_machine_test.rb +54 -1
  89. data/test/unit/event_collection_test.rb +4 -0
  90. data/test/unit/event_test.rb +34 -1
  91. data/test/unit/integrations/active_model_test.rb +80 -0
  92. data/test/unit/integrations/active_record_test.rb +105 -2
  93. data/test/unit/integrations/data_mapper_test.rb +27 -25
  94. data/test/unit/integrations/mongo_mapper_test.rb +80 -25
  95. data/test/unit/integrations/mongoid_test.rb +61 -6
  96. data/test/unit/integrations/sequel_test.rb +8 -2
  97. data/test/unit/machine_test.rb +87 -9
  98. data/test/unit/node_collection_test.rb +129 -12
  99. data/test/unit/state_collection_test.rb +4 -0
  100. data/test/unit/state_test.rb +2 -2
  101. metadata +30 -24
  102. data/README.rdoc +0 -844
@@ -0,0 +1,1029 @@
1
+ # state_machine [![Build Status](https://secure.travis-ci.org/pluginaweek/state_machine.png "Build Status")](http://travis-ci.org/pluginaweek/state_machine)
2
+
3
+ *state_machine* adds support for creating state machines for attributes on any
4
+ Ruby class.
5
+
6
+ ## Resources
7
+
8
+ API
9
+
10
+ * http://rdoc.info/github/pluginaweek/state_machine/master/frames
11
+
12
+ Bugs
13
+
14
+ * http://github.com/pluginaweek/state_machine/issues
15
+
16
+ Development
17
+
18
+ * http://github.com/pluginaweek/state_machine
19
+
20
+ Testing
21
+
22
+ * http://travis-ci.org/pluginaweek/state_machine
23
+
24
+ Source
25
+
26
+ * git://github.com/pluginaweek/state_machine.git
27
+
28
+ Mailing List
29
+
30
+ * http://groups.google.com/group/pluginaweek-talk
31
+
32
+ ## Description
33
+
34
+ State machines make it dead-simple to manage the behavior of a class. Too often,
35
+ the state of an object is kept by creating multiple boolean attributes and
36
+ deciding how to behave based on the values. This can become cumbersome and
37
+ difficult to maintain when the complexity of your class starts to increase.
38
+
39
+ *state_machine* simplifies this design by introducing the various parts of a real
40
+ state machine, including states, events, transitions, and callbacks. However,
41
+ the api is designed to be so simple you don't even need to know what a
42
+ state machine is :)
43
+
44
+ Some brief, high-level features include:
45
+
46
+ * Defining state machines on any Ruby class
47
+ * Multiple state machines on a single class
48
+ * Namespaced state machines
49
+ * before/after/around/failure transition hooks with explicit transition requirements
50
+ * Integration with ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel
51
+ * State predicates
52
+ * State-driven instance / class behavior
53
+ * State values of any data type
54
+ * Dynamically-generated state values
55
+ * Event parallelization
56
+ * Attribute-based event transitions
57
+ * Path analysis
58
+ * Inheritance
59
+ * Internationalization
60
+ * GraphViz visualization creator
61
+ * Flexible machine syntax
62
+
63
+ Examples of the usage patterns for some of the above features are shown below.
64
+ You can find much more detailed documentation in the actual API.
65
+
66
+ ## Usage
67
+
68
+ ### Example
69
+
70
+ Below is an example of many of the features offered by this plugin, including:
71
+
72
+ * Initial states
73
+ * Namespaced states
74
+ * Transition callbacks
75
+ * Conditional transitions
76
+ * State-driven instance behavior
77
+ * Customized state values
78
+ * Parallel events
79
+ * Path analysis
80
+
81
+ Class definition:
82
+
83
+ ```ruby
84
+ class Vehicle
85
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
86
+
87
+ state_machine :state, :initial => :parked do
88
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
89
+
90
+ after_transition :on => :crash, :do => :tow
91
+ after_transition :on => :repair, :do => :fix
92
+ after_transition any => :parked do |vehicle, transition|
93
+ vehicle.seatbelt_on = false
94
+ end
95
+
96
+ after_failure :on => :ignite, :do => :log_start_failure
97
+
98
+ around_transition do |vehicle, transition, block|
99
+ start = Time.now
100
+ block.call
101
+ vehicle.time_used += Time.now - start
102
+ end
103
+
104
+ event :park do
105
+ transition [:idling, :first_gear] => :parked
106
+ end
107
+
108
+ event :ignite do
109
+ transition :stalled => same, :parked => :idling
110
+ end
111
+
112
+ event :idle do
113
+ transition :first_gear => :idling
114
+ end
115
+
116
+ event :shift_up do
117
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
118
+ end
119
+
120
+ event :shift_down do
121
+ transition :third_gear => :second_gear, :second_gear => :first_gear
122
+ end
123
+
124
+ event :crash do
125
+ transition all - [:parked, :stalled] => :stalled, :if => lambda {|vehicle| !vehicle.passed_inspection?}
126
+ end
127
+
128
+ event :repair do
129
+ # The first transition that matches the state and passes its conditions
130
+ # will be used
131
+ transition :stalled => :parked, :unless => :auto_shop_busy
132
+ transition :stalled => same
133
+ end
134
+
135
+ state :parked do
136
+ def speed
137
+ 0
138
+ end
139
+ end
140
+
141
+ state :idling, :first_gear do
142
+ def speed
143
+ 10
144
+ end
145
+ end
146
+
147
+ state all - [:parked, :stalled, :idling] do
148
+ def moving?
149
+ true
150
+ end
151
+ end
152
+
153
+ state :parked, :stalled, :idling do
154
+ def moving?
155
+ false
156
+ end
157
+ end
158
+ end
159
+
160
+ state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
161
+ event :enable do
162
+ transition all => :active
163
+ end
164
+
165
+ event :disable do
166
+ transition all => :off
167
+ end
168
+
169
+ state :active, :value => 1
170
+ state :off, :value => 0
171
+ end
172
+
173
+ def initialize
174
+ @seatbelt_on = false
175
+ @time_used = 0
176
+ @auto_shop_busy = true
177
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
178
+ end
179
+
180
+ def put_on_seatbelt
181
+ @seatbelt_on = true
182
+ end
183
+
184
+ def passed_inspection?
185
+ false
186
+ end
187
+
188
+ def tow
189
+ # tow the vehicle
190
+ end
191
+
192
+ def fix
193
+ # get the vehicle fixed by a mechanic
194
+ end
195
+
196
+ def log_start_failure
197
+ # log a failed attempt to start the vehicle
198
+ end
199
+ end
200
+ ```
201
+
202
+ **Note** the comment made on the `initialize` method in the class. In order for
203
+ state machine attributes to be properly initialized, `super()` must be called.
204
+ See `StateMachine::MacroMethods` for more information about this.
205
+
206
+ Using the above class as an example, you can interact with the state machine
207
+ like so:
208
+
209
+ ```ruby
210
+ vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
211
+ vehicle.state # => "parked"
212
+ vehicle.state_name # => :parked
213
+ vehicle.human_state_name # => "parked"
214
+ vehicle.parked? # => true
215
+ vehicle.can_ignite? # => true
216
+ vehicle.ignite_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
217
+ vehicle.state_events # => [:ignite]
218
+ vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
219
+ vehicle.speed # => 0
220
+ vehicle.moving? # => false
221
+
222
+ vehicle.ignite # => true
223
+ vehicle.parked? # => false
224
+ vehicle.idling? # => true
225
+ vehicle.speed # => 10
226
+ vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
227
+
228
+ vehicle.shift_up # => true
229
+ vehicle.speed # => 10
230
+ vehicle.moving? # => true
231
+ vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
232
+
233
+ vehicle.shift_up # => true
234
+ # Call state-driven behavior that's undefined for the state raises a NoMethodError
235
+ vehicle.speed # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
236
+ vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
237
+
238
+ # The bang (!) operator can raise exceptions if the event fails
239
+ vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from :second_gear
240
+
241
+ # Generic state predicates can raise exceptions if the value does not exist
242
+ vehicle.state?(:parked) # => false
243
+ vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
244
+
245
+ # Namespaced machines have uniquely-generated methods
246
+ vehicle.alarm_state # => 1
247
+ vehicle.alarm_state_name # => :active
248
+
249
+ vehicle.can_disable_alarm? # => true
250
+ vehicle.disable_alarm # => true
251
+ vehicle.alarm_state # => 0
252
+ vehicle.alarm_state_name # => :off
253
+ vehicle.can_enable_alarm? # => true
254
+
255
+ vehicle.alarm_off? # => true
256
+ vehicle.alarm_active? # => false
257
+
258
+ # Events can be fired in parallel
259
+ vehicle.fire_events(:shift_down, :enable_alarm) # => true
260
+ vehicle.state_name # => :first_gear
261
+ vehicle.alarm_state_name # => :active
262
+
263
+ vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachine::InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
264
+
265
+ # Human-friendly names can be accessed for states/events
266
+ Vehicle.human_state_name(:first_gear) # => "first gear"
267
+ Vehicle.human_alarm_state_name(:active) # => "active"
268
+
269
+ Vehicle.human_state_event_name(:shift_down) # => "shift down"
270
+ Vehicle.human_alarm_state_event_name(:enable) # => "enable"
271
+
272
+ # States / events can also be references by the string version of their name
273
+ Vehicle.human_state_name('first_gear') # => "first gear"
274
+ Vehicle.human_state_event_name('shift_down') # => "shift down"
275
+
276
+ # Available transition paths can be analyzed for an object
277
+ vehicle.state_paths # => [[#<StateMachine::Transition ...], [#<StateMachine::Transition ...], ...]
278
+ vehicle.state_paths.to_states # => [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
279
+ vehicle.state_paths.events # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]
280
+
281
+ # Find all paths that start and end on certain states
282
+ vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
283
+ # #<StateMachine::Transition attribute=:state event=:ignite from="parked" ...>,
284
+ # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" ...>
285
+ # ]]
286
+ # Skipping state_machine and writing to attributes directly
287
+ vehicle.state = "parked"
288
+ vehicle.state # => "parked"
289
+ vehicle.state_name # => :parked
290
+
291
+ # *Note* that the following is not supported (see StateMachine::MacroMethods#state_machine):
292
+ # vehicle.state = :parked
293
+ ```
294
+
295
+ ## Integrations
296
+
297
+ In addition to being able to define state machines on all Ruby classes, a set of
298
+ out-of-the-box integrations are available for some of the more popular Ruby
299
+ libraries. These integrations add library-specific behavior, allowing for state
300
+ machines to work more tightly with the conventions defined by those libraries.
301
+
302
+ The integrations currently available include:
303
+
304
+ * ActiveModel classes
305
+ * ActiveRecord models
306
+ * DataMapper resources
307
+ * Mongoid models
308
+ * MongoMapper models
309
+ * Sequel models
310
+
311
+ A brief overview of these integrations is described below.
312
+
313
+ ### ActiveModel
314
+
315
+ The ActiveModel integration is useful for both standalone usage and for providing
316
+ the base implementation for ORMs which implement the ActiveModel API. This
317
+ integration adds support for validation errors, dirty attribute tracking, and
318
+ observers. For example,
319
+
320
+ ```ruby
321
+ class Vehicle
322
+ include ActiveModel::Dirty
323
+ include ActiveModel::Validations
324
+ include ActiveModel::Observing
325
+
326
+ attr_accessor :state
327
+ define_attribute_methods [:state]
328
+
329
+ state_machine :initial => :parked do
330
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
331
+ after_transition any => :parked do |vehicle, transition|
332
+ vehicle.seatbelt = 'off'
333
+ end
334
+ around_transition :benchmark
335
+
336
+ event :ignite do
337
+ transition :parked => :idling
338
+ end
339
+
340
+ state :first_gear, :second_gear do
341
+ validates_presence_of :seatbelt_on
342
+ end
343
+ end
344
+
345
+ def put_on_seatbelt
346
+ ...
347
+ end
348
+
349
+ def benchmark
350
+ ...
351
+ yield
352
+ ...
353
+ end
354
+ end
355
+
356
+ class VehicleObserver < ActiveModel::Observer
357
+ # Callback for :ignite event *before* the transition is performed
358
+ def before_ignite(vehicle, transition)
359
+ # log message
360
+ end
361
+
362
+ # Generic transition callback *after* the transition is performed
363
+ def after_transition(vehicle, transition)
364
+ Audit.log(vehicle, transition)
365
+ end
366
+
367
+ # Generic callback after the transition fails to perform
368
+ def after_failure_to_transition(vehicle, transition)
369
+ Audit.error(vehicle, transition)
370
+ end
371
+ end
372
+ ```
373
+
374
+ For more information about the various behaviors added for ActiveModel state
375
+ machines and how to build new integrations that use ActiveModel, see
376
+ `StateMachine::Integrations::ActiveModel`.
377
+
378
+ ### ActiveRecord
379
+
380
+ The ActiveRecord integration adds support for database transactions, automatically
381
+ saving the record, named scopes, validation errors, and observers. For example,
382
+
383
+ ```ruby
384
+ class Vehicle < ActiveRecord::Base
385
+ state_machine :initial => :parked do
386
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
387
+ after_transition any => :parked do |vehicle, transition|
388
+ vehicle.seatbelt = 'off'
389
+ end
390
+ around_transition :benchmark
391
+
392
+ event :ignite do
393
+ transition :parked => :idling
394
+ end
395
+
396
+ state :first_gear, :second_gear do
397
+ validates_presence_of :seatbelt_on
398
+ end
399
+ end
400
+
401
+ def put_on_seatbelt
402
+ ...
403
+ end
404
+
405
+ def benchmark
406
+ ...
407
+ yield
408
+ ...
409
+ end
410
+ end
411
+
412
+ class VehicleObserver < ActiveRecord::Observer
413
+ # Callback for :ignite event *before* the transition is performed
414
+ def before_ignite(vehicle, transition)
415
+ # log message
416
+ end
417
+
418
+ # Generic transition callback *after* the transition is performed
419
+ def after_transition(vehicle, transition)
420
+ Audit.log(vehicle, transition)
421
+ end
422
+ end
423
+ ```
424
+
425
+ For more information about the various behaviors added for ActiveRecord state
426
+ machines, see `StateMachine::Integrations::ActiveRecord`.
427
+
428
+ ### DataMapper
429
+
430
+ Like the ActiveRecord integration, the DataMapper integration adds support for
431
+ database transactions, automatically saving the record, named scopes, Extlib-like
432
+ callbacks, validation errors, and observers. For example,
433
+
434
+ ```ruby
435
+ class Vehicle
436
+ include DataMapper::Resource
437
+
438
+ property :id, Serial
439
+ property :state, String
440
+
441
+ state_machine :initial => :parked do
442
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
443
+ after_transition any => :parked do |transition|
444
+ self.seatbelt = 'off' # self is the record
445
+ end
446
+ around_transition :benchmark
447
+
448
+ event :ignite do
449
+ transition :parked => :idling
450
+ end
451
+
452
+ state :first_gear, :second_gear do
453
+ validates_presence_of :seatbelt_on
454
+ end
455
+ end
456
+
457
+ def put_on_seatbelt
458
+ ...
459
+ end
460
+
461
+ def benchmark
462
+ ...
463
+ yield
464
+ ...
465
+ end
466
+ end
467
+
468
+ class VehicleObserver
469
+ include DataMapper::Observer
470
+
471
+ observe Vehicle
472
+
473
+ # Callback for :ignite event *before* the transition is performed
474
+ before_transition :on => :ignite do |transition|
475
+ # log message (self is the record)
476
+ end
477
+
478
+ # Generic transition callback *after* the transition is performed
479
+ after_transition do |transition|
480
+ Audit.log(self, transition) # self is the record
481
+ end
482
+
483
+ around_transition do |transition, block|
484
+ # mark start time
485
+ block.call
486
+ # mark stop time
487
+ end
488
+
489
+ # Generic callback after the transition fails to perform
490
+ after_transition_failure do |transition|
491
+ Audit.log(self, transition) # self is the record
492
+ end
493
+ end
494
+ ```
495
+
496
+ **Note** that the DataMapper::Observer integration is optional and only available
497
+ when the dm-observer library is installed.
498
+
499
+ For more information about the various behaviors added for DataMapper state
500
+ machines, see `StateMachine::Integrations::DataMapper`.
501
+
502
+ ### Mongoid
503
+
504
+ The Mongoid integration adds support for automatically saving the record,
505
+ basic scopes, validation errors, and observers. For example,
506
+
507
+ ```ruby
508
+ class Vehicle
509
+ include Mongoid::Document
510
+
511
+ state_machine :initial => :parked do
512
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
513
+ after_transition any => :parked do |vehicle, transition|
514
+ vehicle.seatbelt = 'off' # self is the record
515
+ end
516
+ around_transition :benchmark
517
+
518
+ event :ignite do
519
+ transition :parked => :idling
520
+ end
521
+
522
+ state :first_gear, :second_gear do
523
+ validates_presence_of :seatbelt_on
524
+ end
525
+ end
526
+
527
+ def put_on_seatbelt
528
+ ...
529
+ end
530
+
531
+ def benchmark
532
+ ...
533
+ yield
534
+ ...
535
+ end
536
+ end
537
+
538
+ class VehicleObserver < Mongoid::Observer
539
+ # Callback for :ignite event *before* the transition is performed
540
+ def before_ignite(vehicle, transition)
541
+ # log message
542
+ end
543
+
544
+ # Generic transition callback *after* the transition is performed
545
+ def after_transition(vehicle, transition)
546
+ Audit.log(vehicle, transition)
547
+ end
548
+ end
549
+ ```
550
+
551
+ For more information about the various behaviors added for Mongoid state
552
+ machines, see `StateMachine::Integrations::Mongoid`.
553
+
554
+ ### MongoMapper
555
+
556
+ The MongoMapper integration adds support for automatically saving the record,
557
+ basic scopes, validation errors and callbacks. For example,
558
+
559
+ ```ruby
560
+ class Vehicle
561
+ include MongoMapper::Document
562
+
563
+ state_machine :initial => :parked do
564
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
565
+ after_transition any => :parked do |vehicle, transition|
566
+ vehicle.seatbelt = 'off' # self is the record
567
+ end
568
+ around_transition :benchmark
569
+
570
+ event :ignite do
571
+ transition :parked => :idling
572
+ end
573
+
574
+ state :first_gear, :second_gear do
575
+ validates_presence_of :seatbelt_on
576
+ end
577
+ end
578
+
579
+ def put_on_seatbelt
580
+ ...
581
+ end
582
+
583
+ def benchmark
584
+ ...
585
+ yield
586
+ ...
587
+ end
588
+ end
589
+ ```
590
+
591
+ For more information about the various behaviors added for MongoMapper state
592
+ machines, see `StateMachine::Integrations::MongoMapper`.
593
+
594
+ ### Sequel
595
+
596
+ Like the ActiveRecord integration, the Sequel integration adds support for
597
+ database transactions, automatically saving the record, named scopes, validation
598
+ errors and callbacks. For example,
599
+
600
+ ```ruby
601
+ class Vehicle < Sequel::Model
602
+ state_machine :initial => :parked do
603
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
604
+ after_transition any => :parked do |transition|
605
+ self.seatbelt = 'off' # self is the record
606
+ end
607
+ around_transition :benchmark
608
+
609
+ event :ignite do
610
+ transition :parked => :idling
611
+ end
612
+
613
+ state :first_gear, :second_gear do
614
+ validates_presence_of :seatbelt_on
615
+ end
616
+ end
617
+
618
+ def put_on_seatbelt
619
+ ...
620
+ end
621
+
622
+ def benchmark
623
+ ...
624
+ yield
625
+ ...
626
+ end
627
+ end
628
+ ```
629
+
630
+ For more information about the various behaviors added for Sequel state
631
+ machines, see `StateMachine::Integrations::Sequel`.
632
+
633
+ ## Syntax flexibility
634
+
635
+ Although state_machine introduces a simplified syntax, it still remains
636
+ backwards compatible with previous versions and other state-related libraries by
637
+ providing some flexibility around how transitions are defined. See below for an
638
+ overview of these syntaxes.
639
+
640
+ ### Verbose syntax
641
+
642
+ In general, it's recommended that state machines use the implicit syntax for
643
+ transitions. However, you can be a little more explicit and verbose about
644
+ transitions by using the `:from`, `:except_from`, `:to`,
645
+ and `:except_to` options.
646
+
647
+ For example, transitions and callbacks can be defined like so:
648
+
649
+ ```ruby
650
+ class Vehicle
651
+ state_machine :initial => :parked do
652
+ before_transition :from => :parked, :except_to => :parked, :do => :put_on_seatbelt
653
+ after_transition :to => :parked do |transition|
654
+ self.seatbelt = 'off' # self is the record
655
+ end
656
+
657
+ event :ignite do
658
+ transition :from => :parked, :to => :idling
659
+ end
660
+ end
661
+ end
662
+ ```
663
+
664
+ ### Transition context
665
+
666
+ Some flexibility is provided around the context in which transitions can be
667
+ defined. In almost all examples throughout the documentation, transitions are
668
+ defined within the context of an event. If you prefer to have state machines
669
+ defined in the context of a **state** either out of preference or in order to
670
+ easily migrate from a different library, you can do so as shown below:
671
+
672
+ ```ruby
673
+ class Vehicle
674
+ state_machine :initial => :parked do
675
+ ...
676
+
677
+ state :parked do
678
+ transition :to => :idling, :on => [:ignite, :shift_up], :if => :seatbelt_on?
679
+
680
+ def speed
681
+ 0
682
+ end
683
+ end
684
+
685
+ state :first_gear do
686
+ transition :to => :second_gear, :on => :shift_up
687
+
688
+ def speed
689
+ 10
690
+ end
691
+ end
692
+
693
+ state :idling, :first_gear do
694
+ transition :to => :parked, :on => :park
695
+ end
696
+ end
697
+ end
698
+ ```
699
+
700
+ In the above example, there's no need to specify the +from+ state for each
701
+ transition since it's inferred from the context.
702
+
703
+ You can also define transitions completely outside the context of a particular
704
+ state / event. This may be useful in cases where you're building a state
705
+ machine from a data store instead of part of the class definition. See the
706
+ example below:
707
+
708
+ ```ruby
709
+ class Vehicle
710
+ state_machine :initial => :parked do
711
+ ...
712
+
713
+ transition :parked => :idling, :on => [:ignite, :shift_up]
714
+ transition :first_gear => :second_gear, :second_gear => :third_gear, :on => :shift_up
715
+ transition [:idling, :first_gear] => :parked, :on => :park
716
+ transition [:idling, :first_gear] => :parked, :on => :park
717
+ transition all - [:parked, :stalled] => :stalled, :unless => :auto_shop_busy?
718
+ end
719
+ end
720
+ ```
721
+
722
+ Notice that in these alternative syntaxes:
723
+
724
+ * You can continue to configure `:if` and `:unless` conditions
725
+ * You can continue to define `from` states (when in the machine context) using
726
+ the `all`, `any`, and `same` helper methods
727
+
728
+ ## Static / Dynamic definitions
729
+
730
+ In most cases, the definition of a state machine is **static**. That is to say,
731
+ the states, events and possible transitions are known ahead of time even though
732
+ they may depend on data that's only known at runtime. For example, certain
733
+ transitions may only be available depending on an attribute on that object it's
734
+ being run on. All of the documentation in this library define static machines
735
+ like so:
736
+
737
+ ```ruby
738
+ class Vehicle
739
+ state_machine :state, :initial => :parked do
740
+ event :park do
741
+ transition [:idling, :first_gear] => :parked
742
+ end
743
+
744
+ ...
745
+ end
746
+ end
747
+ ```
748
+
749
+ However, there may be cases where the definition of a state machine is **dynamic**.
750
+ This means that you don't know the possible states or events for a machine until
751
+ runtime. For example, you may allow users in your application to manage the
752
+ state machine of a project or task in your system. This means that the list of
753
+ transitions (and their associated states / events) could be stored externally,
754
+ such as in a database. In a case like this, you can define dynamically-generated
755
+ state machines like so:
756
+
757
+ ```ruby
758
+ class Vehicle
759
+ attr_accessor :state
760
+
761
+ # Replace this with an external source (like a db)
762
+ def transitions
763
+ [
764
+ {:parked => :idling, :on => :ignite},
765
+ {:idling => :first_gear, :first_gear => :second_gear, :on => :shift_up}
766
+ # ...
767
+ ]
768
+ end
769
+
770
+ # Create a state machine for this vehicle instance dynamically based on the
771
+ # transitions defined from the source above
772
+ def machine
773
+ vehicle = self
774
+ @machine ||= Machine.new(vehicle, :initial => :parked) do
775
+ vehicle.transitions.each {|attrs| transition(attrs)}
776
+
777
+ # Persist the state on the vehicle itself
778
+ after_transition do
779
+ vehicle.state = vehicle.machine.state
780
+ vehicle.save
781
+ end
782
+ end
783
+ end
784
+
785
+ def save
786
+ # Save the state change...
787
+ end
788
+ end
789
+
790
+ # Generic class for building machines
791
+ class Machine
792
+ def self.new(object, *args, &block)
793
+ machine = Class.new do
794
+ def definition
795
+ self.class.state_machine
796
+ end
797
+ end
798
+ machine.state_machine(*args, &block)
799
+ machine.new
800
+ end
801
+ end
802
+
803
+ vehicle = Vehicle.new # => #<Vehicle:0xb7236b50>
804
+ vehicle.machine # => #<#<Class:0xb723541c>:0xb722fa30 @state="parked">
805
+ vehicle.machine.state # => "parked"
806
+ vehicle.machine.ignite # => true
807
+ vehicle.machine.state # => "idling
808
+ vehicle.state # => "idling"
809
+ vehicle.machine.state_transitions # => [#<StateMachine::Transition ...>]
810
+ vehicle.machine.definition.states.keys # => :first_gear, :second_gear, :parked, :idling
811
+ ```
812
+
813
+ As you can see, state_machine provides enough flexibility for you to be able
814
+ to create new machine definitions on the fly based on an external source of
815
+ transitions.
816
+
817
+ ## Tools
818
+
819
+ ### Generating graphs
820
+
821
+ This library comes with built-in support for generating di-graphs based on the
822
+ events, states, and transitions defined for a state machine using [GraphViz](http://www.graphviz.org]).
823
+ This requires that both the `ruby-graphviz` gem and graphviz library be
824
+ installed on the system.
825
+
826
+ #### Examples
827
+
828
+ To generate a graph for a specific file / class:
829
+
830
+ ```bash
831
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle
832
+ ```
833
+
834
+ To save files to a specific path:
835
+
836
+ ```bash
837
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files
838
+ ```
839
+
840
+ To customize the image format / orientation:
841
+
842
+ ```bash
843
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg ORIENTATION=landscape
844
+ ```
845
+
846
+ To generate multiple state machine graphs:
847
+
848
+ ```bash
849
+ rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
850
+ ```
851
+
852
+ **Note** that this will generate a different file for every state machine defined
853
+ in the class. The generated files will use an output filename of the format
854
+ `#{class_name}_#{machine_name}.#{format}`.
855
+
856
+ For examples of actual images generated using this task, see those under the
857
+ examples folder.
858
+
859
+ ### Interactive graphs
860
+
861
+ Jean Bovet's [Visual Automata Simulator](http://www.cs.usfca.edu/~jbovet/vas.html)
862
+ is a great tool for "simulating, visualizing and transforming finite state
863
+ automata and Turing Machines". It can help in the creation of states and events
864
+ for your models. It is cross-platform, written in Java.
865
+
866
+ ## Web Frameworks
867
+
868
+ ### Ruby on Rails
869
+
870
+ Integrating state_machine into your Ruby on Rails application is straightforward
871
+ and provides a few additional features specific to the framework. To get
872
+ started, following the steps below.
873
+
874
+ #### 1. Install the gem
875
+
876
+ If using Rails 2.x:
877
+
878
+ ```ruby
879
+ # In config/environment.rb
880
+ ...
881
+ Rails::Initializer.run do |config|
882
+ ...
883
+ config.gem 'state_machine', :version => '~> 1.0'
884
+ ...
885
+ end
886
+ ```
887
+
888
+ If using Rails 3.x or up:
889
+
890
+ ```ruby
891
+ # In Gemfile
892
+ ...
893
+ gem 'state_machine'
894
+ gem 'ruby-graphviz', :require => 'graphviz' # Optional: only required for graphing
895
+ ```
896
+
897
+ As usual, run `bundle install` to load the gems.
898
+
899
+ #### 2. Create a model
900
+
901
+ Create a model with a field to store the state, along with other any other
902
+ fields your application requires:
903
+
904
+ ```bash
905
+ $ rails generate model Vehicle state:string
906
+ $ rake db:migrate
907
+ ```
908
+
909
+ #### 3. Configure the state machine
910
+
911
+ Add the state machine to your model. Following the examples above,
912
+ *app/models/vehicle.rb* might become:
913
+
914
+ ```ruby
915
+ class Vehicle < ActiveRecord::Base
916
+ state_machine :initial => :parked do
917
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
918
+ ...
919
+ end
920
+ end
921
+ ```
922
+
923
+ #### Rake tasks
924
+
925
+ There is a special integration Rake task for generating state machines for
926
+ classes used in a Ruby on Rails application. This task will load the application
927
+ environment, meaning that it's unnecessary to specify the actual file to load.
928
+
929
+ For example,
930
+
931
+ ```bash
932
+ rake state_machine:draw CLASS=Vehicle
933
+ ```
934
+
935
+ If you are using this library as a gem in Rails 2.x, the following must be added
936
+ to the end of your application's Rakefile in order for the above task to work:
937
+
938
+ ```ruby
939
+ require 'tasks/state_machine'
940
+ ```
941
+
942
+ ### Merb
943
+
944
+ #### Rake tasks
945
+
946
+ Like Ruby on Rails, there is a special integration Rake task for generating
947
+ state machines for classes used in a Merb application. This task will load the
948
+ application environment, meaning that it's unnecessary to specify the actual
949
+ files to load.
950
+
951
+ For example,
952
+
953
+ ```bash
954
+ rake state_machine:draw CLASS=Vehicle
955
+ ```
956
+
957
+ ## Testing
958
+
959
+ To run the core test suite (does **not** test any of the integrations):
960
+
961
+ ```bash
962
+ bundle install
963
+ bundle exec rake test
964
+ ```
965
+
966
+ To run integration tests:
967
+
968
+ ```bash
969
+ bundle install
970
+ rake appraisal:install
971
+ rake appraisal:test
972
+ ```
973
+
974
+ You can also test a specific version:
975
+
976
+ ```bash
977
+ rake appraisal:active_model-3.0.0 test
978
+ rake appraisal:active_record-2.0.0 test
979
+ rake appraisal:data_mapper-0.9.4 test
980
+ rake appraisal:mongoid-2.0.0 test
981
+ rake appraisal:mongo_mapper-0.5.5 test
982
+ rake appraisal:sequel-2.8.0 test
983
+ ```
984
+
985
+ ## Caveats
986
+
987
+ The following caveats should be noted when using state_machine:
988
+
989
+ * Overridden event methods won't get invoked when using attribute-based event transitions
990
+ * **DataMapper**: Attribute-based event transitions are disabled when using dm-validations 0.9.4 - 0.9.6
991
+ * **JRuby**: around_transition callbacks in ORM integrations won't work on JRuby since it doesn't support continuations
992
+ * **Factory Girl**: Dynamic initial states don't work because of the way factory_girl
993
+ builds objects. You can work around this in a few ways:
994
+ 1. Use a default state that is common across all objects and rely on events to
995
+ determine the actual initial state for your object.
996
+ 2. Assuming you're not using state-driven behavior on initialization, you can
997
+ re-initialize states after the fact:
998
+
999
+ ```ruby
1000
+ # Re-initialize in FactoryGirl
1001
+ FactoryGirl.define do
1002
+ factory :vehicle do
1003
+ after_build {|user| user.send(:initialize_state_machines, :dynamic => :force)}
1004
+ end
1005
+ end
1006
+
1007
+ # Alternatively re-initialize in your model
1008
+ class Vehicle < ActiveRecord::Base
1009
+ ...
1010
+ before_validation :on => :create {|user| user.send(:initialize_state_machines, :dynamic => :force)}
1011
+ end
1012
+ ```
1013
+
1014
+ ## Dependencies
1015
+
1016
+ * Ruby 1.8.6 or later
1017
+
1018
+ If using specific integrations:
1019
+
1020
+ * [ActiveModel](http://rubyonrails.org) integration: 3.0.0 or later
1021
+ * [ActiveRecord](http://rubyonrails.org) integration: 2.0.0 or later
1022
+ * [DataMapper](http://datamapper.org) integration: 0.9.4 or later
1023
+ * [Mongoid](http://mongoid.org) integration: 2.0.0 or later
1024
+ * [MongoMapper](http://mongomapper.com) integration: 0.5.5 or later
1025
+ * [Sequel](http://sequel.rubyforge.org) integration: 2.8.0 or later
1026
+
1027
+ If graphing state machine:
1028
+
1029
+ * [ruby-graphviz](http://github.com/glejeune/Ruby-Graphviz): 0.9.0 or later