state_machine 0.3.1 → 0.4.0

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 (52) hide show
  1. data/CHANGELOG.rdoc +26 -0
  2. data/README.rdoc +254 -46
  3. data/Rakefile +29 -3
  4. data/examples/AutoShop_state.png +0 -0
  5. data/examples/Car_state.jpg +0 -0
  6. data/examples/Vehicle_state.png +0 -0
  7. data/lib/state_machine.rb +161 -116
  8. data/lib/state_machine/assertions.rb +21 -0
  9. data/lib/state_machine/callback.rb +168 -0
  10. data/lib/state_machine/eval_helpers.rb +67 -0
  11. data/lib/state_machine/event.rb +135 -101
  12. data/lib/state_machine/extensions.rb +83 -0
  13. data/lib/state_machine/guard.rb +115 -0
  14. data/lib/state_machine/integrations/active_record.rb +242 -0
  15. data/lib/state_machine/integrations/data_mapper.rb +198 -0
  16. data/lib/state_machine/integrations/data_mapper/observer.rb +153 -0
  17. data/lib/state_machine/integrations/sequel.rb +169 -0
  18. data/lib/state_machine/machine.rb +746 -352
  19. data/lib/state_machine/transition.rb +104 -212
  20. data/test/active_record.log +34865 -0
  21. data/test/classes/switch.rb +11 -0
  22. data/test/data_mapper.log +14015 -0
  23. data/test/functional/state_machine_test.rb +249 -15
  24. data/test/sequel.log +3835 -0
  25. data/test/test_helper.rb +3 -12
  26. data/test/unit/assertions_test.rb +13 -0
  27. data/test/unit/callback_test.rb +189 -0
  28. data/test/unit/eval_helpers_test.rb +92 -0
  29. data/test/unit/event_test.rb +247 -113
  30. data/test/unit/guard_test.rb +420 -0
  31. data/test/unit/integrations/active_record_test.rb +515 -0
  32. data/test/unit/integrations/data_mapper_test.rb +407 -0
  33. data/test/unit/integrations/sequel_test.rb +244 -0
  34. data/test/unit/invalid_transition_test.rb +1 -1
  35. data/test/unit/machine_test.rb +1056 -98
  36. data/test/unit/state_machine_test.rb +14 -113
  37. data/test/unit/transition_test.rb +269 -495
  38. metadata +44 -30
  39. data/test/app_root/app/models/auto_shop.rb +0 -34
  40. data/test/app_root/app/models/car.rb +0 -19
  41. data/test/app_root/app/models/highway.rb +0 -3
  42. data/test/app_root/app/models/motorcycle.rb +0 -3
  43. data/test/app_root/app/models/switch.rb +0 -23
  44. data/test/app_root/app/models/switch_observer.rb +0 -20
  45. data/test/app_root/app/models/toggle_switch.rb +0 -2
  46. data/test/app_root/app/models/vehicle.rb +0 -78
  47. data/test/app_root/config/environment.rb +0 -7
  48. data/test/app_root/db/migrate/001_create_switches.rb +0 -12
  49. data/test/app_root/db/migrate/002_create_auto_shops.rb +0 -13
  50. data/test/app_root/db/migrate/003_create_highways.rb +0 -11
  51. data/test/app_root/db/migrate/004_create_vehicles.rb +0 -16
  52. data/test/factory.rb +0 -77
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,31 @@
1
1
  == master
2
2
 
3
+ == 0.4.0 / 2008-12-14
4
+
5
+ * Remove the PluginAWeek namespace
6
+ * Add generic attribute predicate (e.g. "#{attribute}?(state_name)") and state predicates (e.g. "#{state}?")
7
+ * Add Sequel support
8
+ * Fix aliasing :initialize on ActiveRecord models causing warnings when the environment is reloaded
9
+ * Fix ActiveRecord state machines trying to query the database on unmigrated models
10
+ * Fix initial states not getting set when the current value is an empty string [Aaron Gibralter]
11
+ * Add rake tasks for generating graphviz files for state machines [Nate Murray]
12
+ * Fix initial state not being included in list of known states
13
+ * Add other_states directive for defining additional states not referenced in transitions or callbacks [Pete Forde]
14
+ * Add next_#{event}_transition for getting the next transition that would be performed if the event were invoked
15
+ * Add the ability to override the pluralized name of an attribute for creating scopes
16
+ * Add the ability to halt callback chains by: throw :halt
17
+ * Add support for dynamic to states in transitions (e.g. :to => lambda {Time.now})
18
+ * Add support for using real blocks in before_transition/after_transition calls instead of using the :do option
19
+ * Add DataMapper support
20
+ * Include states referenced in transition callbacks in the list of a machine's known states
21
+ * Only generate the known states for a machine on demand, rather than calculating beforehand
22
+ * Add the ability to skip state change actions during a transition (e.g. vehicle.ignite(false))
23
+ * Add the ability for the state change action (e.g. +save+ for ActiveRecord) to be configurable
24
+ * Allow state machines to be defined on *any* Ruby class, not just ActiveRecord (removes all external dependencies)
25
+ * Refactor transitions, guards, and callbacks for better organization/design
26
+ * Use a class containing the transition context in callbacks, rather than an ordered list of each individual attribute
27
+ * Add without_#{attribute} named scopes (opposite of the existing with_#{attribute} named scopes) [Sean O'Brien]
28
+
3
29
  == 0.3.1 / 2008-10-26
4
30
 
5
31
  * Fix the initial state not getting set when the state attribute is mass-assigned but protected
data/README.rdoc CHANGED
@@ -1,7 +1,7 @@
1
1
  == state_machine
2
2
 
3
- +state_machine+ adds support for creating state machines for attributes within
4
- a model.
3
+ +state_machine+ adds support for creating state machines for attributes on any
4
+ Ruby class.
5
5
 
6
6
  == Resources
7
7
 
@@ -23,16 +23,29 @@ Source
23
23
 
24
24
  == Description
25
25
 
26
- State machines make it dead-simple to manage the behavior of a model. Too often,
27
- the status of a record is kept by creating multiple boolean columns in the table
28
- and deciding how to behave based on the values in those columns. This can become
29
- cumbersome and difficult to maintain when the complexity of your models starts to
30
- increase.
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.
31
30
 
32
31
  +state_machine+ simplifies this design by introducing the various parts of a real
33
- state machine, including states, events, and transitions. However, the api is
34
- designed to be similar to ActiveRecord in terms of validations and callbacks,
35
- making it so simple you don't even need to know what a state machine is :)
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
39
+ * before/after transition hooks with explicit transition requirements
40
+ * ActiveRecord integration
41
+ * DataMapper integration
42
+ * Sequel integration
43
+ * States of any data type
44
+ * State predicates
45
+ * GraphViz visualization creator
46
+
47
+ Examples of the usage patterns for some of the above features are shown below.
48
+ You can find more detailed documentation in the actual API.
36
49
 
37
50
  == Usage
38
51
 
@@ -43,12 +56,16 @@ Below is an example of many of the features offered by this plugin, including
43
56
  * Transition callbacks
44
57
  * Conditional transitions
45
58
 
46
- class Vehicle < ActiveRecord::Base
59
+ class Vehicle
60
+ attr_accessor :seatbelt_on
61
+
47
62
  state_machine :state, :initial => 'parked' do
48
63
  before_transition :from => %w(parked idling), :do => :put_on_seatbelt
49
- after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.update_attribute(:seatbelt_on, false)}
50
- after_transition :on => 'crash', :do => :tow!
51
- after_transition :on => 'repair', :do => :fix!
64
+ after_transition :on => 'crash', :do => :tow
65
+ after_transition :on => 'repair', :do => :fix
66
+ after_transition :to => 'parked' do |vehicle, transition|
67
+ vehicle.seatbelt_on = false
68
+ end
52
69
 
53
70
  event :park do
54
71
  transition :to => 'parked', :from => %w(idling first_gear)
@@ -83,44 +100,112 @@ Below is an example of many of the features offered by this plugin, including
83
100
  end
84
101
  end
85
102
 
86
- def tow!
87
- # do something here
103
+ def initialize
104
+ @seatbelt_on = false
88
105
  end
89
106
 
90
- def fix!
91
- # do something here
107
+ def put_on_seatbelt
108
+ @seatbelt_on = true
92
109
  end
93
110
 
94
111
  def auto_shop_busy?
95
112
  false
96
113
  end
114
+
115
+ def tow
116
+ # tow the vehicle
117
+ end
118
+
119
+ def fix
120
+ # get the vehicle fixed by a mechanic
121
+ end
97
122
  end
98
123
 
99
- Using the above model as an example, you can interact with the state machine
124
+ Using the above class as an example, you can interact with the state machine
100
125
  like so:
101
126
 
102
- vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state: "parked">
103
- vehicle.ignite # => true
104
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "idling">
105
- vehicle.shift_up # => true
106
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "first_gear">
107
- vehicle.shift_up # => true
108
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state: "second_gear">
127
+ vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
128
+ vehicle.parked? # => true
129
+ vehicle.can_ignite? # => true
130
+ vehicle.next_ignite_transition # => #<StateMachine::Transition:0xb7c34cec ...>
131
+ vehicle.ignite # => true
132
+ vehicle.parked? # => false
133
+ vehicle.idling? # => true
134
+ vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
135
+ vehicle.shift_up # => true
136
+ vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
137
+ vehicle.shift_up # => true
138
+ vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
109
139
 
110
140
  # The bang (!) operator can raise exceptions if the event fails
111
- vehicle.park! # => PluginAWeek::StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
141
+ vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "second_gear"
142
+
143
+ # Generic state predicates can raise exceptions if the value does not exist
144
+ vehicle.state?('parked') # => true
145
+ vehicle.state?('invalid') # => ArgumentError: "parked" is not a known state value
146
+
147
+ == Integrations
148
+
149
+ In addition to being able to define state machines on all Ruby classes, a set of
150
+ out-of-the-box integrations are available for some of the more popular Ruby
151
+ libraries. These integrations add library-specific behavior, allowing for state
152
+ machines to work more tightly with the conventions defined by those libraries.
153
+
154
+ The integrations currently available include:
155
+ * ActiveRecord models
156
+ * DataMapper resources
157
+ * Sequel models
158
+
159
+ A brief overview of these integrations is described below.
160
+
161
+ === ActiveRecord
162
+
163
+ The ActiveRecord integration adds support for database transactions, automatically
164
+ saving the record, named scopes, and observers. For example,
165
+
166
+ class Vehicle < ActiveRecord::Base
167
+ state_machine :initial => 'parked' do
168
+ before_transition :to => 'idling', :do => :put_on_seatbelt
169
+ after_transition :to => 'parked' do |vehicle, transition|
170
+ vehicle.seatbelt = 'off'
171
+ end
172
+
173
+ event :ignite do
174
+ transition :to => 'idling', :from => 'parked'
175
+ end
176
+ end
177
+
178
+ def put_on_seatbelt
179
+ ...
180
+ end
181
+ end
182
+
183
+ class VehicleObserver < ActiveRecord::Observer
184
+ # Callback for :ignite event *before* the transition is performed
185
+ def before_ignite(vehicle, transition)
186
+ # log message
187
+ end
188
+
189
+ # Generic transition callback *before* the transition is performed
190
+ def after_transition(vehicle, transition)
191
+ Audit.log(vehicle, transition)
192
+ end
193
+ end
112
194
 
113
- === With enumerations
195
+ For more information about the various behaviors added for ActiveRecord state
196
+ machines, see StateMachine::Integrations::ActiveRecord.
197
+
198
+ ==== With enumerations
114
199
 
115
200
  Using the acts_as_enumeration[http://github.com/pluginaweek/acts_as_enumeration] plugin
116
- states can be transparently stored using record ids in the database like so:
201
+ with an ActiveRecord integration, states can be transparently stored using
202
+ record ids in the database like so:
117
203
 
118
204
  class VehicleState < ActiveRecord::Base
119
205
  acts_as_enumeration
120
206
 
121
207
  create :id => 1, :name => 'parked'
122
208
  create :id => 2, :name => 'idling'
123
- create :id => 3, :name => 'first_gear'
124
209
  ...
125
210
  end
126
211
 
@@ -133,31 +218,151 @@ states can be transparently stored using record ids in the database like so:
133
218
  event :park do
134
219
  transition :to => 'parked', :from => %w(idling first_gear)
135
220
  end
136
-
137
- event :ignite do
138
- transition :to => 'stalled', :from => 'stalled'
139
- transition :to => 'idling', :from => 'parked'
140
- end
141
221
  end
142
222
 
143
223
  ...
144
224
  end
145
225
 
146
- Notice in the above example, the state machine definition remains *exactly* the
147
- same. However, when interacting with the records, the actual state will be
148
- stored using the identifiers defined for the enumeration:
226
+ Notice that the state machine definition remains *exactly* the same. However,
227
+ when interacting with the records, the actual state will be stored using the
228
+ identifiers defined for the enumeration:
149
229
 
150
230
  vehicle = Vehicle.create # => #<Vehicle id: 1, seatbelt_on: false, state_id: 1>
151
231
  vehicle.ignite # => true
152
232
  vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 2>
153
- vehicle.shift_up # => true
154
- vehicle # => #<Vehicle id: 1, seatbelt_on: true, state_id: 3>
155
233
 
156
234
  This allows states to take on more complex functionality other than just being
157
235
  a string value.
158
236
 
237
+ === DataMapper
238
+
239
+ Like the ActiveRecord integration, the DataMapper integration adds support for
240
+ database transactions, automatically saving the record, named scopes, Extlib-like
241
+ callbacks, and observers. For example,
242
+
243
+ class Vehicle
244
+ include DataMapper::Resource
245
+
246
+ property :id, Serial
247
+ property :state, String
248
+
249
+ state_machine :initial => 'parked' do
250
+ before_transition :to => 'idling', :do => :put_on_seatbelt
251
+ after_transition :to => 'parked' do |transition|
252
+ self.seatbelt = 'off' # self is the record
253
+ end
254
+
255
+ event :ignite do
256
+ transition :to => 'idling', :from => 'parked'
257
+ end
258
+ end
259
+
260
+ def put_on_seatbelt
261
+ ...
262
+ end
263
+ end
264
+
265
+ class VehicleObserver
266
+ include DataMapper::Observer
267
+
268
+ observe Vehicle
269
+
270
+ # Callback for :ignite event *before* the transition is performed
271
+ before_transition :on => :ignite do |transition|
272
+ # log message (self is the record)
273
+ end
274
+
275
+ # Generic transition callback *before* the transition is performed
276
+ after_transition do |transition, saved|
277
+ Audit.log(self, transition) if saved # self is the record
278
+ end
279
+ end
280
+
281
+ For more information about the various behaviors added for DataMapper state
282
+ machines, see StateMachine::Integrations::DataMapper.
283
+
284
+ === Sequel
285
+
286
+ Like the ActiveRecord integration, the Sequel integration adds support for
287
+ database transactions, automatically saving the record, named scopes, and
288
+ callbacks. For example,
289
+
290
+ class Vehicle < Sequel::Model
291
+ state_machine :initial => 'parked' do
292
+ before_transition :to => 'idling', :do => :put_on_seatbelt
293
+ after_transition :to => 'parked' do |transition|
294
+ self.seatbelt = 'off' # self is the record
295
+ end
296
+
297
+ event :ignite do
298
+ transition :to => 'idling', :from => 'parked'
299
+ end
300
+ end
301
+
302
+ def put_on_seatbelt
303
+ ...
304
+ end
305
+ end
306
+
307
+ For more information about the various behaviors added for Sequel state
308
+ machines, see StateMachine::Integrations::Sequel.
309
+
159
310
  == Tools
160
311
 
312
+ === Generating graphs
313
+
314
+ This library comes with built-in support for generating di-graphs based on the
315
+ events, states, and transitions defined for a state machine using GraphViz[http://www.graphviz.org].
316
+ This requires that both the <tt>ruby-graphviz</tt> gem and graphviz library be
317
+ installed on the system.
318
+
319
+ ==== Examples
320
+
321
+ To generate a graph for a specific file / class:
322
+
323
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle
324
+
325
+ To save files to a specific path:
326
+
327
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files
328
+
329
+ To customize the image format:
330
+
331
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg
332
+
333
+ To generate multiple state machine graphs:
334
+
335
+ rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
336
+
337
+ *Note* that this will generate a different file for every state machine defined
338
+ in the class. The generates files will an output filename of the format #{class_name}_#{attribute}.#{format}.
339
+
340
+ For examples of actual images generated using this task, see those under the
341
+ test/examples folder.
342
+
343
+ ==== Ruby on Rails Integration
344
+
345
+ There is a special integration Rake task for generating state machines for
346
+ classes used in a Ruby on Rails application. This task will load the application
347
+ environment, meaning that it's unnecessary to specify the actual file to load.
348
+
349
+ For example,
350
+
351
+ rake state_machine:draw:rails CLASS=Vehicle
352
+
353
+ ==== Merb Integration
354
+
355
+ Like Ruby on Rails, there is a special integration Rake task for generating
356
+ state machines for classes used in a Merb application. This task will load the
357
+ application environment, meaning that it's unnecessary to specify the actual
358
+ files to load.
359
+
360
+ For example,
361
+
362
+ rake state_machine:draw:merb CLASS=Vehicle
363
+
364
+ === Interactive graphs
365
+
161
366
  Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html].
162
367
  This is a great tool for "simulating, visualizing and transforming finite state
163
368
  automata and Turing Machines". This tool can help in the creation of states and
@@ -165,16 +370,19 @@ events for your models. It is cross-platform, written in Java.
165
370
 
166
371
  == Testing
167
372
 
168
- Before you can run any tests, the following gem must be installed:
169
- * plugin_test_helper[http://github.com/pluginaweek/plugin_test_helper]
170
-
171
- To run against a specific version of Rails:
373
+ To run the entire test suite (will test ActiveRecord, DataMapper, and Sequel
374
+ integrations if available):
172
375
 
173
- rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
376
+ rake test
174
377
 
175
378
  == Dependencies
176
379
 
177
- * Rails 2.1 or later
380
+ By default, there are no dependencies. If using specific integrations, those
381
+ dependencies are listed below.
382
+
383
+ * ActiveRecord[http://rubyonrails.org] integration: 2.1.0 or later
384
+ * DataMapper[http://datamapper.org] integration: 0.9.0 or later
385
+ * Sequel[http://sequel.rubyforge.org] integration: 2.8.0 or later
178
386
 
179
387
  == References
180
388
 
data/Rakefile CHANGED
@@ -5,11 +5,11 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'state_machine'
8
- s.version = '0.3.1'
8
+ s.version = '0.4.0'
9
9
  s.platform = Gem::Platform::RUBY
10
- s.summary = 'Adds support for creating state machines for attributes within a model'
10
+ s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
11
11
 
12
- s.files = FileList['{lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
12
+ s.files = FileList['{examples,lib,test}/**/*'] + %w(CHANGELOG.rdoc init.rb LICENSE Rakefile README.rdoc) - FileList['test/app_root/{log,log/*,script,script/*}']
13
13
  s.require_path = 'lib'
14
14
  s.has_rdoc = true
15
15
  s.test_files = Dir['test/**/*_test.rb']
@@ -86,3 +86,29 @@ task :release => [:gem, :package] do
86
86
  ruby_forge.add_release(spec.rubyforge_project, spec.name, spec.version, file)
87
87
  end
88
88
  end
89
+
90
+ namespace :state_machine do
91
+ desc 'Draws a set of state machines using GraphViz. Target files to load with FILE=x,y,z; Machine class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x'
92
+ task :draw do
93
+ # Load the library
94
+ $:.unshift(File.dirname(__FILE__) + '/lib')
95
+ require 'state_machine'
96
+
97
+ # Build drawing options
98
+ options = {}
99
+ options[:file] = ENV['FILE'] if ENV['FILE']
100
+ options[:path] = ENV['TARGET'] if ENV['TARGET']
101
+ options[:format] = ENV['FORMAT'] if ENV['FORMAT']
102
+ options[:font] = ENV['FONT'] if ENV['FONT']
103
+
104
+ StateMachine::Machine.draw(ENV['CLASS'], options)
105
+ end
106
+
107
+ namespace :draw do
108
+ desc 'Draws a set of state machines using GraphViz for a Ruby on Rails application. Target class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x'
109
+ task :rails => [:environment, 'state_machine:draw']
110
+
111
+ desc 'Draws a set of state machines using GraphViz for a Merb application. Target class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x'
112
+ task :merb => [:merb_env, 'state_machine:draw']
113
+ end
114
+ end