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.
- 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
|