state_machine 0.6.3 → 0.7.0

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