state_machine 0.6.3 → 0.7.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 (56) hide show
  1. data/CHANGELOG.rdoc +31 -1
  2. data/README.rdoc +33 -21
  3. data/Rakefile +2 -2
  4. data/examples/merb-rest/controller.rb +51 -0
  5. data/examples/merb-rest/model.rb +28 -0
  6. data/examples/merb-rest/view_edit.html.erb +24 -0
  7. data/examples/merb-rest/view_index.html.erb +23 -0
  8. data/examples/merb-rest/view_new.html.erb +13 -0
  9. data/examples/merb-rest/view_show.html.erb +17 -0
  10. data/examples/rails-rest/controller.rb +43 -0
  11. data/examples/rails-rest/migration.rb +11 -0
  12. data/examples/rails-rest/model.rb +23 -0
  13. data/examples/rails-rest/view_edit.html.erb +25 -0
  14. data/examples/rails-rest/view_index.html.erb +23 -0
  15. data/examples/rails-rest/view_new.html.erb +14 -0
  16. data/examples/rails-rest/view_show.html.erb +17 -0
  17. data/lib/state_machine/assertions.rb +2 -2
  18. data/lib/state_machine/callback.rb +14 -8
  19. data/lib/state_machine/condition_proxy.rb +3 -3
  20. data/lib/state_machine/event.rb +19 -21
  21. data/lib/state_machine/event_collection.rb +114 -0
  22. data/lib/state_machine/extensions.rb +127 -11
  23. data/lib/state_machine/guard.rb +1 -1
  24. data/lib/state_machine/integrations/active_record/locale.rb +2 -1
  25. data/lib/state_machine/integrations/active_record.rb +117 -39
  26. data/lib/state_machine/integrations/data_mapper/observer.rb +20 -64
  27. data/lib/state_machine/integrations/data_mapper.rb +71 -26
  28. data/lib/state_machine/integrations/sequel.rb +69 -21
  29. data/lib/state_machine/machine.rb +267 -139
  30. data/lib/state_machine/machine_collection.rb +145 -0
  31. data/lib/state_machine/matcher.rb +2 -2
  32. data/lib/state_machine/node_collection.rb +9 -4
  33. data/lib/state_machine/state.rb +22 -32
  34. data/lib/state_machine/state_collection.rb +66 -17
  35. data/lib/state_machine/transition.rb +259 -28
  36. data/lib/state_machine.rb +121 -56
  37. data/tasks/state_machine.rake +1 -0
  38. data/tasks/state_machine.rb +26 -0
  39. data/test/active_record.log +116877 -0
  40. data/test/functional/state_machine_test.rb +118 -12
  41. data/test/sequel.log +28542 -0
  42. data/test/unit/callback_test.rb +46 -1
  43. data/test/unit/condition_proxy_test.rb +55 -28
  44. data/test/unit/event_collection_test.rb +228 -0
  45. data/test/unit/event_test.rb +51 -46
  46. data/test/unit/integrations/active_record_test.rb +128 -70
  47. data/test/unit/integrations/data_mapper_test.rb +150 -58
  48. data/test/unit/integrations/sequel_test.rb +63 -6
  49. data/test/unit/invalid_event_test.rb +7 -0
  50. data/test/unit/machine_collection_test.rb +678 -0
  51. data/test/unit/machine_test.rb +198 -91
  52. data/test/unit/node_collection_test.rb +33 -30
  53. data/test/unit/state_collection_test.rb +112 -5
  54. data/test/unit/state_test.rb +23 -3
  55. data/test/unit/transition_test.rb +750 -89
  56. metadata +28 -3
@@ -28,10 +28,41 @@ module StateMachine
28
28
  #
29
29
  # For example,
30
30
  #
31
- # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
31
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
32
32
  # vehicle.name = 'Ford Explorer'
33
33
  # vehicle.ignite # => true
34
- # vehicle.refresh # => #<Vehicle id=1 name="Ford Explorer" state="idling">
34
+ # vehicle.refresh # => #<Vehicle @values={:state=>"idling", :name=>"Ford Explorer", :id=>1}>
35
+ #
36
+ # == Events
37
+ #
38
+ # As described in StateMachine::InstanceMethods#state_machine, event
39
+ # attributes are created for every machine that allow transitions to be
40
+ # performed automatically when the object's action (in this case, :save)
41
+ # is called.
42
+ #
43
+ # In Sequel, these automated events are run in the following order:
44
+ # * before validation - Run before callbacks and persist new states, then validate
45
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
46
+ # * after save - Run after callbacks
47
+ #
48
+ # For example,
49
+ #
50
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
51
+ # vehicle.state_event # => nil
52
+ # vehicle.state_event = 'invalid'
53
+ # vehicle.valid? # => false
54
+ # vehicle.errors.full_messages # => ["state_event is invalid"]
55
+ #
56
+ # vehicle.state_event = 'ignite'
57
+ # vehicle.valid? # => true
58
+ # vehicle.save # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
59
+ # vehicle.state # => "idling"
60
+ # vehicle.state_event # => nil
61
+ #
62
+ # Note that this can also be done on a mass-assignment basis:
63
+ #
64
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
65
+ # vehicle.state # => "idling"
35
66
  #
36
67
  # == Transactions
37
68
  #
@@ -51,7 +82,7 @@ module StateMachine
51
82
  # end
52
83
  # end
53
84
  #
54
- # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
85
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
55
86
  # vehicle.ignite # => false
56
87
  # Message.count # => 0
57
88
  #
@@ -60,6 +91,14 @@ module StateMachine
60
91
  # rolled back. If an after callback halts the chain, the previous result
61
92
  # still applies and the transaction is *not* rolled back.
62
93
  #
94
+ # To turn off transactions:
95
+ #
96
+ # class Vehicle < Sequel::Model
97
+ # state_machine :initial => :parked, :use_transactions => false do
98
+ # ...
99
+ # end
100
+ # end
101
+ #
63
102
  # == Validation errors
64
103
  #
65
104
  # If an event fails to successfully fire because there are no matching
@@ -69,9 +108,9 @@ module StateMachine
69
108
  #
70
109
  # For example,
71
110
  #
72
- # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
111
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
73
112
  # vehicle.ignite # => false
74
- # vehicle.errors.full_messages # => ["state cannot be transitioned via :ignite from :idling"]
113
+ # vehicle.errors.full_messages # => ["state cannot transition via \"ignite\""]
75
114
  #
76
115
  # If an event fails to fire because of a validation error on the record and
77
116
  # *not* because a matching transition was not available, no error messages
@@ -139,6 +178,10 @@ module StateMachine
139
178
  # Note, also, that the transition can be accessed by simply defining
140
179
  # additional arguments in the callback block.
141
180
  module Sequel
181
+ # The default options to use for state machines using this integration
182
+ class << self; attr_reader :defaults; end
183
+ @defaults = {:action => :save}
184
+
142
185
  # Should this integration be used for state machines in the given class?
143
186
  # Classes that include Sequel::Model will automatically use the Sequel
144
187
  # integration.
@@ -146,31 +189,30 @@ module StateMachine
146
189
  defined?(::Sequel::Model) && klass <= ::Sequel::Model
147
190
  end
148
191
 
149
- # Adds a validation error to the given object after failing to fire a
150
- # specific event
151
- def invalidate(object, event)
152
- object.errors.add(attribute, invalid_message(object, event))
192
+ # Adds a validation error to the given object
193
+ def invalidate(object, attribute, message, values = [])
194
+ object.errors.add(attribute, generate_message(message, values))
153
195
  end
154
196
 
155
- # Resets an errors previously added when invalidating the given object
197
+ # Resets any errors previously added when invalidating the given object
156
198
  def reset(object)
157
199
  object.errors.clear
158
200
  end
159
201
 
160
- # Runs a new database transaction, rolling back any changes if the
161
- # yielded block fails (i.e. returns false).
162
- def within_transaction(object)
163
- object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
164
- end
165
-
166
202
  protected
167
- # Sets the default action for all Sequel state machines to +save+
168
- def default_action
169
- :save
203
+ # Skips defining reader/writer methods since this is done automatically
204
+ def define_state_accessor
170
205
  end
171
206
 
172
- # Skips defining reader/writer methods since this is done automatically
173
- def define_attribute_accessor
207
+ # Adds hooks into validation for automatically firing events
208
+ def define_action_helpers
209
+ if super && action == :save
210
+ @instance_helper_module.class_eval do
211
+ define_method(:valid?) do |*args|
212
+ self.class.state_machines.fire_attribute_events(self, :save, false) { super(*args) }
213
+ end
214
+ end
215
+ end
174
216
  end
175
217
 
176
218
  # Creates a scope for finding records *with* a particular state or
@@ -187,6 +229,12 @@ module StateMachine
187
229
  lambda {|model, values| model.filter(~{attribute.to_sym => values})}
188
230
  end
189
231
 
232
+ # Runs a new database transaction, rolling back any changes if the
233
+ # yielded block fails (i.e. returns false).
234
+ def transaction(object)
235
+ object.db.transaction {raise ::Sequel::Error::Rollback unless yield}
236
+ end
237
+
190
238
  # Creates a new callback in the callback chain, always ensuring that
191
239
  # it's configured to bind to the object as this is the convention for
192
240
  # Sequel callbacks
@@ -7,16 +7,67 @@ require 'state_machine/event'
7
7
  require 'state_machine/callback'
8
8
  require 'state_machine/node_collection'
9
9
  require 'state_machine/state_collection'
10
+ require 'state_machine/event_collection'
10
11
  require 'state_machine/matcher_helpers'
11
12
 
12
13
  module StateMachine
13
14
  # Represents a state machine for a particular attribute. State machines
14
- # consist of states, events and a set of transitions that define how the state
15
- # changes after a particular event is fired.
15
+ # consist of states, events and a set of transitions that define how the
16
+ # state changes after a particular event is fired.
16
17
  #
17
- # A state machine will not know all of the possible states for an object unless
18
- # they are referenced *somewhere* in the state machine definition. As a result,
19
- # any unused states should be defined with the +other_states+ or +state+ helper.
18
+ # A state machine will not know all of the possible states for an object
19
+ # unless they are referenced *somewhere* in the state machine definition.
20
+ # As a result, any unused states should be defined with the +other_states+
21
+ # or +state+ helper.
22
+ #
23
+ # == Actions
24
+ #
25
+ # When an action is configured for a state machine, it is invoked when an
26
+ # object transitions via an event. The success of the event becomes
27
+ # dependent on the success of the action. If the action is successful, then
28
+ # the transitioned state remains persisted. However, if the action fails
29
+ # (by returning false), the transitioned state will be rolled back.
30
+ #
31
+ # For example,
32
+ #
33
+ # class Vehicle
34
+ # attr_accessor :fail, :saving_state
35
+ #
36
+ # state_machine :initial => :parked, :action => :save do
37
+ # event :ignite do
38
+ # transition :parked => :idling
39
+ # end
40
+ #
41
+ # event :park do
42
+ # transition :idling => :parked
43
+ # end
44
+ # end
45
+ #
46
+ # def save
47
+ # @saving_state = state
48
+ # fail != true
49
+ # end
50
+ # end
51
+ #
52
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
53
+ # vehicle.save # => true
54
+ # vehicle.saving_state # => "parked" # The state was "parked" was save was called
55
+ #
56
+ # # Successful event
57
+ # vehicle.ignite # => true
58
+ # vehicle.saving_state # => "idling" # The state was "idling" when save was called
59
+ # vehicle.state # => "idling"
60
+ #
61
+ # # Failed event
62
+ # vehicle.fail = true
63
+ # vehicle.park # => false
64
+ # vehicle.saving_state # => "parked"
65
+ # vehicle.state # => "idling"
66
+ #
67
+ # As shown, even though the state is set prior to calling the +save+ action
68
+ # on the object, it will be rolled back to the original state if the action
69
+ # fails. *Note* that this will also be the case if an exception is raised
70
+ # while calling the action.
20
71
  #
21
72
  # == Callbacks
22
73
  #
@@ -26,6 +77,32 @@ module StateMachine
26
77
  # and StateMachine::Machine#after_transition for documentation
27
78
  # on how to define new callbacks.
28
79
  #
80
+ # *Note* that callbacks only get executed within the context of an event.
81
+ # As a result, if a class has an initial state when it's created, any
82
+ # callbacks that would normally get executed when the object enters that
83
+ # state will *not* get triggered.
84
+ #
85
+ # For example,
86
+ #
87
+ # class Vehicle
88
+ # state_machine :initial => :parked do
89
+ # after_transition all => :parked do
90
+ # raise ArgumentError
91
+ # end
92
+ # ...
93
+ # end
94
+ # end
95
+ #
96
+ # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
97
+ # vehicle.save # => true (no exception raised)
98
+ #
99
+ # If you need callbacks to get triggered when an object is created, this
100
+ # should be done by either:
101
+ # * Use a <tt>before :save</tt> or equivalent hook, or
102
+ # * Set an initial state of nil and use the correct event to create the
103
+ # object with the proper state, resulting in callbacks being triggered and
104
+ # the object getting persisted
105
+ #
29
106
  # === Canceling callbacks
30
107
  #
31
108
  # Callbacks can be canceled by throwing :halt at any point during the
@@ -87,7 +164,7 @@ module StateMachine
87
164
  # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
88
165
  # end
89
166
  #
90
- # def self.after_transition(vehicle, transition, result)
167
+ # def self.after_transition(vehicle, transition)
91
168
  # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
92
169
  # end
93
170
  # end
@@ -111,13 +188,13 @@ module StateMachine
111
188
  # end
112
189
  #
113
190
  # [Vehicle, Switch, Project].each do |klass|
114
- # klass.state_machines.each do |machine|
191
+ # klass.state_machines.each do |attribute, machine|
115
192
  # machine.before_transition klass.method(:before_transition)
116
193
  # end
117
194
  # end
118
195
  #
119
196
  # Additional observer-like behavior may be exposed by the various integrations
120
- # available. See below for more information.
197
+ # available. See below for more information on integrations.
121
198
  #
122
199
  # == Overriding instance / class methods
123
200
  #
@@ -129,25 +206,20 @@ module StateMachine
129
206
  # class Vehicle
130
207
  # state_machine do
131
208
  # event :park do
132
- # transition :idling => :parked
209
+ # ...
133
210
  # end
134
211
  # end
135
212
  #
136
- # def park(kind = :parallel, *args)
137
- # take_deep_breath if kind == :parallel
138
- # super(*args)
139
- # end
140
- #
141
- # def take_deep_breath
142
- # sleep 3
213
+ # def park(*args)
214
+ # logger.info "..."
215
+ # super
143
216
  # end
144
217
  # end
145
218
  #
146
219
  # In the above example, the +park+ instance method that's generated on the
147
- # Vehicle class (by the associated event) is overriden with custom behavior
148
- # that takes an additional argument. Once this behavior is complete, the
149
- # original method from the state machine is invoked by simply calling
150
- # <tt>super(*args)</tt>.
220
+ # Vehicle class (by the associated event) is overridden with custom behavior.
221
+ # Once this behavior is complete, the original method from the state machine
222
+ # is invoked by simply calling +super+.
151
223
  #
152
224
  # The same technique can be used for +state+, +state_name+, and all other
153
225
  # instance *and* class methods on the Vehicle class.
@@ -175,10 +247,6 @@ module StateMachine
175
247
  include MatcherHelpers
176
248
 
177
249
  class << self
178
- # The default message to use when invalidating objects that fail to
179
- # transition when triggering an event
180
- attr_accessor :default_invalid_message
181
-
182
250
  # Attempts to find or create a state machine for the given class. For
183
251
  # example,
184
252
  #
@@ -194,16 +262,17 @@ module StateMachine
194
262
  options = args.last.is_a?(Hash) ? args.pop : {}
195
263
  attribute = args.first || :state
196
264
 
197
- # Attempts to find an existing machine
265
+ # Find an existing machine
198
266
  if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
199
- # Create a copy of the state machine if it's being created by a subclass
200
- unless machine.owner_class == owner_class
267
+ # Only create a new copy if changes are being made to the machine in
268
+ # a subclass
269
+ if machine.owner_class != owner_class && (options.any? || block_given?)
201
270
  machine = machine.clone
202
271
  machine.initial_state = options[:initial] if options.include?(:initial)
203
272
  machine.owner_class = owner_class
204
273
  end
205
274
 
206
- # Evaluate DSL caller block
275
+ # Evaluate DSL
207
276
  machine.instance_eval(&block) if block_given?
208
277
  else
209
278
  # No existing machine: create a new one
@@ -245,8 +314,13 @@ module StateMachine
245
314
  end
246
315
  end
247
316
 
248
- # Set defaults
249
- self.default_invalid_message = 'cannot be transitioned via :%s from :%s'
317
+ # Default messages to use for validation errors in ORM integrations
318
+ class << self; attr_accessor :default_messages; end
319
+ @default_messages = {
320
+ :invalid => 'is invalid',
321
+ :invalid_event => 'cannot transition when %s',
322
+ :invalid_transition => 'cannot transition via "%s"'
323
+ }
250
324
 
251
325
  # The class that the machine is defined in
252
326
  attr_accessor :owner_class
@@ -254,8 +328,8 @@ module StateMachine
254
328
  # The attribute for which the machine is being defined
255
329
  attr_reader :attribute
256
330
 
257
- # The events that trigger transitions. These are sorted, by default, in the
258
- # order in which they were defined.
331
+ # The events that trigger transitions. These are sorted, by default, in
332
+ # the order in which they were defined.
259
333
  attr_reader :events
260
334
 
261
335
  # A list of all of the states known to this state machine. This will pull
@@ -282,36 +356,41 @@ module StateMachine
282
356
  # depending on the context.
283
357
  attr_reader :namespace
284
358
 
359
+ # Whether the machine will use transactions when firing events
360
+ attr_reader :use_transactions
361
+
285
362
  # Creates a new state machine for the given attribute
286
363
  def initialize(owner_class, *args, &block)
287
364
  options = args.last.is_a?(Hash) ? args.pop : {}
288
- assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration, :invalid_message)
365
+ assert_valid_keys(options, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
366
+
367
+ # Find an integration that matches this machine's owner class
368
+ if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class)
369
+ extend integration
370
+ options = integration.defaults.merge(options) if integration.respond_to?(:defaults)
371
+ end
372
+
373
+ # Add machine-wide defaults
374
+ options = {:use_transactions => true}.merge(options)
289
375
 
290
376
  # Set machine configuration
291
377
  @attribute = args.first || :state
292
- @events = NodeCollection.new
293
- @states = StateCollection.new
378
+ @events = EventCollection.new(self)
379
+ @states = StateCollection.new(self)
294
380
  @callbacks = {:before => [], :after => []}
295
381
  @namespace = options[:namespace]
296
- @invalid_message = options[:invalid_message]
297
-
382
+ @messages = options[:messages] || {}
383
+ @action = options[:action]
384
+ @use_transactions = options[:use_transactions]
298
385
  self.owner_class = owner_class
299
386
  self.initial_state = options[:initial]
300
387
 
301
- # Find an integration that matches this machine's owner class
302
- if integration = options[:integration] ? StateMachine::Integrations.find(options[:integration]) : StateMachine::Integrations.match(owner_class)
303
- extend integration
304
- end
305
-
306
- # Set integration-specific configurations
307
- @action = options.include?(:action) ? options[:action] : default_action
308
- define_attribute_helpers
388
+ # Define class integration
389
+ define_helpers
309
390
  define_scopes(options[:plural])
310
-
311
- # Call after hook for integration-specific extensions
312
391
  after_initialize
313
392
 
314
- # Evaluate DSL caller block
393
+ # Evaluate DSL
315
394
  instance_eval(&block) if block_given?
316
395
  end
317
396
 
@@ -369,22 +448,18 @@ module StateMachine
369
448
  # class. If the method is already defined in the class, then this will not
370
449
  # override it.
371
450
  #
372
- # Not that in order for inheritance to work properly within state machines,
373
- # any states/events/etc. must be referred to from the current state machine
374
- # associated with the executing class.
375
- #
376
451
  # Example:
377
452
  #
378
453
  # attribute = machine.attribute
379
- # machine.define_instance_method(:parked?) do |machine, object|
380
- # machine.state?(object, :parked)
454
+ # machine.define_instance_method(:state_name) do |machine, object|
455
+ # machine.states.match(object)
381
456
  # end
382
457
  def define_instance_method(method, &block)
383
458
  attribute = self.attribute
384
459
 
385
460
  @instance_helper_module.class_eval do
386
461
  define_method(method) do |*args|
387
- block.call(self.class.state_machines[attribute], self, *args)
462
+ block.call(self.class.state_machine(attribute), self, *args)
388
463
  end
389
464
  end
390
465
  end
@@ -394,10 +469,6 @@ module StateMachine
394
469
  # class. If the method is already defined in the class, then this will not
395
470
  # override it.
396
471
  #
397
- # Not that in order for inheritance to work properly within state machines,
398
- # any states/events/etc. must be referred to from the current state machine
399
- # associated with the executing class.
400
- #
401
472
  # Example:
402
473
  #
403
474
  # machine.define_class_method(:states) do |machine, klass|
@@ -408,7 +479,7 @@ module StateMachine
408
479
 
409
480
  @class_helper_module.class_eval do
410
481
  define_method(method) do |*args|
411
- block.call(self.state_machines[attribute], self, *args)
482
+ block.call(self.state_machine(attribute), self, *args)
412
483
  end
413
484
  end
414
485
  end
@@ -428,7 +499,7 @@ module StateMachine
428
499
  # end
429
500
  #
430
501
  # vehicle = Vehicle.new
431
- # Vehicle.state_machines[:state].initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
502
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
432
503
  #
433
504
  # With a dynamic initial state:
434
505
  #
@@ -443,10 +514,10 @@ module StateMachine
443
514
  # vehicle = Vehicle.new
444
515
  #
445
516
  # vehicle.force_idle = true
446
- # Vehicle.state_machines[:state].initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
517
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
447
518
  #
448
519
  # vehicle.force_idle = false
449
- # Vehicle.state_machines[:state].initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
520
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
450
521
  def initial_state(object)
451
522
  states.fetch(@initial_state.is_a?(Proc) ? @initial_state.call(object) : @initial_state)
452
523
  end
@@ -623,7 +694,6 @@ module StateMachine
623
694
  # vehicle.rotate_driver # => true
624
695
  # vehicle.driver # => "John"
625
696
  # vehicle.passenger # => "Jane"
626
- # vehicle.state # => "parked"
627
697
  #
628
698
  # As can be seen, both the +speed+ and +rotate_driver+ instance method
629
699
  # implementations changed how they behave based on what the current state
@@ -692,58 +762,37 @@ module StateMachine
692
762
  end
693
763
  alias_method :other_states, :state
694
764
 
695
- # Determines whether the given object is in a specific state. If the
696
- # object's current value doesn't match the state, then this will return
697
- # false, otherwise true. If the given state is unknown, then an ArgumentError
698
- # will be raised.
765
+ # Gets the current value stored in the given object's state.
699
766
  #
700
- # == Examples
767
+ # For example,
701
768
  #
702
769
  # class Vehicle
703
770
  # state_machine :initial => :parked do
704
- # other_states :idling
771
+ # ...
705
772
  # end
706
773
  # end
707
774
  #
708
- # machine = Vehicle.state_machines[:state]
709
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
710
- #
711
- # machine.state?(vehicle, :parked) # => true
712
- # machine.state?(vehicle, :idling) # => false
713
- # machine.state?(vehicle, :invalid) # => ArgumentError: :invalid is an invalid key for :name index
714
- def state?(object, name)
715
- states.fetch(name).matches?(object.send(attribute))
775
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
776
+ # Vehicle.state_machine.read(vehicle) # => "parked"
777
+ def read(object)
778
+ object.send(attribute)
716
779
  end
717
780
 
718
- # Determines the current state of the given object as configured by this
719
- # state machine. This will attempt to find a known state that matches
720
- # the value of the attribute on the object. If no state is found, then
721
- # an ArgumentError will be raised.
781
+ # Sets a new value in the given object's state.
722
782
  #
723
- # == Examples
783
+ # For example,
724
784
  #
725
785
  # class Vehicle
726
786
  # state_machine :initial => :parked do
727
- # other_states :idling
787
+ # ...
728
788
  # end
729
789
  # end
730
790
  #
731
- # machine = Vehicle.state_machines[:state]
732
- #
733
- # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
734
- # machine.state_for(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
735
- #
736
- # vehicle.state = 'idling'
737
- # machine.state_for(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
738
- #
739
- # vehicle.state = 'invalid'
740
- # machine.state_for(vehicle) # => ArgumentError: "invalid" is not a known state value
741
- def state_for(object)
742
- value = object.send(attribute)
743
- state = states[value, :value] || states.detect {|state| state.matches?(value)}
744
- raise ArgumentError, "#{value.inspect} is not a known #{attribute} value" unless state
745
-
746
- state
791
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
792
+ # Vehicle.state_machine.write(vehicle, 'idling')
793
+ # vehicle.state # => "idling"
794
+ def write(object, value)
795
+ object.send("#{attribute}=", value)
747
796
  end
748
797
 
749
798
  # Defines one or more events for the machine and the transitions that can
@@ -758,7 +807,7 @@ module StateMachine
758
807
  # (the "park" event is used as an example):
759
808
  # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
760
809
  # the current state of the object.
761
- # * <tt>next_park_transition</tt> - Gets the next transition that would be
810
+ # * <tt>park_transition</tt> - Gets the next transition that would be
762
811
  # performed if the "park" event were to be fired now on the object or nil
763
812
  # if no transitions can be performed.
764
813
  # * <tt>park(run_action = true)</tt> - Fires the "park" event, transitioning
@@ -769,7 +818,7 @@ module StateMachine
769
818
  #
770
819
  # With a namespace of "car", the above names map to the following methods:
771
820
  # * <tt>can_park_car?</tt>
772
- # * <tt>next_park_car_transition</tt>
821
+ # * <tt>park_car_transition</tt>
773
822
  # * <tt>park_car</tt>
774
823
  # * <tt>park_car!</tt>
775
824
  #
@@ -805,6 +854,36 @@ module StateMachine
805
854
  # end
806
855
  # end
807
856
  #
857
+ # == Defining additional arguments
858
+ #
859
+ # Additional arguments on event actions can be defined like so:
860
+ #
861
+ # class Vehicle
862
+ # state_machine do
863
+ # event :park do
864
+ # ...
865
+ # end
866
+ # end
867
+ #
868
+ # def park(kind = :parallel, *args)
869
+ # take_deep_breath if kind == :parallel
870
+ # super
871
+ # end
872
+ #
873
+ # def take_deep_breath
874
+ # sleep 3
875
+ # end
876
+ # end
877
+ #
878
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This
879
+ # allows the entire arguments list to be accessed by transition callbacks
880
+ # through StateMachine::Transition#args like so:
881
+ #
882
+ # after_transition :on => :park do |vehicle, transition|
883
+ # kind = *transition.args
884
+ # ...
885
+ # end
886
+ #
808
887
  # == Example
809
888
  #
810
889
  # class Vehicle
@@ -1002,11 +1081,10 @@ module StateMachine
1002
1081
  add_callback(:after, options.is_a?(Hash) ? options : {:do => options}, &block)
1003
1082
  end
1004
1083
 
1005
- # Marks the given object as invalid after failing to transition via the
1006
- # given event.
1084
+ # Marks the given object as invalid with the given message.
1007
1085
  #
1008
1086
  # By default, this is a no-op.
1009
- def invalidate(object, event)
1087
+ def invalidate(object, attribute, message, values = [])
1010
1088
  end
1011
1089
 
1012
1090
  # Resets an errors previously added when invalidating the given object
@@ -1021,7 +1099,11 @@ module StateMachine
1021
1099
  # default, this will not run any transactions, since the changes aren't
1022
1100
  # taking place within the context of a database.
1023
1101
  def within_transaction(object)
1024
- yield
1102
+ if use_transactions
1103
+ transaction(object) { yield }
1104
+ else
1105
+ yield
1106
+ end
1025
1107
  end
1026
1108
 
1027
1109
  # Draws a directed graph of the machine for visualizing the various events,
@@ -1090,44 +1172,89 @@ module StateMachine
1090
1172
  def after_initialize
1091
1173
  end
1092
1174
 
1093
- # Gets the default action that should be invoked when performing a
1094
- # transition on the attribute for this machine. This may change
1095
- # depending on the configured integration for the owner class.
1096
- def default_action
1097
- end
1098
-
1099
- # Adds helper methods for interacting with this state machine's attribute,
1100
- # including reader, writer, and predicate methods
1101
- def define_attribute_helpers
1102
- define_attribute_accessor
1103
- define_attribute_predicate
1104
-
1105
- attribute = self.attribute
1175
+ # Adds helper methods for interacting with the state machine, including
1176
+ # for states, events, and transitions
1177
+ def define_helpers
1178
+ define_state_accessor
1179
+ define_state_predicate
1180
+ define_event_helpers
1181
+ define_action_helpers if action
1106
1182
 
1107
1183
  # Gets the state name for the current value
1108
1184
  define_instance_method("#{attribute}_name") do |machine, object|
1109
- machine.state_for(object).name
1185
+ machine.states.match(object).name
1110
1186
  end
1111
1187
  end
1112
1188
 
1113
- # Adds reader/writer methods for accessing the attribute
1114
- def define_attribute_accessor
1189
+ # Adds reader/writer methods for accessing the state attribute
1190
+ def define_state_accessor
1115
1191
  attribute = self.attribute
1116
1192
 
1117
1193
  @instance_helper_module.class_eval do
1118
- attr_reader attribute
1119
- attr_writer attribute
1194
+ attr_accessor attribute
1120
1195
  end
1121
1196
  end
1122
1197
 
1123
1198
  # Adds predicate method to the owner class for determining the name of the
1124
1199
  # current state
1125
- def define_attribute_predicate
1126
- attribute = self.attribute
1127
-
1128
- # Checks whether the current state is a given value
1200
+ def define_state_predicate
1129
1201
  define_instance_method("#{attribute}?") do |machine, object, state|
1130
- machine.state?(object, state)
1202
+ machine.states.matches?(object, state)
1203
+ end
1204
+ end
1205
+
1206
+ # Adds helper methods for getting information about this state machine's
1207
+ # events
1208
+ def define_event_helpers
1209
+ # Gets the events that are allowed to fire on the current object
1210
+ define_instance_method("#{attribute}_events") do |machine, object|
1211
+ machine.events.valid_for(object).map {|event| event.name}
1212
+ end
1213
+
1214
+ # Gets the next possible transitions that can be run on the current
1215
+ # object
1216
+ define_instance_method("#{attribute}_transitions") do |machine, object, *args|
1217
+ machine.events.transitions_for(object, *args)
1218
+ end
1219
+
1220
+ # Add helpers for interacting with the action
1221
+ if action
1222
+ attribute = self.attribute
1223
+
1224
+ # Tracks the event / transition to invoke when the action is called
1225
+ @instance_helper_module.class_eval do
1226
+ attr_writer "#{attribute}_event"
1227
+ attr_accessor "#{attribute}_event_transition"
1228
+ end
1229
+
1230
+ # Interpret non-blank events as present
1231
+ define_instance_method("#{attribute}_event") do |machine, object|
1232
+ event = object.instance_variable_get("@#{attribute}_event")
1233
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1234
+ end
1235
+ end
1236
+ end
1237
+
1238
+ # Adds helper methods for automatically firing events when an action
1239
+ # is invoked
1240
+ def define_action_helpers
1241
+ action = self.action
1242
+
1243
+ if owner_class.method_defined?(action) && !owner_class.state_machines.any? {|attribute, machine| machine.action == action && machine != self}
1244
+ # Action is defined and hasn't already been overridden by another machine
1245
+ @instance_helper_module.class_eval do
1246
+ # Override the default action to invoke the before / after hooks
1247
+ define_method(action) do |*args|
1248
+ value = nil
1249
+ result = self.class.state_machines.fire_attribute_events(self, action) { value = super(*args) }
1250
+ value.nil? ? result : value
1251
+ end
1252
+ end
1253
+
1254
+ true
1255
+ else
1256
+ # Action already defined: don't add integration-specific hooks
1257
+ false
1131
1258
  end
1132
1259
  end
1133
1260
 
@@ -1137,7 +1264,6 @@ module StateMachine
1137
1264
  # automatically determined by either calling +pluralize+ on the attribute
1138
1265
  # name or adding an "s" to the end of the name.
1139
1266
  def define_scopes(custom_plural = nil)
1140
- attribute = self.attribute
1141
1267
  plural = custom_plural || (attribute.to_s.respond_to?(:pluralize) ? attribute.to_s.pluralize : "#{attribute}s")
1142
1268
 
1143
1269
  [attribute, plural].uniq.each do |name|
@@ -1148,10 +1274,7 @@ module StateMachine
1148
1274
  # Converts state names to their corresponding values so that they
1149
1275
  # can be looked up properly
1150
1276
  define_class_method(method) do |machine, klass, *states|
1151
- machine_states = machine.states
1152
- values = states.flatten.map {|state| machine_states.fetch(state).value}
1153
-
1154
- # Invoke the original scope implementation
1277
+ values = states.flatten.map {|state| machine.states.fetch(state).value}
1155
1278
  scope.call(klass, values)
1156
1279
  end
1157
1280
  end
@@ -1162,17 +1285,22 @@ module StateMachine
1162
1285
  # Creates a scope for finding objects *with* a particular value or values
1163
1286
  # for the attribute.
1164
1287
  #
1165
- # This is only applicable to specific integrations.
1288
+ # By default, this is a no-op.
1166
1289
  def create_with_scope(name)
1167
1290
  end
1168
1291
 
1169
1292
  # Creates a scope for finding objects *without* a particular value or
1170
1293
  # values for the attribute.
1171
1294
  #
1172
- # This is only applicable to specific integrations.
1295
+ # By default, this is a no-op.
1173
1296
  def create_without_scope(name)
1174
1297
  end
1175
1298
 
1299
+ # Always yields
1300
+ def transaction(object)
1301
+ yield
1302
+ end
1303
+
1176
1304
  # Adds a new transition callback of the given type.
1177
1305
  def add_callback(type, options, &block)
1178
1306
  callbacks[type] << callback = Callback.new(options, &block)
@@ -1183,7 +1311,7 @@ module StateMachine
1183
1311
  # Tracks the given set of states in the list of all known states for
1184
1312
  # this machine
1185
1313
  def add_states(new_states)
1186
- new_states.collect do |new_state|
1314
+ new_states.map do |new_state|
1187
1315
  unless state = states[new_state]
1188
1316
  states << state = State.new(self, new_state)
1189
1317
  end
@@ -1194,8 +1322,8 @@ module StateMachine
1194
1322
 
1195
1323
  # Generates the message to use when invalidating the given object after
1196
1324
  # failing to transition on a specific event
1197
- def invalid_message(object, event)
1198
- (@invalid_message || self.class.default_invalid_message) % [event.name, state_for(object).name]
1325
+ def generate_message(name, values)
1326
+ (@messages[name] || self.class.default_messages[name]) % values.map {|value| value.last}
1199
1327
  end
1200
1328
  end
1201
1329
  end