pluginaweek / state_machine

Adds support for creating state machines for attributes on any Ruby class

This URL has Read+Write access

state_machine / README.rdoc
f3565041 » obrie 2008-05-04 Completely rewritten from s... 1 == state_machine
03ead942 » obrie 2006-10-21 Initial import. 2
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 3 +state_machine+ adds support for creating state machines for attributes on any
4 Ruby class.
03ead942 » obrie 2006-10-21 Initial import. 5
29c1d69f » obrie 2007-01-01 Removed install.rb files. 6 == Resources
7
1c257492 » obrie 2007-09-26 Update README format 8 API
29c1d69f » obrie 2007-01-01 Removed install.rb files. 9
f3565041 » obrie 2008-05-04 Completely rewritten from s... 10 * http://api.pluginaweek.org/state_machine
29c1d69f » obrie 2007-01-01 Removed install.rb files. 11
695fcf70 » obrie 2008-06-25 Update list of plugin resou... 12 Bugs
13
14 * http://pluginaweek.lighthouseapp.com/projects/13288-state_machine
15
29c1d69f » obrie 2007-01-01 Removed install.rb files. 16 Development
17
695fcf70 » obrie 2008-06-25 Update list of plugin resou... 18 * http://github.com/pluginaweek/state_machine
29c1d69f » obrie 2007-01-01 Removed install.rb files. 19
1c257492 » obrie 2007-09-26 Update README format 20 Source
21
695fcf70 » obrie 2008-06-25 Update list of plugin resou... 22 * git://github.com/pluginaweek/state_machine.git
1c257492 » obrie 2007-09-26 Update README format 23
29c1d69f » obrie 2007-01-01 Removed install.rb files. 24 == Description
25
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 26 State machines make it dead-simple to manage the behavior of a class. Too often,
27 the status of an object is kept by creating multiple boolean attributes and
28 deciding how to behave based on the values. This can become cumbersome and
29 difficult to maintain when the complexity of your class starts to increase.
29c1d69f » obrie 2007-01-01 Removed install.rb files. 30
179ce54f » obrie 2008-07-05 Add more descriptive except... 31 +state_machine+ simplifies this design by introducing the various parts of a real
b68d4bda » obrie 2008-12-14 Add Sequel support 32 state machine, including states, events, transitions, and callbacks. However,
33 the api is designed to be so simple you don't even need to know what a
34 state machine is :)
35
36 Some brief, high-level features include:
37 * Defining state machines on any Ruby class
38 * Multiple state machines on a single class
cfa7757d » obrie 2008-12-17 Add :namespace option for g... 39 * Namespaced state machines
b68d4bda » obrie 2008-12-14 Add Sequel support 40 * before/after transition hooks with explicit transition requirements
41 * ActiveRecord integration
42 * DataMapper integration
43 * Sequel integration
44 * States of any data type
48d83dfb » obrie 2008-12-14 Add generic attribute predi... Comment 45 * State predicates
db88b6fa » obrie 2008-12-19 Add support for state-drive... 46 * State-driven behavior
b68d4bda » obrie 2008-12-14 Add Sequel support 47 * GraphViz visualization creator
48
49 Examples of the usage patterns for some of the above features are shown below.
50 You can find more detailed documentation in the actual API.
8563ca6b » obrie 2007-09-21 More documentation 51
52 == Usage
29c1d69f » obrie 2007-01-01 Removed install.rb files. 53
f3565041 » obrie 2008-05-04 Completely rewritten from s... 54 === Example
55
d71fedbe » obrie 2008-12-18 Simplify initialize hooks, ... 56 Below is an example of many of the features offered by this plugin, including:
179ce54f » obrie 2008-07-05 Add more descriptive except... 57 * Initial states
db88b6fa » obrie 2008-12-19 Add support for state-drive... 58 * Namespaced states
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 59 * Transition callbacks
179ce54f » obrie 2008-07-05 Add more descriptive except... 60 * Conditional transitions
db88b6fa » obrie 2008-12-19 Add support for state-drive... 61 * State-driven behavior
179ce54f » obrie 2008-07-05 Add more descriptive except... 62
d71fedbe » obrie 2008-12-18 Simplify initialize hooks, ... 63 Class definition:
64
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 65 class Vehicle
66 attr_accessor :seatbelt_on
67
c02e3dc0 » obrie 2008-10-04 Fix state machine examples ... 68 state_machine :state, :initial => 'parked' do
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 69 before_transition :from => %w(parked idling), :do => :put_on_seatbelt
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 70 after_transition :on => 'crash', :do => :tow
71 after_transition :on => 'repair', :do => :fix
72 after_transition :to => 'parked' do |vehicle, transition|
73 vehicle.seatbelt_on = false
74 end
f3565041 » obrie 2008-05-04 Completely rewritten from s... 75
76 event :park do
77 transition :to => 'parked', :from => %w(idling first_gear)
78 end
79
80 event :ignite do
81 transition :to => 'stalled', :from => 'stalled'
82 transition :to => 'idling', :from => 'parked'
83 end
84
85 event :idle do
86 transition :to => 'idling', :from => 'first_gear'
87 end
88
89 event :shift_up do
90 transition :to => 'first_gear', :from => 'idling'
91 transition :to => 'second_gear', :from => 'first_gear'
92 transition :to => 'third_gear', :from => 'second_gear'
93 end
94
95 event :shift_down do
96 transition :to => 'second_gear', :from => 'third_gear'
97 transition :to => 'first_gear', :from => 'second_gear'
98 end
99
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 100 event :crash do
f3565041 » obrie 2008-05-04 Completely rewritten from s... 101 transition :to => 'stalled', :from => %w(first_gear second_gear third_gear), :unless => :auto_shop_busy?
102 end
103
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 104 event :repair do
f3565041 » obrie 2008-05-04 Completely rewritten from s... 105 transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
106 end
db88b6fa » obrie 2008-12-19 Add support for state-drive... 107
108 state 'parked' do
109 def speed
110 0
111 end
112 end
113
114 state 'idling', 'first_gear' do
115 def speed
116 10
117 end
118 end
119
120 state 'second_gear' do
121 def speed
122 20
123 end
124 end
f3565041 » obrie 2008-05-04 Completely rewritten from s... 125 end
179ce54f » obrie 2008-07-05 Add more descriptive except... 126
cfa7757d » obrie 2008-12-17 Add :namespace option for g... 127 state_machine :hood_state, :initial => 'closed', :namespace => 'hood' do
128 event :open do
129 transition :to => 'opened', :from => 'closed'
130 end
131
132 event :close do
133 transition :to => 'closed', :from => 'opened'
134 end
135 end
136
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 137 def initialize
138 @seatbelt_on = false
d71fedbe » obrie 2008-12-18 Simplify initialize hooks, ... 139 super() # NOTE: This *must* be called, otherwise states won't get initialized
179ce54f » obrie 2008-07-05 Add more descriptive except... 140 end
141
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 142 def put_on_seatbelt
143 @seatbelt_on = true
179ce54f » obrie 2008-07-05 Add more descriptive except... 144 end
145
146 def auto_shop_busy?
147 false
148 end
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 149
150 def tow
151 # tow the vehicle
152 end
153
154 def fix
155 # get the vehicle fixed by a mechanic
156 end
f3565041 » obrie 2008-05-04 Completely rewritten from s... 157 end
b5066674 » obrie 2007-09-21 Move test fixtures out of t... 158
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 159 Using the above class as an example, you can interact with the state machine
179ce54f » obrie 2008-07-05 Add more descriptive except... 160 like so:
161
b68d4bda » obrie 2008-12-14 Add Sequel support 162 vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
48d83dfb » obrie 2008-12-14 Add generic attribute predi... Comment 163 vehicle.parked? # => true
b68d4bda » obrie 2008-12-14 Add Sequel support 164 vehicle.can_ignite? # => true
146afff4 » obrie 2008-12-14 Remove the PluginAWeek name... 165 vehicle.next_ignite_transition # => #<StateMachine::Transition:0xb7c34cec ...>
37fc8a67 » obrie 2008-12-19 Fix example in README 166 vehicle.speed # => 0
167
b68d4bda » obrie 2008-12-14 Add Sequel support 168 vehicle.ignite # => true
48d83dfb » obrie 2008-12-14 Add generic attribute predi... Comment 169 vehicle.parked? # => false
170 vehicle.idling? # => true
37fc8a67 » obrie 2008-12-19 Fix example in README 171 vehicle.speed # => 10
b68d4bda » obrie 2008-12-14 Add Sequel support 172 vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
37fc8a67 » obrie 2008-12-19 Fix example in README 173
b68d4bda » obrie 2008-12-14 Add Sequel support 174 vehicle.shift_up # => true
db88b6fa » obrie 2008-12-19 Add support for state-drive... 175 vehicle.speed # => 10
37fc8a67 » obrie 2008-12-19 Fix example in README 176 vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
177
b68d4bda » obrie 2008-12-14 Add Sequel support 178 vehicle.shift_up # => true
db88b6fa » obrie 2008-12-19 Add support for state-drive... 179 vehicle.speed # => 20
37fc8a67 » obrie 2008-12-19 Fix example in README 180 vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
179ce54f » obrie 2008-07-05 Add more descriptive except... 181
182 # The bang (!) operator can raise exceptions if the event fails
146afff4 » obrie 2008-12-14 Remove the PluginAWeek name... 183 vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
48d83dfb » obrie 2008-12-14 Add generic attribute predi... Comment 184
185 # Generic state predicates can raise exceptions if the value does not exist
186 vehicle.state?('parked') # => true
187 vehicle.state?('invalid') # => ArgumentError: "parked" is not a known state value
cfa7757d » obrie 2008-12-17 Add :namespace option for g... 188
189 # Namespaced machines have uniquely-generated methods
190 vehicle.can_open_hood? # => true
191 vehicle.open_hood # => true
192 vehicle.can_close_hood? # => true
193
194 vehicle.hood_opened? # => true
195 vehicle.hood_closed? # => false
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 196
d71fedbe » obrie 2008-12-18 Simplify initialize hooks, ... 197 *Note* the comment made on the +initialize+ method in the class. In order for
198 state machine attributes to be properly initialized, <tt>super()</tt> must be called.
199 See StateMachine::MacroMethods for more information about this.
200
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 201 == Integrations
202
203 In addition to being able to define state machines on all Ruby classes, a set of
204 out-of-the-box integrations are available for some of the more popular Ruby
205 libraries. These integrations add library-specific behavior, allowing for state
206 machines to work more tightly with the conventions defined by those libraries.
207
208 The integrations currently available include:
209 * ActiveRecord models
210 * DataMapper resources
b68d4bda » obrie 2008-12-14 Add Sequel support 211 * Sequel models
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 212
213 A brief overview of these integrations is described below.
214
215 === ActiveRecord
216
217 The ActiveRecord integration adds support for database transactions, automatically
218 saving the record, named scopes, and observers. For example,
219
220 class Vehicle < ActiveRecord::Base
221 state_machine :initial => 'parked' do
222 before_transition :to => 'idling', :do => :put_on_seatbelt
223 after_transition :to => 'parked' do |vehicle, transition|
224 vehicle.seatbelt = 'off'
225 end
226
227 event :ignite do
228 transition :to => 'idling', :from => 'parked'
229 end
230 end
231
232 def put_on_seatbelt
233 ...
234 end
235 end
236
237 class VehicleObserver < ActiveRecord::Observer
238 # Callback for :ignite event *before* the transition is performed
239 def before_ignite(vehicle, transition)
240 # log message
241 end
242
243 # Generic transition callback *before* the transition is performed
244 def after_transition(vehicle, transition)
245 Audit.log(vehicle, transition)
246 end
247 end
248
249 For more information about the various behaviors added for ActiveRecord state
146afff4 » obrie 2008-12-14 Remove the PluginAWeek name... 250 machines, see StateMachine::Integrations::ActiveRecord.
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 251
252 ==== With enumerations
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 253
254 Using the acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration] plugin
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 255 with an ActiveRecord integration, states can be transparently stored using
256 record ids in the database like so:
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 257
258 class VehicleState < ActiveRecord::Base
259 acts_as_enumeration
260
261 create :id => 1, :name => 'parked'
262 create :id => 2, :name => 'idling'
263 ...
264 end
265
266 class Vehicle < ActiveRecord::Base
267 belongs_to :state, :class_name => 'VehicleState'
268
c02e3dc0 » obrie 2008-10-04 Fix state machine examples ... 269 state_machine :state, :initial => 'parked' do
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 270 ...
271
272 event :park do
273 transition :to => 'parked', :from => %w(idling first_gear)
274 end
275 end
276
277 ...
278 end
279
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 280 Notice that the state machine definition remains *exactly* the same. However,
281 when interacting with the records, the actual state will be stored using the
282 identifiers defined for the enumeration:
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 283
284 vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state_id: 1>
285 vehicle.ignite # => true
286 vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 2>
287
288 This allows states to take on more complex functionality other than just being
289 a string value.
179ce54f » obrie 2008-07-05 Add more descriptive except... 290
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 291 === DataMapper
292
293 Like the ActiveRecord integration, the DataMapper integration adds support for
294 database transactions, automatically saving the record, named scopes, Extlib-like
295 callbacks, and observers. For example,
296
297 class Vehicle
298 include DataMapper::Resource
299
300 property :id, Serial
301 property :state, String
302
303 state_machine :initial => 'parked' do
304 before_transition :to => 'idling', :do => :put_on_seatbelt
305 after_transition :to => 'parked' do |transition|
306 self.seatbelt = 'off' # self is the record
307 end
308
309 event :ignite do
310 transition :to => 'idling', :from => 'parked'
311 end
312 end
313
314 def put_on_seatbelt
315 ...
316 end
317 end
318
319 class VehicleObserver
320 include DataMapper::Observer
321
322 observe Vehicle
323
324 # Callback for :ignite event *before* the transition is performed
325 before_transition :on => :ignite do |transition|
326 # log message (self is the record)
327 end
328
329 # Generic transition callback *before* the transition is performed
330 after_transition do |transition, saved|
331 Audit.log(self, transition) if saved # self is the record
332 end
333 end
334
335 For more information about the various behaviors added for DataMapper state
146afff4 » obrie 2008-12-14 Remove the PluginAWeek name... 336 machines, see StateMachine::Integrations::DataMapper.
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 337
b68d4bda » obrie 2008-12-14 Add Sequel support 338 === Sequel
339
340 Like the ActiveRecord integration, the Sequel integration adds support for
341 database transactions, automatically saving the record, named scopes, and
342 callbacks. For example,
343
344 class Vehicle < Sequel::Model
345 state_machine :initial => 'parked' do
346 before_transition :to => 'idling', :do => :put_on_seatbelt
347 after_transition :to => 'parked' do |transition|
348 self.seatbelt = 'off' # self is the record
349 end
350
351 event :ignite do
352 transition :to => 'idling', :from => 'parked'
353 end
354 end
355
356 def put_on_seatbelt
357 ...
358 end
359 end
360
361 For more information about the various behaviors added for Sequel state
146afff4 » obrie 2008-12-14 Remove the PluginAWeek name... 362 machines, see StateMachine::Integrations::Sequel.
b68d4bda » obrie 2008-12-14 Add Sequel support 363
b5066674 » obrie 2007-09-21 Move test fixtures out of t... 364 == Tools
365
6f25124e » jashmenn 2008-12-08 Add rake tasks for generati... Comment 366 === Generating graphs
367
368 This library comes with built-in support for generating di-graphs based on the
369 events, states, and transitions defined for a state machine using GraphViz[http://www.graphviz.org].
370 This requires that both the <tt>ruby-graphviz</tt> gem and graphviz library be
371 installed on the system.
372
373 ==== Examples
374
375 To generate a graph for a specific file / class:
376
377 rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle
378
379 To save files to a specific path:
380
381 rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files
382
383 To customize the image format:
384
385 rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg
386
387 To generate multiple state machine graphs:
388
389 rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
390
391 *Note* that this will generate a different file for every state machine defined
0b8609e7 » obrie 2008-12-15 Tweak README 392 in the class. The generated files will use an output filename of the format
393 #{class_name}_#{attribute}.#{format}.
6f25124e » jashmenn 2008-12-08 Add rake tasks for generati... Comment 394
395 For examples of actual images generated using this task, see those under the
0b8609e7 » obrie 2008-12-15 Tweak README 396 examples folder.
6f25124e » jashmenn 2008-12-08 Add rake tasks for generati... Comment 397
398 ==== Ruby on Rails Integration
399
400 There is a special integration Rake task for generating state machines for
401 classes used in a Ruby on Rails application. This task will load the application
402 environment, meaning that it's unnecessary to specify the actual file to load.
403
404 For example,
405
406 rake state_machine:draw:rails CLASS=Vehicle
407
408 ==== Merb Integration
409
410 Like Ruby on Rails, there is a special integration Rake task for generating
411 state machines for classes used in a Merb application. This task will load the
412 application environment, meaning that it's unnecessary to specify the actual
413 files to load.
414
415 For example,
416
417 rake state_machine:draw:merb CLASS=Vehicle
418
419 === Interactive graphs
420
8563ca6b » obrie 2007-09-21 More documentation 421 Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html].
422 This is a great tool for "simulating, visualizing and transforming finite state
423 automata and Turing Machines". This tool can help in the creation of states and
424 events for your models. It is cross-platform, written in Java.
b5066674 » obrie 2007-09-21 Move test fixtures out of t... 425
f3565041 » obrie 2008-05-04 Completely rewritten from s... 426 == Testing
427
48d83dfb » obrie 2008-12-14 Add generic attribute predi... Comment 428 To run the entire test suite (will test ActiveRecord, DataMapper, and Sequel
0b8609e7 » obrie 2008-12-15 Tweak README 429 integrations if the proper dependencies are available):
594ad289 » obrie 2008-05-12 Add information about what ... 430
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 431 rake test
594ad289 » obrie 2008-05-12 Add information about what ... 432
433 == Dependencies
434
307ac8eb » obrie 2008-12-07 Re-design to be library-agn... 435 By default, there are no dependencies. If using specific integrations, those
436 dependencies are listed below.
437
438 * ActiveRecord[http://rubyonrails.org] integration: 2.1.0 or later
439 * DataMapper[http://datamapper.org] integration: 0.9.0 or later
b68d4bda » obrie 2008-12-14 Add Sequel support 440 * Sequel[http://sequel.rubyforge.org] integration: 2.8.0 or later
594ad289 » obrie 2008-05-12 Add information about what ... 441
8563ca6b » obrie 2007-09-21 More documentation 442 == References
443
6a94afcc » obrie 2008-09-06 MAJOR REWRITE! Replace all ... Comment 444 * acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration]