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
@@ -0,0 +1,169 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with Sequel models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # Sequel model:
9
+ #
10
+ # class Vehicle < Sequel::Model
11
+ # state_machine :initial => 'parked' do
12
+ # event :ignite do
13
+ # transition :to => 'idling', :from => 'parked'
14
+ # end
15
+ # end
16
+ # end
17
+ #
18
+ # The examples in the sections below will use the above class as a
19
+ # reference.
20
+ #
21
+ # == Actions
22
+ #
23
+ # By default, the action that will be invoked when a state is transitioned
24
+ # is the +save+ action. This will cause the resource to save the changes
25
+ # made to the state machine's attribute. *Note* that if any other changes
26
+ # were made to the resource prior to transition, then those changes will
27
+ # be made as well.
28
+ #
29
+ # For example,
30
+ #
31
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state=nil>
32
+ # vehicle.name = 'Ford Explorer'
33
+ # vehicle.ignite # => true
34
+ # vehicle.refresh # => #<Vehicle id=1 name="Ford Explorer" state="idling">
35
+ #
36
+ # == Transactions
37
+ #
38
+ # In order to ensure that any changes made during transition callbacks
39
+ # are rolled back during a failed attempt, every transition is wrapped
40
+ # within a transaction.
41
+ #
42
+ # For example,
43
+ #
44
+ # class Message < Sequel::Model
45
+ # end
46
+ #
47
+ # Vehicle.state_machine do
48
+ # before_transition do |transition|
49
+ # Message.create(:content => transition.inspect)
50
+ # false
51
+ # end
52
+ # end
53
+ #
54
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state=nil>
55
+ # vehicle.ignite # => false
56
+ # Message.count # => 0
57
+ #
58
+ # *Note* that only before callbacks that halt the callback chain and
59
+ # failed attempts to save the record will result in the transaction being
60
+ # rolled back. If an after callback halts the chain, the previous result
61
+ # still applies and the transaction is *not* rolled back.
62
+ #
63
+ # == Scopes
64
+ #
65
+ # To assist in filtering models with specific states, a series of class
66
+ # methods are defined on the model for finding records with or without a
67
+ # particular set of states.
68
+ #
69
+ # These named scopes are the functional equivalent of the following
70
+ # definitions:
71
+ #
72
+ # class Vehicle < Sequel::Model
73
+ # class << self
74
+ # def with_states(*values)
75
+ # filter(:state => values)
76
+ # end
77
+ # alias_method :with_state, :with_states
78
+ #
79
+ # def without_states(*values)
80
+ # filter(~{:state => values})
81
+ # end
82
+ # alias_method :without_state, :without_states
83
+ # end
84
+ # end
85
+ #
86
+ # Because of the way scopes work in Sequel, they can be chained like so:
87
+ #
88
+ # Vehicle.with_state('parked').with_state('idling').order(:id.desc)
89
+ #
90
+ # == Callbacks
91
+ #
92
+ # All before/after transition callbacks defined for Sequel resources
93
+ # behave in the same way that other Sequel hooks behave. Rather than
94
+ # passing in the record as an argument to the callback, the callback is
95
+ # instead bound to the object and evaluated within its context.
96
+ #
97
+ # For example,
98
+ #
99
+ # class Vehicle < Sequel::Model
100
+ # state_machine :initial => 'parked' do
101
+ # before_transition :to => 'idling' do
102
+ # put_on_seatbelt
103
+ # end
104
+ #
105
+ # before_transition do |transition|
106
+ # # log message
107
+ # end
108
+ #
109
+ # event :ignite do
110
+ # transition :to => 'idling', :from => 'parked'
111
+ # end
112
+ # end
113
+ #
114
+ # def put_on_seatbelt
115
+ # ...
116
+ # end
117
+ # end
118
+ #
119
+ # Note, also, that the transition can be accessed by simply defining
120
+ # additional arguments in the callback block.
121
+ module Sequel
122
+ # Should this integration be used for state machines in the given class?
123
+ # Classes that include Sequel::Model will automatically use the Sequel
124
+ # integration.
125
+ def self.matches?(klass)
126
+ defined?(::Sequel::Model) && klass <= ::Sequel::Model
127
+ end
128
+
129
+ # Runs a new database transaction, rolling back any changes if the
130
+ # yielded block fails (i.e. returns false).
131
+ def within_transaction(object)
132
+ object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
133
+ end
134
+
135
+ protected
136
+ # Sets the default action for all Sequel state machines to +save+
137
+ def default_action
138
+ :save
139
+ end
140
+
141
+ # Defines a scope for finding records *with* a particular value or
142
+ # values for the attribute
143
+ def define_with_scope(name)
144
+ attribute = self.attribute
145
+ (class << owner_class; self; end).class_eval do
146
+ define_method(name) {|*values| filter(attribute.to_sym => values.flatten)}
147
+ end
148
+ end
149
+
150
+ # Defines a scope for finding records *without* a particular value or
151
+ # values for the attribute
152
+ def define_without_scope(name)
153
+ attribute = self.attribute
154
+ (class << owner_class; self; end).class_eval do
155
+ define_method(name) {|*values| filter(~{attribute.to_sym => values.flatten})}
156
+ end
157
+ end
158
+
159
+ # Creates a new callback in the callback chain, always ensuring that
160
+ # it's configured to bind to the object as this is the convention for
161
+ # Sequel callbacks
162
+ def add_callback(type, options, &block)
163
+ options[:bind_to_object] = true
164
+ options[:terminator] = @terminator ||= lambda {|result| result == false}
165
+ super
166
+ end
167
+ end
168
+ end
169
+ end
@@ -1,412 +1,806 @@
1
+ require 'state_machine/extensions'
1
2
  require 'state_machine/event'
3
+ require 'state_machine/callback'
4
+ require 'state_machine/assertions'
2
5
 
3
- module PluginAWeek #:nodoc:
4
- module StateMachine
5
- # Represents a state machine for a particular attribute. State machines
6
- # consist of events (a.k.a. actions) and a set of transitions that define
7
- # how the state changes after a particular event is fired.
6
+ # Load each available integration
7
+ Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
8
+ require "state_machine/integrations/#{File.basename(path)}"
9
+ end
10
+
11
+ module StateMachine
12
+ # Represents a state machine for a particular attribute. State machines
13
+ # consist of events and a set of transitions that define how the state
14
+ # changes after a particular event is fired.
15
+ #
16
+ # A state machine may not necessarily know all of the possible states for
17
+ # an object since they can be any arbitrary value. As a result, anything
18
+ # that relies on a list of all possible states should keep in mind that if
19
+ # a state has not been referenced *anywhere* in the state machine definition,
20
+ # then it will *not* be a known state unless the +other_states+ is used.
21
+ #
22
+ # == State values
23
+ #
24
+ # While string are the most common object type used for setting values on
25
+ # the state of the machine, there are no restrictions on what can be used.
26
+ # This means that symbols, integers, dates/times, etc. can all be used.
27
+ #
28
+ # With string states:
29
+ #
30
+ # class Vehicle
31
+ # state_machine :initial => 'parked' do
32
+ # event :ignite do
33
+ # transition :to => 'idling', :from => 'parked'
34
+ # end
35
+ # end
36
+ # end
37
+ #
38
+ # With symbolic states:
39
+ #
40
+ # class Vehicle
41
+ # state_machine :initial => :parked do
42
+ # event :ignite do
43
+ # transition :to => :idling, :from => :parked
44
+ # end
45
+ # end
46
+ # end
47
+ #
48
+ # With time states:
49
+ #
50
+ # class Switch
51
+ # state_machine :activated_at
52
+ # event :activate do
53
+ # transition :to => lambda {Time.now}
54
+ # end
55
+ #
56
+ # event :deactivate do
57
+ # transition :to => nil
58
+ # end
59
+ # end
60
+ # end
61
+ #
62
+ # == Callbacks
63
+ #
64
+ # Callbacks are supported for hooking before and after every possible
65
+ # transition in the machine. Each callback is invoked in the order in which
66
+ # it was defined. See StateMachine::Machine#before_transition
67
+ # and StateMachine::Machine#after_transition for documentation
68
+ # on how to define new callbacks.
69
+ #
70
+ # === Canceling callbacks
71
+ #
72
+ # Callbacks can be canceled by throwing :halt at any point during the
73
+ # callback. For example,
74
+ #
75
+ # ...
76
+ # throw :halt
77
+ # ...
78
+ #
79
+ # If a +before+ callback halts the chain, the associated transition and all
80
+ # later callbacks are canceled. If an +after+ callback halts the chain,
81
+ # the later callbacks are canceled, but the transition is still successful.
82
+ #
83
+ # *Note* that if a +before+ callback fails and the bang version of an event
84
+ # was invoked, an exception will be raised instead of returning false. For
85
+ # example,
86
+ #
87
+ # class Vehicle
88
+ # state_machine, :initial => 'parked' do
89
+ # before_transition :to => 'idling', :do => lambda {|vehicle| throw :halt}
90
+ # ...
91
+ # end
92
+ # end
93
+ #
94
+ # vehicle = Vehicle.new
95
+ # vehicle.park # => false
96
+ # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition via :park from "idling"
97
+ #
98
+ # == Observers
99
+ #
100
+ # Observers, in the sense of external classes and *not* Ruby's Observable
101
+ # mechanism, can hook into state machines as well. Such observers use the
102
+ # same callback api that's used internally.
103
+ #
104
+ # Below are examples of defining observers for the following state machine:
105
+ #
106
+ # class Vehicle
107
+ # state_machine do
108
+ # event :ignite do
109
+ # transition :to => 'idling', :from => 'parked'
110
+ # end
111
+ # ...
112
+ # end
113
+ # ...
114
+ # end
115
+ #
116
+ # Event/Transition behaviors:
117
+ #
118
+ # class VehicleObserver
119
+ # def self.before_park(vehicle, transition)
120
+ # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
121
+ # end
122
+ #
123
+ # def self.after_park(vehicle, transition, result)
124
+ # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
125
+ # end
126
+ #
127
+ # def self.before_transition(vehicle, transition)
128
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
129
+ # end
130
+ #
131
+ # def self.after_transition(vehicle, transition, result)
132
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
133
+ # end
134
+ # end
135
+ #
136
+ # Vehicle.state_machine do
137
+ # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
138
+ # before_transition VehicleObserver.method(:before_transition)
139
+ #
140
+ # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
141
+ # after_transition VehicleObserver.method(:after_transition)
142
+ # end
143
+ #
144
+ # One common callback is to record transitions for all models in the system
145
+ # for auditing/debugging purposes. Below is an example of an observer that
146
+ # can easily automate this process for all models:
147
+ #
148
+ # class StateMachineObserver
149
+ # def self.before_transition(object, transition)
150
+ # Audit.log_transition(object.attributes)
151
+ # end
152
+ # end
153
+ #
154
+ # [Vehicle, Switch, Project].each do |klass|
155
+ # klass.state_machines.each do |machine|
156
+ # machine.before_transition klass.method(:before_transition)
157
+ # end
158
+ # end
159
+ #
160
+ # Additional observer-like behavior may be exposed by the various
161
+ # integrations available. See below for more information.
162
+ #
163
+ # == Integrations
164
+ #
165
+ # By default, state machines are library-agnostic, meaning that they work
166
+ # on any Ruby class and have no external dependencies. However, there are
167
+ # certain libraries which expose additional behavior that can be taken
168
+ # advantage of by state machines.
169
+ #
170
+ # This library is built to work out of the box with a few popular Ruby
171
+ # libraries that allow for additional behavior to provide a cleaner and
172
+ # smoother experience. This is especially the case for objects backed by a
173
+ # database that may allow for transactions, persistent storage,
174
+ # search/filters, callbacks, etc.
175
+ #
176
+ # When a state machine is defined for classes using any of the above libraries,
177
+ # it will try to automatically determine the integration to use (Agnostic,
178
+ # ActiveRecord, DataMapper, or Sequel) based on the class definition. To
179
+ # see how each integration affects the machine's behavior, refer to all
180
+ # constants defined under the StateMachine::Integrations namespace.
181
+ class Machine
182
+ include Assertions
183
+
184
+ # The class that the machine is defined in
185
+ attr_reader :owner_class
186
+
187
+ # The attribute for which the machine is being defined
188
+ attr_reader :attribute
189
+
190
+ # The initial state that the machine will be in when an object is created
191
+ attr_reader :initial_state
192
+
193
+ # The events that trigger transitions
194
+ attr_reader :events
195
+
196
+ # A list of all of the states known to this state machine. This will pull
197
+ # state names from the following sources:
198
+ # * Initial state
199
+ # * Event transitions (:to, :from, :except_to, and :except_from options)
200
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
201
+ # * Unreferenced states (using +other_states+ helper)
202
+ attr_reader :states
203
+
204
+ # The callbacks to invoke before/after a transition is performed
205
+ attr_reader :callbacks
206
+
207
+ # The action to invoke when an object transitions
208
+ attr_reader :action
209
+
210
+ class << self
211
+ # Attempts to find or create a state machine for the given class. For
212
+ # example,
213
+ #
214
+ # StateMachine::Machine.find_or_create(Switch)
215
+ # StateMachine::Machine.find_or_create(Switch, :initial => 'off')
216
+ # StateMachine::Machine.find_or_create(Switch, 'status')
217
+ # StateMachine::Machine.find_or_create(Switch, 'status', :initial => 'off')
218
+ #
219
+ # If a machine of the given name already exists in one of the class's
220
+ # superclasses, then a copy of that machine will be created and stored
221
+ # in the new owner class (the original will remain unchanged).
222
+ def find_or_create(owner_class, *args)
223
+ options = args.last.is_a?(Hash) ? args.pop : {}
224
+ attribute = args.any? ? args.first.to_s : 'state'
225
+
226
+ # Attempts to find an existing machine
227
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
228
+ machine = machine.within_context(owner_class, options) unless machine.owner_class == owner_class
229
+ else
230
+ # No existing machine: create a new one
231
+ machine = new(owner_class, attribute, options)
232
+ end
233
+
234
+ machine
235
+ end
236
+
237
+ # Draws the state machines defined in the given classes using GraphViz.
238
+ # The given classes must be a comma-delimited string of class names.
239
+ #
240
+ # Configuration options:
241
+ # * +file+ - A comma-delimited string of files to load that contain the state machine definitions to draw
242
+ # * +path+ - The path to write the graph file to
243
+ # * +format+ - The image format to generate the graph in
244
+ # * +font+ - The name of the font to draw state names in
245
+ def draw(class_names, options = {})
246
+ raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
247
+
248
+ # Load any files
249
+ if files = options.delete(:file)
250
+ files.split(',').each {|file| require file}
251
+ end
252
+
253
+ class_names.split(',').each do |class_name|
254
+ # Navigate through the namespace structure to get to the class
255
+ klass = Object
256
+ class_name.split('::').each do |name|
257
+ klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
258
+ end
259
+
260
+ # Draw each of the class's state machines
261
+ klass.state_machines.values.each do |machine|
262
+ machine.draw(options)
263
+ end
264
+ end
265
+ end
266
+ end
267
+
268
+ # Creates a new state machine for the given attribute
269
+ def initialize(owner_class, *args, &block)
270
+ options = args.last.is_a?(Hash) ? args.pop : {}
271
+ assert_valid_keys(options, :initial, :action, :plural, :integration)
272
+
273
+ # Set machine configuration
274
+ @attribute = (args.first || 'state').to_s
275
+ @events = {}
276
+ @states = []
277
+ @callbacks = {:before => [], :after => []}
278
+ @action = options[:action]
279
+
280
+ # Add class-/instance-level methods to the owner class for state initialization
281
+ owner_class.class_eval do
282
+ extend StateMachine::ClassMethods
283
+ include StateMachine::InstanceMethods
284
+ end unless owner_class.included_modules.include?(StateMachine::InstanceMethods)
285
+
286
+ # Initialize the context of the machine
287
+ set_context(owner_class, :initial => options[:initial], :integration => options[:integration], &block)
288
+
289
+ # Set integration-specific configurations
290
+ @action ||= default_action unless options.include?(:action)
291
+ define_attribute_accessor
292
+ define_scopes(options[:plural])
293
+
294
+ # Call after hook for integration-specific extensions
295
+ after_initialize
296
+ end
297
+
298
+ # Creates a copy of this machine in addition to copies of each associated
299
+ # event, so that the list of transitions for each event don't conflict
300
+ # with different machines
301
+ def initialize_copy(orig) #:nodoc:
302
+ super
303
+
304
+ @events = @events.inject({}) do |events, (name, event)|
305
+ event = event.dup
306
+ event.machine = self
307
+ events[name] = event
308
+ events
309
+ end
310
+ @states = @states.dup
311
+ @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
312
+ end
313
+
314
+ # Creates a copy of this machine within the context of the given class.
315
+ # This should be used for inheritance support of state machines.
316
+ def within_context(owner_class, options = {}) #:nodoc:
317
+ machine = dup
318
+ machine.set_context(owner_class, {:integration => @integration}.merge(options))
319
+ machine
320
+ end
321
+
322
+ # Changes the context of this machine to the given class so that new
323
+ # events and transitions are created in the proper context.
324
+ #
325
+ # Configuration options:
326
+ # * +initial+ - The initial value to set the attribute to
327
+ # * +integration+ - The name of the integration for extending this machine with library-specific behavior
328
+ #
329
+ # All other configuration options for the machine can only be set on
330
+ # creation.
331
+ def set_context(owner_class, options = {}) #:nodoc:
332
+ assert_valid_keys(options, :initial, :integration)
333
+
334
+ @owner_class = owner_class
335
+ if options[:initial]
336
+ @initial_state = options[:initial]
337
+ add_states([@initial_state]) unless @initial_state.is_a?(Proc)
338
+ end
339
+
340
+ # Find an integration that can be used for implementing various parts
341
+ # of the state machine that may behave differently in different libraries
342
+ if @integration = options[:integration] || StateMachine::Integrations.constants.find {|name| StateMachine::Integrations.const_get(name).matches?(owner_class)}
343
+ extend StateMachine::Integrations.const_get(@integration.to_s.gsub(/(?:^|_)(.)/) {$1.upcase})
344
+ end
345
+
346
+ # Record this machine as matched to the attribute in the current owner
347
+ # class. This will override any machines mapped to the same attribute
348
+ # in any superclasses.
349
+ owner_class.state_machines[attribute] = self
350
+ end
351
+
352
+ # Gets the initial state of the machine for the given object. If a dynamic
353
+ # initial state was configured for this machine, then the object will be
354
+ # passed into the proc to help determine the actual value of the initial
355
+ # state.
356
+ #
357
+ # == Examples
358
+ #
359
+ # With static initial state:
360
+ #
361
+ # class Vehicle
362
+ # state_machine :initial => 'parked' do
363
+ # ...
364
+ # end
365
+ # end
366
+ #
367
+ # Vehicle.state_machines['state'].initial_state(vehicle) # => "parked"
368
+ #
369
+ # With dynamic initial state:
370
+ #
371
+ # class Vehicle
372
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
373
+ # ...
374
+ # end
375
+ # end
376
+ #
377
+ # Vehicle.state_machines['state'].initial_state(vehicle) # => "idling"
378
+ def initial_state(object)
379
+ @initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state
380
+ end
381
+
382
+ # Defines additional states that are possible in the state machine, but
383
+ # which are derived outside of any events/transitions or possibly
384
+ # dynamically via Proc. This allows the creation of state conditionals
385
+ # which are not defined in the standard :to or :from structure.
8
386
  #
9
- # A state machine may not necessarily know all of the possible states for
10
- # an object since they can be any arbitrary value. As a result, anything
11
- # that relies on a list of all possible states should keep in mind that if
12
- # a state has not been referenced *anywhere* in the state machine definition,
13
- # then it will *not* be a known state.
387
+ # == Example
14
388
  #
15
- # == Callbacks
389
+ # class Vehicle
390
+ # state_machine :initial => 'parked' do
391
+ # event :ignite do
392
+ # transition :to => 'idling', :from => 'parked'
393
+ # end
394
+ #
395
+ # other_states %w(stalled stopped)
396
+ # end
397
+ #
398
+ # def stop
399
+ # self.state = 'stopped'
400
+ # end
401
+ # end
16
402
  #
17
- # Callbacks are supported for hooking before and after every possible
18
- # transition in the machine. Each callback is invoked in the order in which
19
- # it was defined. See PluginAWeek::StateMachine::Machine#before_transition
20
- # and PluginAWeek::StateMachine::Machine#after_transition for documentation
21
- # on how to define new callbacks.
403
+ # In the above state machine, the known states would be:
404
+ # * +idling+
405
+ # * +parked+
406
+ # * +stalled+
407
+ # * +stopped+
22
408
  #
23
- # === Cancelling callbacks
409
+ # Since +stalled+ and +stopped+ are not referenced in any transitions or
410
+ # callbacks, they are explicitly defined.
411
+ def other_states(*args)
412
+ add_states(args.flatten)
413
+ end
414
+
415
+ # Defines an event for the machine.
24
416
  #
25
- # If a +before+ callback returns +false+, all the later callbacks and
26
- # associated transition are cancelled. If an +after+ callback returns false,
27
- # the later callbacks are cancelled, but the transition is still successful.
28
- # This is the same behavior as exposed by ActiveRecord's callback support.
417
+ # == Instance methods
29
418
  #
30
- # *Note* that if a +before+ callback fails and the bang version of an event
31
- # was invoked, an exception will be raised instead of returning false.
419
+ # The following instance methods are generated when a new event is defined
420
+ # (the "park" event is used as an example):
421
+ # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given the current state of the object.
422
+ # * <tt>next_park_transition</tt> - Gets the next transition that would be performed if the "park" event were to be fired now on the object or nil if no transitions can be performed.
423
+ # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state.
424
+ # * <tt>park!(run_action = true)</tt> - Fires the "park" event, transitioning from the current state to the next valid state. If the transition fails, then a StateMachine::InvalidTransition error will be raised.
32
425
  #
33
- # == Observers
426
+ # == Defining transitions
34
427
  #
35
- # ActiveRecord observers can also hook into state machines in addition to
36
- # the conventional before_save, after_save, etc. behaviors. The following
37
- # types of behaviors can be observed:
38
- # * events (e.g. before_park/after_park, before_ignite/after_ignite)
39
- # * transitions (before_transition/after_transition)
428
+ # +event+ requires a block which allows you to define the possible
429
+ # transitions that can happen as a result of that event. For example,
40
430
  #
41
- # Each method takes a set of parameters that provides additional information
42
- # about the transition that caused the observer to be notified. Below are
43
- # examples of defining observers for the following state machine:
431
+ # event :park do
432
+ # transition :to => 'parked', :from => 'idle'
433
+ # end
434
+ #
435
+ # event :first_gear do
436
+ # transition :to => 'first_gear', :from => 'parked', :if => :seatbelt_on?
437
+ # end
44
438
  #
45
- # class Vehicle < ActiveRecord::Base
439
+ # See StateMachine::Event#transition for more information on
440
+ # the possible options that can be passed in.
441
+ #
442
+ # *Note* that this block is executed within the context of the actual event
443
+ # object. As a result, you will not be able to reference any class methods
444
+ # on the model without referencing the class itself. For example,
445
+ #
446
+ # class Vehicle
447
+ # def self.safe_states
448
+ # %w(parked idling stalled)
449
+ # end
450
+ #
46
451
  # state_machine do
47
452
  # event :park do
48
- # transition :to => 'parked', :from => 'idling'
453
+ # transition :to => 'parked', :from => Car.safe_states
454
+ # end
455
+ # end
456
+ # end
457
+ #
458
+ # == Example
459
+ #
460
+ # class Vehicle
461
+ # state_machine do
462
+ # event :park do
463
+ # transition :to => 'parked', :from => %w(first_gear reverse)
49
464
  # end
50
465
  # ...
51
466
  # end
52
- # ...
53
467
  # end
468
+ def event(name, &block)
469
+ name = name.to_s
470
+ event = events[name] ||= Event.new(self, name)
471
+ event.instance_eval(&block)
472
+ add_states(event.known_states)
473
+
474
+ event
475
+ end
476
+
477
+ # Creates a callback that will be invoked *before* a transition is
478
+ # performed so long as the given configuration options match the transition.
479
+ # Each part of the transition (event, to state, from state) must match in
480
+ # order for the callback to get invoked.
481
+ #
482
+ # Configuration options:
483
+ # * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
484
+ # * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
485
+ # * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
486
+ # * +except_to+ - One more states *not* being transitioned to
487
+ # * +except_from+ - One or more states *not* being transitioned from
488
+ # * +except_on+ - One or more events that *did not* fire the transition
489
+ # * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
490
+ # * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
491
+ # * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
492
+ #
493
+ # The +except+ group of options (+except_to+, +exception_from+, and
494
+ # +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
495
+ # +from+, and +on+, respectively)
54
496
  #
55
- # Event behaviors:
497
+ # == The callback
56
498
  #
57
- # class VehicleObserver < ActiveRecord::Observer
58
- # def before_park(vehicle, from_state, to_state)
59
- # logger.info "Vehicle #{vehicle.id} instructed to park... state is: #{from_state}, state will be: #{to_state}"
499
+ # When defining additional configuration options, callbacks must be defined
500
+ # in either the :do option or as a block. For example,
501
+ #
502
+ # class Vehicle
503
+ # state_machine do
504
+ # before_transition :to => 'parked', :do => :set_alarm
505
+ # before_transition :to => 'parked' do |vehicle, transition|
506
+ # vehicle.set_alarm
507
+ # end
508
+ # ...
60
509
  # end
510
+ # end
511
+ #
512
+ # === Accessing the transition
513
+ #
514
+ # In addition to passing the object being transitioned, the actual
515
+ # transition describing the context (e.g. event, from state, to state)
516
+ # can be accessed as well. This additional argument is only passed if the
517
+ # callback allows for it.
518
+ #
519
+ # For example,
520
+ #
521
+ # class Vehicle
522
+ # # Only specifies one parameter (the object being transitioned)
523
+ # before_transition :to => 'parked', :do => lambda {|vehicle| vehicle.set_alarm}
61
524
  #
62
- # def after_park(vehicle, from_state, to_state)
63
- # logger.info "Vehicle #{vehicle.id} instructed to park... state was: #{from_state}, state is: #{to_state}"
64
- # end
525
+ # # Specifies 2 parameters (object being transitioned and actual transition)
526
+ # before_transition :to => 'parked', :do => lambda {|vehicle, transition| vehicle.set_alarm(transition)}
65
527
  # end
66
528
  #
67
- # Transition behaviors:
529
+ # *Note* that the object in the callback will only be passed in as an
530
+ # argument if callbacks are configured to *not* be bound to the object
531
+ # involved. This is the default and may change on a per-integration basis.
532
+ #
533
+ # See StateMachine::Transition for more information about the
534
+ # attributes available on the transition.
68
535
  #
69
- # class VehicleObserver < ActiveRecord::Observer
70
- # def before_transition(vehicle, attribute, event, from_state, to_state)
71
- # logger.info "Vehicle #{vehicle.id} instructed to #{event}... #{attribute} is: #{from_state}, #{attribute} will be: #{to_state}"
536
+ # == Examples
537
+ #
538
+ # Below is an example of a class with one state machine and various types
539
+ # of +before+ transitions defined for it:
540
+ #
541
+ # class Vehicle
542
+ # state_machine do
543
+ # # Before all transitions
544
+ # before_transition :update_dashboard
545
+ #
546
+ # # Before specific transition:
547
+ # before_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
548
+ #
549
+ # # With conditional callback:
550
+ # before_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
551
+ #
552
+ # # Using :except counterparts:
553
+ # before_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
554
+ # ...
72
555
  # end
73
- #
74
- # def after_transition(vehicle, attribute, event, from_state, to_state)
75
- # logger.info "Vehicle #{vehicle.id} instructed to #{event}... #{attribute} was: #{from_state}, #{attribute} is: #{to_state}"
556
+ # end
557
+ #
558
+ # As can be seen, any number of transitions can be created using various
559
+ # combinations of configuration options.
560
+ def before_transition(options = {}, &block)
561
+ add_callback(:before, options.is_a?(Hash) ? options : {:do => options}, &block)
562
+ end
563
+
564
+ # Creates a callback that will be invoked *after* a transition is
565
+ # performed, so long as the given configuration options match the transition.
566
+ # Each part of the transition (event, to state, from state) must match
567
+ # in order for the callback to get invoked.
568
+ #
569
+ # Configuration options:
570
+ # * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
571
+ # * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
572
+ # * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
573
+ # * +except_to+ - One more states *not* being transitioned to
574
+ # * +except_from+ - One or more states *not* being transitioned from
575
+ # * +except_on+ - One or more events that *did not* fire the transition
576
+ # * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
577
+ # * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
578
+ # * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
579
+ #
580
+ # The +except+ group of options (+except_to+, +exception_from+, and
581
+ # +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
582
+ # +from+, and +on+, respectively)
583
+ #
584
+ # == The callback
585
+ #
586
+ # When defining additional configuration options, callbacks must be defined
587
+ # in either the :do option or as a block. For example,
588
+ #
589
+ # class Vehicle
590
+ # state_machine do
591
+ # after_transition :to => 'parked', :do => :set_alarm
592
+ # after_transition :to => 'parked' do |vehicle, transition, result|
593
+ # vehicle.set_alarm
594
+ # end
595
+ # ...
76
596
  # end
77
597
  # end
78
598
  #
79
- # One common callback is to record transitions for all models in the system
80
- # for audit/debugging purposes. Below is an example of an observer that can
81
- # easily automate this process for all models:
599
+ # === Accessing the transition / result
600
+ #
601
+ # In addition to passing the object being transitioned, the actual
602
+ # transition describing the context (e.g. event, from state, to state) and
603
+ # the result from calling the object's action can be optionally passed as
604
+ # well. These additional arguments are only passed if the callback allows
605
+ # for it.
82
606
  #
83
- # class StateMachineObserver < ActiveRecord::Observer
84
- # observe Vehicle, Switch, AutoShop
607
+ # For example,
608
+ #
609
+ # class Vehicle
610
+ # # Only specifies one parameter (the object being transitioned)
611
+ # after_transition :to => 'parked', :do => lambda {|vehicle| vehicle.set_alarm}
85
612
  #
86
- # def before_transition(record, attribute, event, from_state, to_state)
87
- # transition = StateTransition.build(:record => record, :attribute => attribute, :event => event, :from_state => from_state, :to_state => to_state)
88
- # transition.save # Will cancel rollback/cancel transition if this fails
613
+ # # Specifies 3 parameters (object being transitioned, transition, and action result)
614
+ # after_transition :to => 'parked', :do => lambda {|vehicle, transition, result| vehicle.set_alarm(transition) if result}
615
+ # end
616
+ #
617
+ # *Note* that the object in the callback will only be passed in as an
618
+ # argument if callbacks are configured to *not* be bound to the object
619
+ # involved. This is the default and may change on a per-integration basis.
620
+ #
621
+ # See StateMachine::Transition for more information about the
622
+ # attributes available on the transition.
623
+ #
624
+ # == Examples
625
+ #
626
+ # Below is an example of a model with one state machine and various types
627
+ # of +after+ transitions defined for it:
628
+ #
629
+ # class Vehicle
630
+ # state_machine do
631
+ # # After all transitions
632
+ # after_transition :update_dashboard
633
+ #
634
+ # # After specific transition:
635
+ # after_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
636
+ #
637
+ # # With conditional callback:
638
+ # after_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
639
+ #
640
+ # # Using :except counterparts:
641
+ # after_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
642
+ # ...
89
643
  # end
90
644
  # end
91
- class Machine
92
- # The class that the machine is defined for
93
- attr_reader :owner_class
94
-
95
- # The attribute for which the state machine is being defined
96
- attr_reader :attribute
97
-
98
- # The initial state that the machine will be in when a record is created
99
- attr_reader :initial_state
100
-
101
- # A list of the states defined in the transitions of all of the events
102
- attr_reader :states
103
-
104
- # The events that trigger transitions
105
- attr_reader :events
645
+ #
646
+ # As can be seen, any number of transitions can be created using various
647
+ # combinations of configuration options.
648
+ def after_transition(options = {}, &block)
649
+ add_callback(:after, options.is_a?(Hash) ? options : {:do => options}, &block)
650
+ end
651
+
652
+ # Runs a transaction, rolling back any changes if the yielded block fails.
653
+ #
654
+ # This is only applicable to integrations that involve databases. By
655
+ # default, this will not run any transactions, since the changes aren't
656
+ # taking place within the context of a database.
657
+ def within_transaction(object)
658
+ yield
659
+ end
660
+
661
+ # Draws a directed graph of the machine for visualizing the various events,
662
+ # states, and their transitions.
663
+ #
664
+ # This requires both the Ruby graphviz gem and the graphviz library be
665
+ # installed on the system.
666
+ #
667
+ # Configuration options:
668
+ # * +name+ - The name of the file to write to (without the file extension). Default is "#{owner_class.name}_#{attribute}"
669
+ # * +path+ - The path to write the graph file to. Default is the current directory (".").
670
+ # * +format+ - The image format to generate the graph in. Default is "png'.
671
+ # * +font+ - The name of the font to draw state names in. Default is "Arial'.
672
+ def draw(options = {})
673
+ options = {
674
+ :name => "#{owner_class.name}_#{attribute}",
675
+ :path => '.',
676
+ :format => 'png',
677
+ :font => 'Arial'
678
+ }.merge(options)
679
+ assert_valid_keys(options, :name, :font, :path, :format)
106
680
 
107
- # Creates a new state machine for the given attribute
108
- #
109
- # Configuration options:
110
- # * +initial+ - The initial value to set the attribute to. This can be an actual value or a proc, which will be evaluated at runtime.
111
- #
112
- # == Scopes
113
- #
114
- # This will automatically create a named scope called with_#{attribute}
115
- # that will find all records that have the attribute set to a given value.
116
- # For example,
117
- #
118
- # Switch.with_state('on') # => Finds all switches where the state is on
119
- # Switch.with_states('on', 'off') # => Finds all switches where the state is either on or off
120
- #
121
- # *Note* that if class methods already exist with those names (i.e. "with_state"
122
- # or "with_states"), then a scope will not be defined for that name.
123
- def initialize(owner_class, attribute = 'state', options = {})
124
- set_context(owner_class, options)
681
+ begin
682
+ # Load the graphviz library
683
+ require 'rubygems'
684
+ require 'graphviz'
125
685
 
126
- @attribute = attribute.to_s
127
- @states = []
128
- @events = {}
686
+ graph = GraphViz.new('G', :output => options[:format], :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}"))
129
687
 
130
- add_transition_callbacks
131
- add_named_scopes
132
- end
133
-
134
- # Creates a copy of this machine in addition to copies of each associated
135
- # event, so that the list of transitions for each event don't conflict
136
- # with different machines
137
- def initialize_copy(orig) #:nodoc:
138
- super
688
+ # Add nodes
689
+ states.each do |state|
690
+ shape = state == @initial_state ? 'doublecircle' : 'circle'
691
+ state = state.is_a?(Proc) ? 'lambda' : state.to_s
692
+ graph.add_node(state, :width => '1', :height => '1', :fixedsize => 'true', :shape => shape, :fontname => options[:font])
693
+ end
139
694
 
140
- @states = @states.dup
141
- @events = @events.inject({}) do |events, (name, event)|
142
- event = event.dup
143
- event.machine = self
144
- events[name] = event
145
- events
695
+ # Add edges
696
+ events.values.each do |event|
697
+ event.guards.each do |guard|
698
+ # From states: :from, everything but :except states, or all states
699
+ from_states = Array(guard.requirements[:from]) || guard.requirements[:except_from] && (states - Array(guard.requirements[:except_from])) || states
700
+ to_state = guard.requirements[:to]
701
+ to_state = to_state.is_a?(Proc) ? 'lambda' : to_state.to_s if to_state
702
+
703
+ from_states.each do |from_state|
704
+ from_state = from_state.to_s
705
+ graph.add_edge(from_state, to_state || from_state, :label => event.name, :fontname => options[:font])
706
+ end
707
+ end
146
708
  end
709
+
710
+ # Generate the graph
711
+ graph.output
712
+
713
+ true
714
+ rescue LoadError
715
+ $stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` and try again.'
716
+ false
147
717
  end
148
-
149
- # Creates a copy of this machine within the context of the given class.
150
- # This should be used for inheritance support of state machines.
151
- def within_context(owner_class, options = {}) #:nodoc:
152
- machine = dup
153
- machine.set_context(owner_class, options)
154
- machine
718
+ end
719
+
720
+ protected
721
+ # Runs additional initialization hooks. By default, this is a no-op.
722
+ def after_initialize
155
723
  end
156
724
 
157
- # Changes the context of this machine to the given class so that new
158
- # events and transitions are created in the proper context.
159
- def set_context(owner_class, options = {}) #:nodoc:
160
- options.assert_valid_keys(:initial)
161
-
162
- @owner_class = owner_class
163
- @initial_state = options[:initial] if options[:initial]
725
+ # Gets the default action that should be invoked when performing a
726
+ # transition on the attribute for this machine. This may change
727
+ # depending on the configured integration for the owner class.
728
+ def default_action
164
729
  end
165
730
 
166
- # Gets the initial state of the machine for the given record. If a record
167
- # is specified a and a dynamic initial state was configured for the machine,
168
- # then that record will be passed into the proc to help determine the actual
169
- # value of the initial state.
170
- #
171
- # == Examples
172
- #
173
- # With normal initial state:
174
- #
175
- # class Vehicle < ActiveRecord::Base
176
- # state_machine :initial => 'parked' do
177
- # ...
178
- # end
179
- # end
180
- #
181
- # Vehicle.state_machines['state'].initial_state(@vehicle) # => "parked"
182
- #
183
- # With dynamic initial state:
184
- #
185
- # class Vehicle < ActiveRecord::Base
186
- # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? 'idling' : 'parked'} do
187
- # ...
188
- # end
189
- # end
190
- #
191
- # Vehicle.state_machines['state'].initial_state(@vehicle) # => "idling"
192
- def initial_state(record)
193
- @initial_state.is_a?(Proc) ? @initial_state.call(record) : @initial_state
731
+ # Adds reader/writer methods for accessing the attribute that this state
732
+ # machine is defined for.
733
+ def define_attribute_accessor
734
+ attribute = self.attribute
735
+
736
+ owner_class.class_eval do
737
+ attr_reader attribute unless method_defined?(attribute) || private_method_defined?(attribute)
738
+ attr_writer attribute unless method_defined?("#{attribute}=") || private_method_defined?("#{attribute}=")
739
+
740
+ # Checks whether the current state is a given value. If the value
741
+ # is not a known state, then an ArgumentError is raised.
742
+ define_method("#{attribute}?") do |state|
743
+ raise ArgumentError, "#{state.inspect} is not a known #{attribute} value" unless self.class.state_machines[attribute].states.include?(state)
744
+ send(attribute) == state
745
+ end unless method_defined?("#{attribute}?") || private_method_defined?("#{attribute}?")
746
+ end
194
747
  end
195
748
 
196
- # Defines an event of the system
197
- #
198
- # == Instance methods
199
- #
200
- # The following instance methods are generated when a new event is defined
201
- # (the "park" event is used as an example):
202
- # * <tt>park</tt> - Fires the "park" event, transitioning from the current state to the next valid state.
203
- # * <tt>park!</tt> - Fires the "park" event, transitioning from the current state to the next valid state. If the transition cannot happen (for validation, database, etc. reasons), then an error will be raised.
204
- # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given the current state of the record.
205
- #
206
- # == Defining transitions
207
- #
208
- # +event+ requires a block which allows you to define the possible
209
- # transitions that can happen as a result of that event. For example,
210
- #
211
- # event :park do
212
- # transition :to => 'parked', :from => 'idle'
213
- # end
214
- #
215
- # event :first_gear do
216
- # transition :to => 'first_gear', :from => 'parked', :if => :seatbelt_on?
217
- # end
218
- #
219
- # See PluginAWeek::StateMachine::Event#transition for more information on
220
- # the possible options that can be passed in.
221
- #
222
- # *Note* that this block is executed within the context of the actual event
223
- # object. As a result, you will not be able to reference any class methods
224
- # on the model without referencing the class itself. For example,
225
- #
226
- # class Car < ActiveRecord::Base
227
- # def self.safe_states
228
- # %w(parked idling stalled)
229
- # end
230
- #
231
- # state_machine :state do
232
- # event :park do
233
- # transition :to => 'parked', :from => Car.safe_states
234
- # end
235
- # end
236
- # end
237
- #
238
- # == Example
239
- #
240
- # class Car < ActiveRecord::Base
241
- # state_machine(:state, :initial => 'parked') do
242
- # event :park, :after => :release_seatbelt do
243
- # transition :to => 'parked', :from => %w(first_gear reverse)
244
- # end
245
- # ...
246
- # end
247
- # end
248
- def event(name, &block)
249
- name = name.to_s
250
- event = events[name] ||= Event.new(self, name)
251
- event.instance_eval(&block)
749
+ # Defines the with/without scope helpers for this attribute. Both the
750
+ # singular and plural versions of the attribute are defined for each
751
+ # scope helper. A custom plural can be specified if it cannot be
752
+ # automatically determined by either calling +pluralize+ on the attribute
753
+ # name or adding an "s" to the end of the name.
754
+ def define_scopes(custom_plural = nil)
755
+ plural = custom_plural || (attribute.respond_to?(:pluralize) ? attribute.pluralize : "#{attribute}s")
252
756
 
253
- # Record the states so that the machine can keep a list of all known
254
- # states that have been defined
255
- event.transitions.each do |transition|
256
- @states |= [transition.options[:to]] + Array(transition.options[:from]) + Array(transition.options[:except_from])
257
- @states.sort!
757
+ [attribute, plural].uniq.each do |name|
758
+ define_with_scope("with_#{name}") unless owner_class.respond_to?("with_#{name}")
759
+ define_without_scope("without_#{name}") unless owner_class.respond_to?("without_#{name}")
258
760
  end
259
-
260
- event
261
761
  end
262
762
 
263
- # Creates a callback that will be invoked *before* a transition has been
264
- # performed, so long as the given configuration options match the transition.
265
- # Each part of the transition (to state, from state, and event) must match
266
- # in order for the callback to get invoked.
267
- #
268
- # Configuration options:
269
- # * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
270
- # * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
271
- # * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
272
- # * +except_to+ - One more states *not* being transitioned to
273
- # * +except_from+ - One or more states *not* being transitioned from
274
- # * +except_on+ - One or more events that *did not* fire the transition
275
- # * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
276
- # * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
277
- # * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
278
- #
279
- # The +except+ group of options (+except_to+, +exception_from+, and
280
- # +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
281
- # +from+, and +on+, respectively)
282
- #
283
- # == The callback
284
- #
285
- # When defining additional configuration options, callbacks must be defined
286
- # in the :do option like so:
763
+ # Defines a scope for finding objects *with* a particular value or
764
+ # values for the attribute.
287
765
  #
288
- # class Vehicle < ActiveRecord::Base
289
- # state_machine do
290
- # before_transition :to => 'parked', :do => :set_alarm
291
- # ...
292
- # end
293
- # end
294
- #
295
- # == Examples
296
- #
297
- # Below is an example of a model with one state machine and various types
298
- # of +before+ transitions defined for it:
299
- #
300
- # class Vehicle < ActiveRecord::Base
301
- # state_machine do
302
- # # Before all transitions
303
- # before_transition :update_dashboard
304
- #
305
- # # Before specific transition:
306
- # before_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
307
- #
308
- # # With conditional callback:
309
- # before_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
310
- #
311
- # # Using :except counterparts:
312
- # before_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
313
- # ...
314
- # end
315
- # end
316
- #
317
- # As can be seen, any number of transitions can be created using various
318
- # combinations of configuration options.
319
- def before_transition(options = {})
320
- add_transition_callback(:before, options)
766
+ # This is only applicable to specific integrations.
767
+ def define_with_scope(name)
321
768
  end
322
769
 
323
- # Creates a callback that will be invoked *after* a transition has been
324
- # performed, so long as the given configuration options match the transition.
325
- # Each part of the transition (to state, from state, and event) must match
326
- # in order for the callback to get invoked.
327
- #
328
- # Configuration options:
329
- # * +to+ - One or more states being transitioned to. If none are specified, then all states will match.
330
- # * +from+ - One or more states being transitioned from. If none are specified, then all states will match.
331
- # * +on+ - One or more events that fired the transition. If none are specified, then all events will match.
332
- # * +except_to+ - One more states *not* being transitioned to
333
- # * +except_from+ - One or more states *not* being transitioned from
334
- # * +except_on+ - One or more events that *did not* fire the transition
335
- # * +do+ - The callback to invoke when a transition matches. This can be a method, proc or string.
336
- # * +if+ - A method, proc or string to call to determine if the callback should occur (e.g. :if => :allow_callbacks, or :if => lambda {|user| user.signup_step > 2}). The method, proc or string should return or evaluate to a true or false value.
337
- # * +unless+ - A method, proc or string to call to determine if the callback should not occur (e.g. :unless => :skip_callbacks, or :unless => lambda {|user| user.signup_step <= 2}). The method, proc or string should return or evaluate to a true or false value.
338
- #
339
- # The +except+ group of options (+except_to+, +exception_from+, and
340
- # +except_on+) acts as the +unless+ equivalent of their counterparts (+to+,
341
- # +from+, and +on+, respectively)
342
- #
343
- # == The callback
344
- #
345
- # When defining additional configuration options, callbacks must be defined
346
- # in the :do option like so:
347
- #
348
- # class Vehicle < ActiveRecord::Base
349
- # state_machine do
350
- # after_transition :to => 'parked', :do => :set_alarm
351
- # ...
352
- # end
353
- # end
354
- #
355
- # == Examples
770
+ # Defines a scope for finding objects *without* a particular value or
771
+ # values for the attribute.
356
772
  #
357
- # Below is an example of a model with one state machine and various types
358
- # of +after+ transitions defined for it:
359
- #
360
- # class Vehicle < ActiveRecord::Base
361
- # state_machine do
362
- # # After all transitions
363
- # after_transition :update_dashboard
364
- #
365
- # # After specific transition:
366
- # after_transition :to => 'parked', :from => %w(first_gear idling), :on => 'park', :do => :take_off_seatbelt
367
- #
368
- # # With conditional callback:
369
- # after_transition :to => 'parked', :do => :take_off_seatbelt, :if => :seatbelt_on?
370
- #
371
- # # Using :except counterparts:
372
- # after_transition :except_to => 'stalled', :except_from => 'stalled', :except_on => 'crash', :do => :update_dashboard
373
- # ...
374
- # end
375
- # end
376
- #
377
- # As can be seen, any number of transitions can be created using various
378
- # combinations of configuration options.
379
- def after_transition(options = {})
380
- add_transition_callback(:after, options)
773
+ # This is only applicable to specific integrations.
774
+ def define_without_scope(name)
381
775
  end
382
776
 
383
- private
384
- # Adds the given callback to the callback chain during a state transition
385
- def add_transition_callback(type, options)
386
- options = {:do => options} unless options.is_a?(Hash)
387
- options.assert_valid_keys(:to, :from, :on, :except_to, :except_from, :except_on, :do, :if, :unless)
388
-
389
- # The actual callback (defined in the :do option) must be defined
390
- raise ArgumentError, ':do callback must be specified' unless options[:do]
391
-
392
- # Create the callback
393
- owner_class.send("#{type}_transition_#{attribute}", options.delete(:do), options)
394
- end
395
-
396
- # Add before/after callbacks for when the attribute transitions to a
397
- # different value
398
- def add_transition_callbacks
399
- %w(before after).each {|type| owner_class.define_callbacks("#{type}_transition_#{attribute}") }
400
- end
777
+ # Adds a new transition callback of the given type.
778
+ def add_callback(type, options, &block)
779
+ @callbacks[type] << callback = Callback.new(options, &block)
780
+ add_states(callback.known_states)
781
+ callback
782
+ end
783
+
784
+ # Tracks the given set of states in the list of all known states for
785
+ # this machine
786
+ def add_states(states)
787
+ new_states = states - @states
788
+ @states += new_states
401
789
 
402
- # Add named scopes for finding records with a particular value or values
403
- # for the attribute
404
- def add_named_scopes
405
- [attribute, attribute.pluralize].uniq.each do |name|
406
- name = "with_#{name}"
407
- owner_class.named_scope name.to_sym, lambda {|*values| {:conditions => {attribute => values.flatten}}} unless owner_class.respond_to?(name)
790
+ # Add state predicates
791
+ attribute = self.attribute
792
+ new_states.each do |state|
793
+ if state.is_a?(String) || state.is_a?(Symbol)
794
+ name = "#{state}?"
795
+
796
+ owner_class.class_eval do
797
+ # Checks whether the current state is equal to the given value
798
+ define_method(name) do
799
+ self.send(attribute) == state
800
+ end unless method_defined?(name) || private_method_defined?(name)
801
+ end
408
802
  end
409
803
  end
410
- end
804
+ end
411
805
  end
412
806
  end