state_machine 0.4.1 → 0.4.2
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 +8 -0
- data/README.rdoc +55 -1
- data/Rakefile +1 -1
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/lib/state_machine.rb +146 -9
- data/lib/state_machine/assertions.rb +1 -1
- data/lib/state_machine/callback.rb +3 -2
- data/lib/state_machine/event.rb +21 -10
- data/lib/state_machine/extensions.rb +14 -56
- data/lib/state_machine/guard.rb +36 -1
- data/lib/state_machine/integrations/active_record.rb +2 -1
- data/lib/state_machine/integrations/data_mapper/observer.rb +14 -0
- data/lib/state_machine/machine.rb +215 -108
- data/lib/state_machine/state.rb +175 -0
- data/test/active_record.log +35152 -0
- data/test/data_mapper.log +9296 -0
- data/test/functional/state_machine_test.rb +106 -0
- data/test/sequel.log +5208 -0
- data/test/unit/event_test.rb +35 -0
- data/test/unit/guard_test.rb +140 -0
- data/test/unit/integrations/active_record_test.rb +34 -1
- data/test/unit/machine_test.rb +273 -72
- data/test/unit/state_test.rb +583 -0
- metadata +7 -3
- data/examples/Car_state.jpg +0 -0
data/CHANGELOG.rdoc
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
== master
|
|
2
2
|
|
|
3
|
+
== 0.4.2 / 2008-12-28
|
|
4
|
+
|
|
5
|
+
* Fix graphs not being drawn the same way consistently
|
|
6
|
+
* Add support for sharing transitions across multiple events
|
|
7
|
+
* Add support for state-driven behavior
|
|
8
|
+
* Simplify initialize hooks, requiring super to be called instead
|
|
9
|
+
* Add :namespace option for generated state predicates / event methods
|
|
10
|
+
|
|
3
11
|
== 0.4.1 / 2008-12-16
|
|
4
12
|
|
|
5
13
|
* Fix nil states not being handled properly in guards, known states, or visualizations
|
data/README.rdoc
CHANGED
|
@@ -36,12 +36,14 @@ state machine is :)
|
|
|
36
36
|
Some brief, high-level features include:
|
|
37
37
|
* Defining state machines on any Ruby class
|
|
38
38
|
* Multiple state machines on a single class
|
|
39
|
+
* Namespaced state machines
|
|
39
40
|
* before/after transition hooks with explicit transition requirements
|
|
40
41
|
* ActiveRecord integration
|
|
41
42
|
* DataMapper integration
|
|
42
43
|
* Sequel integration
|
|
43
44
|
* States of any data type
|
|
44
45
|
* State predicates
|
|
46
|
+
* State-driven behavior
|
|
45
47
|
* GraphViz visualization creator
|
|
46
48
|
|
|
47
49
|
Examples of the usage patterns for some of the above features are shown below.
|
|
@@ -51,10 +53,14 @@ You can find more detailed documentation in the actual API.
|
|
|
51
53
|
|
|
52
54
|
=== Example
|
|
53
55
|
|
|
54
|
-
Below is an example of many of the features offered by this plugin, including
|
|
56
|
+
Below is an example of many of the features offered by this plugin, including:
|
|
55
57
|
* Initial states
|
|
58
|
+
* Namespaced states
|
|
56
59
|
* Transition callbacks
|
|
57
60
|
* Conditional transitions
|
|
61
|
+
* State-driven behavior
|
|
62
|
+
|
|
63
|
+
Class definition:
|
|
58
64
|
|
|
59
65
|
class Vehicle
|
|
60
66
|
attr_accessor :seatbelt_on
|
|
@@ -98,10 +104,39 @@ Below is an example of many of the features offered by this plugin, including
|
|
|
98
104
|
event :repair do
|
|
99
105
|
transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
|
|
100
106
|
end
|
|
107
|
+
|
|
108
|
+
state 'parked' do
|
|
109
|
+
def speed
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
state 'idling', 'first_gear' do
|
|
115
|
+
def speed
|
|
116
|
+
10
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
state 'second_gear' do
|
|
121
|
+
def speed
|
|
122
|
+
20
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
state_machine :hood_state, :initial => 'closed', :namespace => 'hood' do
|
|
128
|
+
event :open do
|
|
129
|
+
transition :to => 'opened', :from => 'closed'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
event :close do
|
|
133
|
+
transition :to => 'closed', :from => 'opened'
|
|
134
|
+
end
|
|
101
135
|
end
|
|
102
136
|
|
|
103
137
|
def initialize
|
|
104
138
|
@seatbelt_on = false
|
|
139
|
+
super() # NOTE: This *must* be called, otherwise states won't get initialized
|
|
105
140
|
end
|
|
106
141
|
|
|
107
142
|
def put_on_seatbelt
|
|
@@ -128,13 +163,20 @@ like so:
|
|
|
128
163
|
vehicle.parked? # => true
|
|
129
164
|
vehicle.can_ignite? # => true
|
|
130
165
|
vehicle.next_ignite_transition # => #<StateMachine::Transition:0xb7c34cec ...>
|
|
166
|
+
vehicle.speed # => 0
|
|
167
|
+
|
|
131
168
|
vehicle.ignite # => true
|
|
132
169
|
vehicle.parked? # => false
|
|
133
170
|
vehicle.idling? # => true
|
|
171
|
+
vehicle.speed # => 10
|
|
134
172
|
vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
|
|
173
|
+
|
|
135
174
|
vehicle.shift_up # => true
|
|
175
|
+
vehicle.speed # => 10
|
|
136
176
|
vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
|
|
177
|
+
|
|
137
178
|
vehicle.shift_up # => true
|
|
179
|
+
vehicle.speed # => 20
|
|
138
180
|
vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
|
|
139
181
|
|
|
140
182
|
# The bang (!) operator can raise exceptions if the event fails
|
|
@@ -143,6 +185,18 @@ like so:
|
|
|
143
185
|
# Generic state predicates can raise exceptions if the value does not exist
|
|
144
186
|
vehicle.state?('parked') # => true
|
|
145
187
|
vehicle.state?('invalid') # => ArgumentError: "parked" is not a known state value
|
|
188
|
+
|
|
189
|
+
# Namespaced machines have uniquely-generated methods
|
|
190
|
+
vehicle.can_open_hood? # => true
|
|
191
|
+
vehicle.open_hood # => true
|
|
192
|
+
vehicle.can_close_hood? # => true
|
|
193
|
+
|
|
194
|
+
vehicle.hood_opened? # => true
|
|
195
|
+
vehicle.hood_closed? # => false
|
|
196
|
+
|
|
197
|
+
*Note* the comment made on the +initialize+ method in the class. In order for
|
|
198
|
+
state machine attributes to be properly initialized, <tt>super()</tt> must be called.
|
|
199
|
+
See StateMachine::MacroMethods for more information about this.
|
|
146
200
|
|
|
147
201
|
== Integrations
|
|
148
202
|
|
data/Rakefile
CHANGED
|
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
|
|
|
5
5
|
|
|
6
6
|
spec = Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'state_machine'
|
|
8
|
-
s.version = '0.4.
|
|
8
|
+
s.version = '0.4.2'
|
|
9
9
|
s.platform = Gem::Platform::RUBY
|
|
10
10
|
s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
|
|
11
11
|
|
data/examples/AutoShop_state.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/examples/Vehicle_state.png
CHANGED
|
Binary file
|
data/lib/state_machine.rb
CHANGED
|
@@ -9,16 +9,17 @@ module StateMachine
|
|
|
9
9
|
# attribute, if not specified, is "state".
|
|
10
10
|
#
|
|
11
11
|
# Configuration options:
|
|
12
|
-
# * +initial+ - The initial value to set the attribute to. This can be a static value or a
|
|
12
|
+
# * +initial+ - The initial value to set the attribute to. This can be a static value or a lambda block which will be evaluated at runtime. Default is nil.
|
|
13
13
|
# * +action+ - The action to invoke when an object transitions. Default is nil unless otherwise specified by the configured integration.
|
|
14
14
|
# * +plural+ - The pluralized name of the attribute. By default, this will attempt to call +pluralize+ on the attribute, otherwise an "s" is appended.
|
|
15
|
-
# * +
|
|
15
|
+
# * +namespace+ - The name to use for namespacing all generated instance methods (e.g. "email" => "activate_email", "deactivate_email", etc.). Default is no namespace.
|
|
16
|
+
# * +integration+ - The name of the integration to use for adding library-specific behavior to the machine. Built-in integrations include :data_mapper, :active_record, and :sequel. By default, this is determined automatically.
|
|
16
17
|
#
|
|
17
18
|
# This also requires a block which will be used to actually configure the
|
|
18
|
-
# events and transitions for the state machine. *Note* that this
|
|
19
|
-
# will be executed within the context of the state machine. As a
|
|
20
|
-
# you will not be able to access any class methods unless you refer
|
|
21
|
-
# them directly (i.e. specifying the class name).
|
|
19
|
+
# states, events and transitions for the state machine. *Note* that this
|
|
20
|
+
# block will be executed within the context of the state machine. As a
|
|
21
|
+
# result, you will not be able to access any class methods unless you refer
|
|
22
|
+
# to them directly (i.e. specifying the class name).
|
|
22
23
|
#
|
|
23
24
|
# For examples on the types of configured state machines and blocks, see
|
|
24
25
|
# the section below.
|
|
@@ -95,6 +96,82 @@ module StateMachine
|
|
|
95
96
|
# end
|
|
96
97
|
# end
|
|
97
98
|
#
|
|
99
|
+
# == Attribute initialization
|
|
100
|
+
#
|
|
101
|
+
# For most classes, the initial values for state machine attributes are
|
|
102
|
+
# automatically assigned when a new object is created. However, this
|
|
103
|
+
# behavior will *not* work if the class defines an +initialize+ method
|
|
104
|
+
# without properly calling +super+.
|
|
105
|
+
#
|
|
106
|
+
# For example,
|
|
107
|
+
#
|
|
108
|
+
# class Vehicle
|
|
109
|
+
# state_machine :state, :initial => 'parked' do
|
|
110
|
+
# ...
|
|
111
|
+
# end
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# v = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
|
|
115
|
+
# v.state # => "parked"
|
|
116
|
+
#
|
|
117
|
+
# In the above example, no +initialize+ method is defined. As a result,
|
|
118
|
+
# the default behavior of initializing the state machine attributes is used.
|
|
119
|
+
#
|
|
120
|
+
# In the following example, a custom +initialize+ method is defined:
|
|
121
|
+
#
|
|
122
|
+
# class Vehicle
|
|
123
|
+
# state_machine :state, :initial => 'parked' do
|
|
124
|
+
# ...
|
|
125
|
+
# end
|
|
126
|
+
#
|
|
127
|
+
# def initialize
|
|
128
|
+
# end
|
|
129
|
+
# end
|
|
130
|
+
#
|
|
131
|
+
# v = Vehicle.new # => #<Vehicle:0xb7c77678>
|
|
132
|
+
# v.state # => nil
|
|
133
|
+
#
|
|
134
|
+
# Since the +initialize+ method is defined, the state machine attributes
|
|
135
|
+
# never get initialized. In order to ensure that all initialization hooks
|
|
136
|
+
# are called, the custom method *must* call +super+ without any arguments
|
|
137
|
+
# like so:
|
|
138
|
+
#
|
|
139
|
+
# class Vehicle
|
|
140
|
+
# state_machine :state, :initial => 'parked' do
|
|
141
|
+
# ...
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
144
|
+
# def initialize(attributes = {})
|
|
145
|
+
# ...
|
|
146
|
+
# super()
|
|
147
|
+
# end
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# v = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
|
151
|
+
# v.state # => "parked"
|
|
152
|
+
#
|
|
153
|
+
# Because of the way the inclusion of modules works in Ruby, calling <tt>super()</tt>
|
|
154
|
+
# will not only call the superclass's +initialize+, but also +initialize+ on
|
|
155
|
+
# all included modules. This allows the original state machine hook to get
|
|
156
|
+
# called properly.
|
|
157
|
+
#
|
|
158
|
+
# If you want to avoid calling the superclass's constructor, but still want
|
|
159
|
+
# to initialize the state machine attributes:
|
|
160
|
+
#
|
|
161
|
+
# class Vehicle
|
|
162
|
+
# state_machine :state, :initial => 'parked' do
|
|
163
|
+
# ...
|
|
164
|
+
# end
|
|
165
|
+
#
|
|
166
|
+
# def initialize(attributes = {})
|
|
167
|
+
# ...
|
|
168
|
+
# initialize_state_machines
|
|
169
|
+
# end
|
|
170
|
+
# end
|
|
171
|
+
#
|
|
172
|
+
# v = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
|
|
173
|
+
# v.state # => "parked"
|
|
174
|
+
#
|
|
98
175
|
# == States
|
|
99
176
|
#
|
|
100
177
|
# All of the valid states for the machine are automatically tracked based
|
|
@@ -103,8 +180,8 @@ module StateMachine
|
|
|
103
180
|
# explicitly added using the StateMachine::Machine#other_states
|
|
104
181
|
# helper.
|
|
105
182
|
#
|
|
106
|
-
#
|
|
107
|
-
# on the class. For example,
|
|
183
|
+
# When using String or Symbol-based states, a predicate method for that
|
|
184
|
+
# state is generated on the class. For example,
|
|
108
185
|
#
|
|
109
186
|
# class Vehicle
|
|
110
187
|
# state_machine :initial => 'parked' do
|
|
@@ -122,6 +199,11 @@ module StateMachine
|
|
|
122
199
|
# Each predicate method will return true if it matches the object's
|
|
123
200
|
# current state. Otherwise, it will return false.
|
|
124
201
|
#
|
|
202
|
+
# When a namespace is configured for a state machine, the name will be
|
|
203
|
+
# prepended to each state predicate like so:
|
|
204
|
+
# * <tt>car_parked?</tt>
|
|
205
|
+
# * <tt>car_idling?</tt>
|
|
206
|
+
#
|
|
125
207
|
# == Events and Transitions
|
|
126
208
|
#
|
|
127
209
|
# For more information about how to configure an event and its associated
|
|
@@ -130,10 +212,65 @@ module StateMachine
|
|
|
130
212
|
# == Defining callbacks
|
|
131
213
|
#
|
|
132
214
|
# Within the +state_machine+ block, you can also define callbacks for
|
|
133
|
-
#
|
|
215
|
+
# transitions. For more information about defining these callbacks,
|
|
134
216
|
# see StateMachine::Machine#before_transition and
|
|
135
217
|
# StateMachine::Machine#after_transition.
|
|
136
218
|
#
|
|
219
|
+
# == Namespaces
|
|
220
|
+
#
|
|
221
|
+
# When a namespace is configured for a state machine, the name provided will
|
|
222
|
+
# be used in generating the instance methods for interacting with
|
|
223
|
+
# events/states in the machine. This is particularly useful when a class
|
|
224
|
+
# has multiple state machines and it would be difficult to differentiate
|
|
225
|
+
# between the various states / events.
|
|
226
|
+
#
|
|
227
|
+
# For example,
|
|
228
|
+
#
|
|
229
|
+
# class Vehicle
|
|
230
|
+
# state_machine :heater_state, :initial => 'off' :namespace => 'heater' do
|
|
231
|
+
# event :turn_on do
|
|
232
|
+
# transition :to => 'on', :from => 'off'
|
|
233
|
+
# end
|
|
234
|
+
#
|
|
235
|
+
# event :turn_off do
|
|
236
|
+
# transition :to => 'off', :from => 'on'
|
|
237
|
+
# end
|
|
238
|
+
# end
|
|
239
|
+
#
|
|
240
|
+
# state_machine :hood_state, :initial => 'closed', :namespace => 'hood' do
|
|
241
|
+
# event :open do
|
|
242
|
+
# transition :to => 'opened', :from => 'closed'
|
|
243
|
+
# end
|
|
244
|
+
#
|
|
245
|
+
# event :close do
|
|
246
|
+
# transition :to => 'closed', :from => 'opened'
|
|
247
|
+
# end
|
|
248
|
+
# end
|
|
249
|
+
# end
|
|
250
|
+
#
|
|
251
|
+
# The above class defines two state machines: +heater_state+ and +hood_state+.
|
|
252
|
+
# For the +heater_state+ machine, the following methods are generated since
|
|
253
|
+
# it's namespaced by "heater":
|
|
254
|
+
# * <tt>can_turn_on_heater?</tt>
|
|
255
|
+
# * <tt>turn_on_heater</tt>
|
|
256
|
+
# * ...
|
|
257
|
+
# * <tt>can_turn_off_heater?</tt>
|
|
258
|
+
# * <tt>turn_off_heater</tt>
|
|
259
|
+
# * ..
|
|
260
|
+
# * <tt>heater_off?</tt>
|
|
261
|
+
# * <tt>heater_on?</tt>
|
|
262
|
+
#
|
|
263
|
+
# As shown, each method is unique to the state machine so that the states
|
|
264
|
+
# and events don't conflict. The same goes for the +hood_state+ machine:
|
|
265
|
+
# * <tt>can_open_hood?</tt>
|
|
266
|
+
# * <tt>open_hood</tt>
|
|
267
|
+
# * ...
|
|
268
|
+
# * <tt>can_close_hood?</tt>
|
|
269
|
+
# * <tt>close_hood</tt>
|
|
270
|
+
# * ..
|
|
271
|
+
# * <tt>hood_open?</tt>
|
|
272
|
+
# * <tt>hood_closed?</tt>
|
|
273
|
+
#
|
|
137
274
|
# == Scopes
|
|
138
275
|
#
|
|
139
276
|
# For integrations that support it, a group of default scope filters will
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module StateMachine
|
|
2
|
-
# Provides a set of helper methods for making assertions about content of
|
|
2
|
+
# Provides a set of helper methods for making assertions about the content of
|
|
3
3
|
# various objects
|
|
4
4
|
module Assertions
|
|
5
5
|
# Validates that all keys in the given hash *only* includes the specified
|
|
@@ -58,8 +58,8 @@ module StateMachine
|
|
|
58
58
|
# An optional block for determining whether to cancel the callback chain
|
|
59
59
|
# based on the return value of the callback. By default, the callback
|
|
60
60
|
# chain never cancels based on the return value (i.e. there is no implicit
|
|
61
|
-
# terminator). Certain integrations, such as ActiveRecord
|
|
62
|
-
# default value.
|
|
61
|
+
# terminator). Certain integrations, such as ActiveRecord and Sequel,
|
|
62
|
+
# change this default value.
|
|
63
63
|
#
|
|
64
64
|
# == Examples
|
|
65
65
|
#
|
|
@@ -148,6 +148,7 @@ module StateMachine
|
|
|
148
148
|
# when the callback is invoked
|
|
149
149
|
def bound_method(block)
|
|
150
150
|
# Generate a thread-safe unbound method that can be used on any object
|
|
151
|
+
# This is essentially a workaround for not having Ruby 1.9's instance_exec
|
|
151
152
|
unbound_method = Object.class_eval do
|
|
152
153
|
time = Time.now
|
|
153
154
|
method_name = "__bind_#{time.to_i}_#{time.usec}"
|
data/lib/state_machine/event.rb
CHANGED
|
@@ -44,7 +44,7 @@ module StateMachine
|
|
|
44
44
|
# Creates a new transition that will be evaluated when the event is fired.
|
|
45
45
|
#
|
|
46
46
|
# Configuration options:
|
|
47
|
-
# * +to+ - The state that being transitioned to. If not specified, then the transition will not change the state.
|
|
47
|
+
# * +to+ - The state that's being transitioned to. If not specified, then the transition will not change the state.
|
|
48
48
|
# * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state.
|
|
49
49
|
# * +except_from+ - A state or array of states that *cannot* be transitioned from.
|
|
50
50
|
# * +if+ - Specifies a method, proc or string to call to determine if the transition should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
|
|
@@ -119,32 +119,43 @@ module StateMachine
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# Draws a representation of this event on the given graph. This will
|
|
123
|
+
# create 1 or more edges on the graph for each guard (i.e. transition)
|
|
124
|
+
# configured.
|
|
125
|
+
#
|
|
126
|
+
# A collection of the generated edges will be returned.
|
|
127
|
+
def draw(graph)
|
|
128
|
+
valid_states = machine.states_order
|
|
129
|
+
guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
|
|
130
|
+
end
|
|
131
|
+
|
|
122
132
|
protected
|
|
123
133
|
# Add the various instance methods that can transition the object using
|
|
124
134
|
# the current event
|
|
125
135
|
def add_actions
|
|
126
136
|
attribute = machine.attribute
|
|
127
|
-
name = self.name
|
|
137
|
+
qualified_name = name = self.name
|
|
138
|
+
qualified_name = "#{name}_#{machine.namespace}" if machine.namespace
|
|
128
139
|
|
|
129
140
|
machine.owner_class.class_eval do
|
|
130
141
|
# Checks whether the event can be fired on the current object
|
|
131
|
-
define_method("can_#{
|
|
132
|
-
self.class.state_machines[attribute].
|
|
142
|
+
define_method("can_#{qualified_name}?") do
|
|
143
|
+
self.class.state_machines[attribute].event(name).can_fire?(self)
|
|
133
144
|
end
|
|
134
145
|
|
|
135
146
|
# Gets the next transition that would be performed if the event were to be fired now
|
|
136
|
-
define_method("next_#{
|
|
137
|
-
self.class.state_machines[attribute].
|
|
147
|
+
define_method("next_#{qualified_name}_transition") do
|
|
148
|
+
self.class.state_machines[attribute].event(name).next_transition(self)
|
|
138
149
|
end
|
|
139
150
|
|
|
140
151
|
# Fires the event
|
|
141
|
-
define_method(
|
|
142
|
-
self.class.state_machines[attribute].
|
|
152
|
+
define_method(qualified_name) do |*args|
|
|
153
|
+
self.class.state_machines[attribute].event(name).fire(self, *args)
|
|
143
154
|
end
|
|
144
155
|
|
|
145
156
|
# Fires the event, raising an exception if it fails to transition
|
|
146
|
-
define_method("#{
|
|
147
|
-
send(
|
|
157
|
+
define_method("#{qualified_name}!") do |*args|
|
|
158
|
+
send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{attribute} via :#{name} from #{send(attribute).inspect}")
|
|
148
159
|
end
|
|
149
160
|
end
|
|
150
161
|
end
|
|
@@ -3,46 +3,6 @@ module StateMachine
|
|
|
3
3
|
def self.extended(base) #:nodoc:
|
|
4
4
|
base.class_eval do
|
|
5
5
|
@state_machines = {}
|
|
6
|
-
|
|
7
|
-
# method_added may get defined by the class, so instead it's chained
|
|
8
|
-
class << self
|
|
9
|
-
alias_method :method_added_without_state_machine, :method_added
|
|
10
|
-
alias_method :method_added, :method_added_with_state_machine
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Ensures that the +initialize+ hook defined in StateMachine::InstanceMethods
|
|
16
|
-
# remains there even if the class defines its own +initialize+ method
|
|
17
|
-
# *after* the state machine has been defined. For example,
|
|
18
|
-
#
|
|
19
|
-
# class Switch
|
|
20
|
-
# state_machine do
|
|
21
|
-
# ...
|
|
22
|
-
# end
|
|
23
|
-
#
|
|
24
|
-
# def initialize(attributes = {})
|
|
25
|
-
# ...
|
|
26
|
-
# end
|
|
27
|
-
# end
|
|
28
|
-
def method_added_with_state_machine(method) #:nodoc:
|
|
29
|
-
method_added_without_state_machine(method)
|
|
30
|
-
|
|
31
|
-
# Aliasing the +initialize+ method also invokes +method_added+, so
|
|
32
|
-
# alias processing is tracked to prevent an infinite loop
|
|
33
|
-
if !@skip_initialize_hook && [:initialize, :initialize_with_state_machine].include?(method)
|
|
34
|
-
@skip_initialize_hook = true
|
|
35
|
-
|
|
36
|
-
# Re-defining +initialize+ instead of alias chaining is done in order to
|
|
37
|
-
# prevent +initialize+ from showing up in #instance_methods
|
|
38
|
-
alias_method :initialize_without_state_machine, :initialize
|
|
39
|
-
class_eval <<-end_eval, __FILE__, __LINE__
|
|
40
|
-
def initialize(*args, &block)
|
|
41
|
-
initialize_with_state_machine(*args, &block)
|
|
42
|
-
end
|
|
43
|
-
end_eval
|
|
44
|
-
|
|
45
|
-
@skip_initialize_hook = false
|
|
46
6
|
end
|
|
47
7
|
end
|
|
48
8
|
|
|
@@ -51,7 +11,7 @@ module StateMachine
|
|
|
51
11
|
# is available to each subclass, each subclass having a copy of its
|
|
52
12
|
# superclass's attribute.
|
|
53
13
|
#
|
|
54
|
-
# The hash of state machines maps +
|
|
14
|
+
# The hash of state machines maps +attribute+ => +machine+, e.g.
|
|
55
15
|
#
|
|
56
16
|
# Vehicle.state_machines # => {"state" => #<StateMachine::Machine:0xb6f6e4a4 ...>
|
|
57
17
|
def state_machines
|
|
@@ -60,25 +20,23 @@ module StateMachine
|
|
|
60
20
|
end
|
|
61
21
|
|
|
62
22
|
module InstanceMethods
|
|
63
|
-
def self.included(base) #:nodoc:
|
|
64
|
-
# Methods added from an included module don't invoke +method_added+,
|
|
65
|
-
# triggering the initialize alias, so it's done explicitly
|
|
66
|
-
base.method_added(:initialize_with_state_machine)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
23
|
# Defines the initial values for state machine attributes. The values
|
|
70
24
|
# will be set *after* the original initialize method is invoked. This is
|
|
71
25
|
# necessary in order to ensure that the object is initialized before
|
|
72
26
|
# dynamic initial attributes are evaluated.
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
self.class.state_machines.each do |attribute, machine|
|
|
77
|
-
# Set the initial value of the machine's attribute unless it already
|
|
78
|
-
# exists (which must mean the defaults are being skipped)
|
|
79
|
-
value = send(attribute)
|
|
80
|
-
send("#{attribute}=", machine.initial_state(self)) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
|
81
|
-
end
|
|
27
|
+
def initialize(*args, &block)
|
|
28
|
+
super
|
|
29
|
+
initialize_state_machines
|
|
82
30
|
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
def initialize_state_machines #:nodoc:
|
|
34
|
+
self.class.state_machines.each do |attribute, machine|
|
|
35
|
+
# Set the initial value of the machine's attribute unless it already
|
|
36
|
+
# exists (which must mean the defaults are being skipped)
|
|
37
|
+
value = send(attribute)
|
|
38
|
+
send("#{attribute}=", machine.initial_state(self)) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
83
41
|
end
|
|
84
42
|
end
|