state_machine 1.0.2 → 1.0.3

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