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.
- data/CHANGELOG.rdoc +31 -1
- data/README.rdoc +33 -21
- data/Rakefile +2 -2
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/lib/state_machine/assertions.rb +2 -2
- data/lib/state_machine/callback.rb +14 -8
- data/lib/state_machine/condition_proxy.rb +3 -3
- data/lib/state_machine/event.rb +19 -21
- data/lib/state_machine/event_collection.rb +114 -0
- data/lib/state_machine/extensions.rb +127 -11
- data/lib/state_machine/guard.rb +1 -1
- data/lib/state_machine/integrations/active_record/locale.rb +2 -1
- data/lib/state_machine/integrations/active_record.rb +117 -39
- data/lib/state_machine/integrations/data_mapper/observer.rb +20 -64
- data/lib/state_machine/integrations/data_mapper.rb +71 -26
- data/lib/state_machine/integrations/sequel.rb +69 -21
- data/lib/state_machine/machine.rb +267 -139
- data/lib/state_machine/machine_collection.rb +145 -0
- data/lib/state_machine/matcher.rb +2 -2
- data/lib/state_machine/node_collection.rb +9 -4
- data/lib/state_machine/state.rb +22 -32
- data/lib/state_machine/state_collection.rb +66 -17
- data/lib/state_machine/transition.rb +259 -28
- data/lib/state_machine.rb +121 -56
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +26 -0
- data/test/active_record.log +116877 -0
- data/test/functional/state_machine_test.rb +118 -12
- data/test/sequel.log +28542 -0
- data/test/unit/callback_test.rb +46 -1
- data/test/unit/condition_proxy_test.rb +55 -28
- data/test/unit/event_collection_test.rb +228 -0
- data/test/unit/event_test.rb +51 -46
- data/test/unit/integrations/active_record_test.rb +128 -70
- data/test/unit/integrations/data_mapper_test.rb +150 -58
- data/test/unit/integrations/sequel_test.rb +63 -6
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +678 -0
- data/test/unit/machine_test.rb +198 -91
- data/test/unit/node_collection_test.rb +33 -30
- data/test/unit/state_collection_test.rb +112 -5
- data/test/unit/state_test.rb +23 -3
- data/test/unit/transition_test.rb +750 -89
- metadata +28 -3
@@ -28,10 +28,41 @@ module StateMachine
|
|
28
28
|
#
|
29
29
|
# For example,
|
30
30
|
#
|
31
|
-
# vehicle = Vehicle.create # => #<Vehicle
|
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
|
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
|
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
|
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
|
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
|
150
|
-
|
151
|
-
|
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
|
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
|
-
#
|
168
|
-
def
|
169
|
-
:save
|
203
|
+
# Skips defining reader/writer methods since this is done automatically
|
204
|
+
def define_state_accessor
|
170
205
|
end
|
171
206
|
|
172
|
-
#
|
173
|
-
def
|
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
|
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
|
18
|
-
# they are referenced *somewhere* in the state machine definition.
|
19
|
-
# any unused states should be defined with the +other_states+
|
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
|
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
|
-
#
|
209
|
+
# ...
|
133
210
|
# end
|
134
211
|
# end
|
135
212
|
#
|
136
|
-
# def park(
|
137
|
-
#
|
138
|
-
# super
|
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
|
148
|
-
#
|
149
|
-
#
|
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
|
-
#
|
265
|
+
# Find an existing machine
|
198
266
|
if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[attribute]
|
199
|
-
#
|
200
|
-
|
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
|
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
|
-
#
|
249
|
-
|
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
|
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, :
|
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 =
|
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
|
-
@
|
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
|
-
#
|
302
|
-
|
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
|
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(:
|
380
|
-
# machine.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
#
|
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
|
-
#
|
767
|
+
# For example,
|
701
768
|
#
|
702
769
|
# class Vehicle
|
703
770
|
# state_machine :initial => :parked do
|
704
|
-
#
|
771
|
+
# ...
|
705
772
|
# end
|
706
773
|
# end
|
707
774
|
#
|
708
|
-
#
|
709
|
-
#
|
710
|
-
|
711
|
-
|
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
|
-
#
|
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
|
-
#
|
783
|
+
# For example,
|
724
784
|
#
|
725
785
|
# class Vehicle
|
726
786
|
# state_machine :initial => :parked do
|
727
|
-
#
|
787
|
+
# ...
|
728
788
|
# end
|
729
789
|
# end
|
730
790
|
#
|
731
|
-
#
|
732
|
-
#
|
733
|
-
# vehicle
|
734
|
-
|
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>
|
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>
|
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
|
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,
|
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
|
-
|
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
|
-
#
|
1094
|
-
#
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
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.
|
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
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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.
|
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
|
1198
|
-
(@
|
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
|