state_machine 0.3.1 → 0.4.0

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