verborghs-state_machine 0.9.4
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 +360 -0
- data/LICENSE +20 -0
- data/README.rdoc +635 -0
- data/Rakefile +77 -0
- 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/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- 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/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +241 -0
- data/lib/state_machine/condition_proxy.rb +106 -0
- data/lib/state_machine/eval_helpers.rb +83 -0
- data/lib/state_machine/event.rb +267 -0
- data/lib/state_machine/event_collection.rb +122 -0
- data/lib/state_machine/extensions.rb +149 -0
- data/lib/state_machine/guard.rb +230 -0
- data/lib/state_machine/initializers/merb.rb +1 -0
- data/lib/state_machine/initializers/rails.rb +5 -0
- data/lib/state_machine/initializers.rb +4 -0
- data/lib/state_machine/integrations/active_model/locale.rb +11 -0
- data/lib/state_machine/integrations/active_model/observer.rb +45 -0
- data/lib/state_machine/integrations/active_model.rb +445 -0
- data/lib/state_machine/integrations/active_record/locale.rb +20 -0
- data/lib/state_machine/integrations/active_record.rb +522 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +175 -0
- data/lib/state_machine/integrations/data_mapper.rb +379 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +309 -0
- data/lib/state_machine/integrations/sequel.rb +356 -0
- data/lib/state_machine/integrations.rb +83 -0
- data/lib/state_machine/machine.rb +1645 -0
- data/lib/state_machine/machine_collection.rb +64 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +260 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +399 -0
- data/lib/state_machine/transition_collection.rb +244 -0
- data/lib/state_machine.rb +421 -0
- data/lib/tasks/state_machine.rake +1 -0
- data/lib/tasks/state_machine.rb +27 -0
- data/test/files/en.yml +9 -0
- data/test/files/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +980 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +728 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +222 -0
- data/test/unit/event_collection_test.rb +324 -0
- data/test/unit/event_test.rb +795 -0
- data/test/unit/guard_test.rb +909 -0
- data/test/unit/integrations/active_model_test.rb +956 -0
- data/test/unit/integrations/active_record_test.rb +1918 -0
- data/test/unit/integrations/data_mapper_test.rb +1814 -0
- data/test/unit/integrations/mongo_mapper_test.rb +1382 -0
- data/test/unit/integrations/sequel_test.rb +1492 -0
- data/test/unit/integrations_test.rb +50 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +565 -0
- data/test/unit/machine_test.rb +2349 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +848 -0
- data/test/unit/transition_collection_test.rb +2098 -0
- data/test/unit/transition_test.rb +1384 -0
- metadata +176 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# Represents a collection of transitions in a state machine
|
3
|
+
class TransitionCollection < Array
|
4
|
+
include Assertions
|
5
|
+
|
6
|
+
# Whether to skip running the action for each transition's machine
|
7
|
+
attr_reader :skip_actions
|
8
|
+
|
9
|
+
# Whether to skip running the after callbacks
|
10
|
+
attr_reader :skip_after
|
11
|
+
|
12
|
+
# Whether transitions should wrapped around a transaction block
|
13
|
+
attr_reader :use_transaction
|
14
|
+
|
15
|
+
# Creates a new collection of transitions that can be run in parallel. Each
|
16
|
+
# transition *must* be for a different attribute.
|
17
|
+
#
|
18
|
+
# Configuration options:
|
19
|
+
# * <tt>:actions</tt> - Whether to run the action configured for each transition
|
20
|
+
# * <tt>:after</tt> - Whether to run after callbacks
|
21
|
+
# * <tt>:transaction</tt> - Whether to wrap transitions within a transaction
|
22
|
+
def initialize(transitions = [], options = {})
|
23
|
+
super(transitions)
|
24
|
+
|
25
|
+
# Determine the validity of the transitions as a whole
|
26
|
+
@valid = all?
|
27
|
+
reject! {|transition| !transition}
|
28
|
+
|
29
|
+
attributes = map {|transition| transition.attribute}.uniq
|
30
|
+
raise ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
|
31
|
+
|
32
|
+
assert_valid_keys(options, :actions, :after, :transaction)
|
33
|
+
options = {:actions => true, :after => true, :transaction => true}.merge(options)
|
34
|
+
@skip_actions = !options[:actions]
|
35
|
+
@skip_after = !options[:after]
|
36
|
+
@use_transaction = options[:transaction]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Runs each of the collection's transitions in parallel.
|
40
|
+
#
|
41
|
+
# All transitions will run through the following steps:
|
42
|
+
# 1. Before callbacks
|
43
|
+
# 2. Persist state
|
44
|
+
# 3. Invoke action
|
45
|
+
# 4. After callbacks (if configured)
|
46
|
+
# 5. Rollback (if action is unsuccessful)
|
47
|
+
#
|
48
|
+
# If a block is passed to this method, that block will be called instead
|
49
|
+
# of invoking each transition's action.
|
50
|
+
def perform(&block)
|
51
|
+
reset
|
52
|
+
|
53
|
+
if valid?
|
54
|
+
if use_event_attributes? && !block_given?
|
55
|
+
each do |transition|
|
56
|
+
transition.transient = true
|
57
|
+
transition.machine.write(object, :event_transition, transition)
|
58
|
+
end
|
59
|
+
|
60
|
+
run_actions
|
61
|
+
else
|
62
|
+
within_transaction do
|
63
|
+
catch(:halt) { run_callbacks(&block) }
|
64
|
+
rollback unless success?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
if actions.length == 1 && results.include?(actions.first)
|
70
|
+
results[actions.first]
|
71
|
+
else
|
72
|
+
success?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
attr_reader :results #:nodoc:
|
78
|
+
|
79
|
+
# Is this a valid set of transitions? If the collection was creating with
|
80
|
+
# any +false+ values for transitions, then the the collection will be
|
81
|
+
# marked as invalid.
|
82
|
+
def valid?
|
83
|
+
@valid
|
84
|
+
end
|
85
|
+
|
86
|
+
# Did each transition perform successfully? This will only be true if the
|
87
|
+
# following requirements are met:
|
88
|
+
# * No +before+ callbacks halt
|
89
|
+
# * All actions run successfully (always true if skipping actions)
|
90
|
+
def success?
|
91
|
+
@success
|
92
|
+
end
|
93
|
+
|
94
|
+
# Gets the object being transitioned
|
95
|
+
def object
|
96
|
+
first.object
|
97
|
+
end
|
98
|
+
|
99
|
+
# Gets the list of actions to run. If configured to skip actions, then
|
100
|
+
# this will return an empty collection.
|
101
|
+
def actions
|
102
|
+
empty? ? [nil] : map {|transition| transition.action}.uniq
|
103
|
+
end
|
104
|
+
|
105
|
+
# Determines whether an event attribute be used to trigger the transitions
|
106
|
+
# in this collection or whether the transitions be run directly *outside*
|
107
|
+
# of the action.
|
108
|
+
def use_event_attributes?
|
109
|
+
!skip_actions && !skip_after && actions.all? && actions.length == 1 && first.machine.action_helper_defined?
|
110
|
+
end
|
111
|
+
|
112
|
+
# Resets any information tracked from previous attempts to perform the
|
113
|
+
# collection
|
114
|
+
def reset
|
115
|
+
@results = {}
|
116
|
+
@success = false
|
117
|
+
end
|
118
|
+
|
119
|
+
# Runs each transition's callbacks recursively. Once all before callbacks
|
120
|
+
# have been executed, the transitions will then be persisted and the
|
121
|
+
# configured actions will be run.
|
122
|
+
#
|
123
|
+
# If any transition fails to run its callbacks, :halt will be thrown.
|
124
|
+
def run_callbacks(index = 0, &block)
|
125
|
+
if transition = self[index]
|
126
|
+
throw :halt unless transition.run_callbacks(:after => !skip_after) do
|
127
|
+
run_callbacks(index + 1, &block)
|
128
|
+
{:result => results[transition.action], :success => success?}
|
129
|
+
end
|
130
|
+
else
|
131
|
+
persist
|
132
|
+
run_actions(&block)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Transitions the current value of the object's states to those specified by
|
137
|
+
# each transition
|
138
|
+
def persist
|
139
|
+
each {|transition| transition.persist}
|
140
|
+
end
|
141
|
+
|
142
|
+
# Runs the actions for each transition. If a block is given method, then it
|
143
|
+
# will be called instead of invoking each transition's action.
|
144
|
+
#
|
145
|
+
# The results of the actions will be used to determine #success?.
|
146
|
+
def run_actions
|
147
|
+
catch_exceptions do
|
148
|
+
@success = if block_given?
|
149
|
+
result = yield
|
150
|
+
actions.each {|action| results[action] = result}
|
151
|
+
!!result
|
152
|
+
else
|
153
|
+
actions.compact.each {|action| !skip_actions && results[action] = object.send(action)}
|
154
|
+
results.values.all?
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Rolls back changes made to the object's states via each transition
|
160
|
+
def rollback
|
161
|
+
each {|transition| transition.rollback}
|
162
|
+
end
|
163
|
+
|
164
|
+
# Wraps the given block with a rescue handler so that any exceptions that
|
165
|
+
# occur will automatically result in the transition rolling back any changes
|
166
|
+
# that were made to the object involved.
|
167
|
+
def catch_exceptions
|
168
|
+
begin
|
169
|
+
yield
|
170
|
+
rescue Exception
|
171
|
+
rollback
|
172
|
+
raise
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Runs a block within a transaction for the object being transitioned. If
|
177
|
+
# transactions are disabled, then this is a no-op.
|
178
|
+
def within_transaction
|
179
|
+
if use_transaction && !empty?
|
180
|
+
first.within_transaction do
|
181
|
+
yield
|
182
|
+
success?
|
183
|
+
end
|
184
|
+
else
|
185
|
+
yield
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Represents a collection of transitions that were generated from attribute-
|
191
|
+
# based events
|
192
|
+
class AttributeTransitionCollection < TransitionCollection
|
193
|
+
def initialize(transitions = [], options = {}) #:nodoc:
|
194
|
+
super(transitions, {:transaction => false, :actions => false}.merge(options))
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
# Hooks into running transition callbacks so that event / event transition
|
199
|
+
# attributes can be properly updated
|
200
|
+
def run_callbacks(index = 0)
|
201
|
+
if index == 0
|
202
|
+
# Clears any traces of the event attribute to prevent it from being
|
203
|
+
# evaluated multiple times if actions are nested
|
204
|
+
each do |transition|
|
205
|
+
transition.machine.write(object, :event, nil)
|
206
|
+
transition.machine.write(object, :event_transition, nil)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Rollback only if exceptions occur during before callbacks
|
210
|
+
begin
|
211
|
+
super
|
212
|
+
rescue Exception
|
213
|
+
rollback unless @before_run
|
214
|
+
raise
|
215
|
+
end
|
216
|
+
|
217
|
+
# Persists transitions on the object if partial transition was successful.
|
218
|
+
# This allows us to reference them later to complete the transition with
|
219
|
+
# after callbacks.
|
220
|
+
each {|transition| transition.machine.write(object, :event_transition, transition)} if skip_after && success?
|
221
|
+
else
|
222
|
+
super
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Tracks that before callbacks have now completed
|
227
|
+
def persist
|
228
|
+
@before_run = true
|
229
|
+
super
|
230
|
+
end
|
231
|
+
|
232
|
+
# Resets callback tracking
|
233
|
+
def reset
|
234
|
+
super
|
235
|
+
@before_run = false
|
236
|
+
end
|
237
|
+
|
238
|
+
# Resets the event attribute so it can be re-evaluated if attempted again
|
239
|
+
def rollback
|
240
|
+
super
|
241
|
+
each {|transition| transition.machine.write(object, :event, transition.event) unless transition.transient?}
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
@@ -0,0 +1,421 @@
|
|
1
|
+
require 'state_machine/machine'
|
2
|
+
|
3
|
+
# A state machine is a model of behavior composed of states, events, and
|
4
|
+
# transitions. This helper adds support for defining this type of
|
5
|
+
# functionality on any Ruby class.
|
6
|
+
module StateMachine
|
7
|
+
module MacroMethods
|
8
|
+
# Creates a new state machine with the given name. The default name, if not
|
9
|
+
# specified, is <tt>:state</tt>.
|
10
|
+
#
|
11
|
+
# Configuration options:
|
12
|
+
# * <tt>:attribute</tt> - The name of the attribute to store the state value
|
13
|
+
# in. By default, this is the same as the name of the machine.
|
14
|
+
# * <tt>:initial</tt> - The initial state of the attribute. This can be a
|
15
|
+
# static state or a lambda block which will be evaluated at runtime
|
16
|
+
# (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
|
17
|
+
# Default is nil.
|
18
|
+
# * <tt>:action</tt> - The instance method to invoke when an object
|
19
|
+
# transitions. Default is nil unless otherwise specified by the
|
20
|
+
# configured integration.
|
21
|
+
# * <tt>:namespace</tt> - The name to use for namespacing all generated
|
22
|
+
# state / event instance methods (e.g. "heater" would generate
|
23
|
+
# :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
|
24
|
+
# Default is nil.
|
25
|
+
# * <tt>:integration</tt> - The name of the integration to use for adding
|
26
|
+
# library-specific behavior to the machine. Built-in integrations
|
27
|
+
# include :active_model, :active_record, :data_mapper, :mongo_mapper, and
|
28
|
+
# :sequel. By default, this is determined automatically.
|
29
|
+
#
|
30
|
+
# Configuration options relevant to ORM integrations:
|
31
|
+
# * <tt>:plural</tt> - The pluralized name of the attribute. By default,
|
32
|
+
# this will attempt to call +pluralize+ on the attribute. If this
|
33
|
+
# method is not available, an "s" is appended. This is used for
|
34
|
+
# generating scopes.
|
35
|
+
# * <tt>:messages</tt> - The error messages to use when invalidating
|
36
|
+
# objects due to failed transitions. Messages include:
|
37
|
+
# * <tt>:invalid</tt>
|
38
|
+
# * <tt>:invalid_event</tt>
|
39
|
+
# * <tt>:invalid_transition</tt>
|
40
|
+
# * <tt>:use_transactions</tt> - Whether transactions should be used when
|
41
|
+
# firing events. Default is true unless otherwise specified by the
|
42
|
+
# configured integration.
|
43
|
+
#
|
44
|
+
# This also expects a block which will be used to actually configure the
|
45
|
+
# states, events and transitions for the state machine. *Note* that this
|
46
|
+
# block will be executed within the context of the state machine. As a
|
47
|
+
# result, you will not be able to access any class methods unless you refer
|
48
|
+
# to them directly (i.e. specifying the class name).
|
49
|
+
#
|
50
|
+
# For examples on the types of state machine configurations and blocks, see
|
51
|
+
# the section below.
|
52
|
+
#
|
53
|
+
# == Examples
|
54
|
+
#
|
55
|
+
# With the default name/attribute and no configuration:
|
56
|
+
#
|
57
|
+
# class Vehicle
|
58
|
+
# state_machine do
|
59
|
+
# event :park do
|
60
|
+
# ...
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# The above example will define a state machine named "state" that will
|
66
|
+
# store the value in the +state+ attribute. Every vehicle will start
|
67
|
+
# without an initial state.
|
68
|
+
#
|
69
|
+
# With a custom name / attribute:
|
70
|
+
#
|
71
|
+
# class Vehicle
|
72
|
+
# state_machine :status, :attribute => :status_value do
|
73
|
+
# ...
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# With a static initial state:
|
78
|
+
#
|
79
|
+
# class Vehicle
|
80
|
+
# state_machine :status, :initial => :parked do
|
81
|
+
# ...
|
82
|
+
# end
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# With a dynamic initial state:
|
86
|
+
#
|
87
|
+
# class Vehicle
|
88
|
+
# state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
|
89
|
+
# ...
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# == Class Methods
|
94
|
+
#
|
95
|
+
# The following class methods will be automatically generated by the
|
96
|
+
# state machine based on the *name* of the machine. Any existing methods
|
97
|
+
# will not be overwritten.
|
98
|
+
# * <tt>human_state_name(state)</tt> - Gets the humanized value for the
|
99
|
+
# given state. This may be generated by internationalization libraries if
|
100
|
+
# supported by the integration.
|
101
|
+
# * <tt>human_state_event_name(event)</tt> - Gets the humanized value for
|
102
|
+
# the given event. This may be generated by internationalization
|
103
|
+
# libraries if supported by the integration.
|
104
|
+
#
|
105
|
+
# For example,
|
106
|
+
#
|
107
|
+
# class Vehicle
|
108
|
+
# state_machine :state, :initial => :parked do
|
109
|
+
# event :ignite do
|
110
|
+
# transition :parked => :idling
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# event :shift_up do
|
114
|
+
# transition :idling => :first_gear
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# Vehicle.human_state_name(:parked) # => "parked"
|
120
|
+
# Vehicle.human_state_name(:first_gear) # => "first gear"
|
121
|
+
# Vehicle.human_state_event_name(:park) # => "park"
|
122
|
+
# Vehicle.human_state_event_name(:shift_up) # => "shift up"
|
123
|
+
#
|
124
|
+
# == Instance Methods
|
125
|
+
#
|
126
|
+
# The following instance methods will be automatically generated by the
|
127
|
+
# state machine based on the *name* of the machine. Any existing methods
|
128
|
+
# will not be overwritten.
|
129
|
+
# * <tt>state</tt> - Gets the current value for the attribute
|
130
|
+
# * <tt>state=(value)</tt> - Sets the current value for the attribute
|
131
|
+
# * <tt>state?(name)</tt> - Checks the given state name against the current
|
132
|
+
# state. If the name is not a known state, then an ArgumentError is raised.
|
133
|
+
# * <tt>state_name</tt> - Gets the name of the state for the current value
|
134
|
+
# * <tt>human_state_name</tt> - Gets the human-readable name of the state
|
135
|
+
# for the current value
|
136
|
+
# * <tt>state_events</tt> - Gets the list of events that can be fired on
|
137
|
+
# the current object's state (uses the *unqualified* event names)
|
138
|
+
# * <tt>state_transitions(requirements = {})</tt> - Gets the list of possible
|
139
|
+
# transitions that can be made on the current object's state. Additional
|
140
|
+
# requirements, such as the :from / :to state and :on event can be specified
|
141
|
+
# to restrict the transitions to select. By default, the current state
|
142
|
+
# will be used for the :from state.
|
143
|
+
#
|
144
|
+
# For example,
|
145
|
+
#
|
146
|
+
# class Vehicle
|
147
|
+
# state_machine :state, :initial => :parked do
|
148
|
+
# event :ignite do
|
149
|
+
# transition :parked => :idling
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# event :park do
|
153
|
+
# transition :idling => :parked
|
154
|
+
# end
|
155
|
+
# end
|
156
|
+
# end
|
157
|
+
#
|
158
|
+
# vehicle = Vehicle.new
|
159
|
+
# vehicle.state # => "parked"
|
160
|
+
# vehicle.state_name # => :parked
|
161
|
+
# vehicle.human_state_name # => "parked"
|
162
|
+
# vehicle.state?(:parked) # => true
|
163
|
+
#
|
164
|
+
# # Changing state
|
165
|
+
# vehicle.state = 'idling'
|
166
|
+
# vehicle.state # => "idling"
|
167
|
+
# vehicle.state_name # => :idling
|
168
|
+
# vehicle.state?(:parked) # => false
|
169
|
+
#
|
170
|
+
# # Getting current event / transition availability
|
171
|
+
# vehicle.state_events # => [:park]
|
172
|
+
# vehicle.park # => true
|
173
|
+
# vehicle.state_events # => [:ignite]
|
174
|
+
#
|
175
|
+
# vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
176
|
+
# vehicle.ignite
|
177
|
+
# vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
|
178
|
+
#
|
179
|
+
# == Attribute initialization
|
180
|
+
#
|
181
|
+
# For most classes, the initial values for state machine attributes are
|
182
|
+
# automatically assigned when a new object is created. However, this
|
183
|
+
# behavior will *not* work if the class defines an +initialize+ method
|
184
|
+
# without properly calling +super+.
|
185
|
+
#
|
186
|
+
# For example,
|
187
|
+
#
|
188
|
+
# class Vehicle
|
189
|
+
# state_machine :state, :initial => :parked do
|
190
|
+
# ...
|
191
|
+
# end
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
|
195
|
+
# vehicle.state # => "parked"
|
196
|
+
#
|
197
|
+
# In the above example, no +initialize+ method is defined. As a result,
|
198
|
+
# the default behavior of initializing the state machine attributes is used.
|
199
|
+
#
|
200
|
+
# In the following example, a custom +initialize+ method is defined:
|
201
|
+
#
|
202
|
+
# class Vehicle
|
203
|
+
# state_machine :state, :initial => :parked do
|
204
|
+
# ...
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# def initialize
|
208
|
+
# end
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
|
212
|
+
# vehicle.state # => nil
|
213
|
+
#
|
214
|
+
# Since the +initialize+ method is defined, the state machine attributes
|
215
|
+
# never get initialized. In order to ensure that all initialization hooks
|
216
|
+
# are called, the custom method *must* call +super+ without any arguments
|
217
|
+
# like so:
|
218
|
+
#
|
219
|
+
# class Vehicle
|
220
|
+
# state_machine :state, :initial => :parked do
|
221
|
+
# ...
|
222
|
+
# end
|
223
|
+
#
|
224
|
+
# def initialize(attributes = {})
|
225
|
+
# ...
|
226
|
+
# super()
|
227
|
+
# end
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
|
231
|
+
# vehicle.state # => "parked"
|
232
|
+
#
|
233
|
+
# Because of the way the inclusion of modules works in Ruby, calling
|
234
|
+
# <tt>super()</tt> will not only call the superclass's +initialize+, but
|
235
|
+
# also +initialize+ on all included modules. This allows the original state
|
236
|
+
# machine hook to get called properly.
|
237
|
+
#
|
238
|
+
# If you want to avoid calling the superclass's constructor, but still want
|
239
|
+
# to initialize the state machine attributes:
|
240
|
+
#
|
241
|
+
# class Vehicle
|
242
|
+
# state_machine :state, :initial => :parked do
|
243
|
+
# ...
|
244
|
+
# end
|
245
|
+
#
|
246
|
+
# def initialize(attributes = {})
|
247
|
+
# ...
|
248
|
+
# initialize_state_machines
|
249
|
+
# end
|
250
|
+
# end
|
251
|
+
#
|
252
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
|
253
|
+
# vehicle.state # => "parked"
|
254
|
+
#
|
255
|
+
# == States
|
256
|
+
#
|
257
|
+
# All of the valid states for the machine are automatically tracked based
|
258
|
+
# on the events, transitions, and callbacks defined for the machine. If
|
259
|
+
# there are additional states that are never referenced, these should be
|
260
|
+
# explicitly added using the StateMachine::Machine#state or
|
261
|
+
# StateMachine::Machine#other_states helpers.
|
262
|
+
#
|
263
|
+
# When a new state is defined, a predicate method for that state is
|
264
|
+
# generated on the class. For example,
|
265
|
+
#
|
266
|
+
# class Vehicle
|
267
|
+
# state_machine :initial => :parked do
|
268
|
+
# event :ignite do
|
269
|
+
# transition all => :idling
|
270
|
+
# end
|
271
|
+
# end
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# ...will generate the following instance methods (assuming they're not
|
275
|
+
# already defined in the class):
|
276
|
+
# * <tt>parked?</tt>
|
277
|
+
# * <tt>idling?</tt>
|
278
|
+
#
|
279
|
+
# Each predicate method will return true if it matches the object's
|
280
|
+
# current state. Otherwise, it will return false.
|
281
|
+
#
|
282
|
+
# == Events and Transitions
|
283
|
+
#
|
284
|
+
# Events defined on the machine are the interface to transitioning states
|
285
|
+
# for an object. Events can be fired either directly (through the method
|
286
|
+
# generated for the event) or indirectly (through attributes defined on
|
287
|
+
# the machine).
|
288
|
+
#
|
289
|
+
# For example,
|
290
|
+
#
|
291
|
+
# class Vehicle
|
292
|
+
# include DataMapper::Resource
|
293
|
+
# property :id, Serial
|
294
|
+
#
|
295
|
+
# state_machine :initial => :parked do
|
296
|
+
# event :ignite do
|
297
|
+
# transition :parked => :idling
|
298
|
+
# end
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# state_machine :alarm_state, :initial => :active do
|
302
|
+
# event :disable do
|
303
|
+
# transition all => :off
|
304
|
+
# end
|
305
|
+
# end
|
306
|
+
# end
|
307
|
+
#
|
308
|
+
# # Fire +ignite+ event directly
|
309
|
+
# vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
|
310
|
+
# vehicle.ignite # => true
|
311
|
+
# vehicle.state # => "idling"
|
312
|
+
# vehicle.alarm_state # => "active"
|
313
|
+
#
|
314
|
+
# # Fire +disable+ event automatically
|
315
|
+
# vehicle.alarm_state_event = 'disable'
|
316
|
+
# vehicle.save # => true
|
317
|
+
# vehicle.alarm_state # => "off"
|
318
|
+
#
|
319
|
+
# In the above example, the +state+ attribute is transitioned using the
|
320
|
+
# +ignite+ action that's generated from the state machine. On the other
|
321
|
+
# hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
|
322
|
+
# attribute that automatically gets fired when the machine's action (+save+)
|
323
|
+
# is invoked.
|
324
|
+
#
|
325
|
+
# For more information about how to configure an event and its associated
|
326
|
+
# transitions, see StateMachine::Machine#event.
|
327
|
+
#
|
328
|
+
# == Defining callbacks
|
329
|
+
#
|
330
|
+
# Within the +state_machine+ block, you can also define callbacks for
|
331
|
+
# transitions. For more information about defining these callbacks,
|
332
|
+
# see StateMachine::Machine#before_transition, StateMachine::Machine#after_transition,
|
333
|
+
# and StateMachine::Machine#around_transition.
|
334
|
+
#
|
335
|
+
# == Namespaces
|
336
|
+
#
|
337
|
+
# When a namespace is configured for a state machine, the name provided
|
338
|
+
# will be used in generating the instance methods for interacting with
|
339
|
+
# states/events in the machine. This is particularly useful when a class
|
340
|
+
# has multiple state machines and it would be difficult to differentiate
|
341
|
+
# between the various states / events.
|
342
|
+
#
|
343
|
+
# For example,
|
344
|
+
#
|
345
|
+
# class Vehicle
|
346
|
+
# state_machine :heater_state, :initial => :off, :namespace => 'heater' do
|
347
|
+
# event :turn_on do
|
348
|
+
# transition all => :on
|
349
|
+
# end
|
350
|
+
#
|
351
|
+
# event :turn_off do
|
352
|
+
# transition all => :off
|
353
|
+
# end
|
354
|
+
# end
|
355
|
+
#
|
356
|
+
# state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
|
357
|
+
# event :turn_on do
|
358
|
+
# transition all => :active
|
359
|
+
# end
|
360
|
+
#
|
361
|
+
# event :turn_off do
|
362
|
+
# transition all => :off
|
363
|
+
# end
|
364
|
+
# end
|
365
|
+
# end
|
366
|
+
#
|
367
|
+
# The above class defines two state machines: +heater_state+ and +alarm_state+.
|
368
|
+
# For the +heater_state+ machine, the following methods are generated since
|
369
|
+
# it's namespaced by "heater":
|
370
|
+
# * <tt>can_turn_on_heater?</tt>
|
371
|
+
# * <tt>turn_on_heater</tt>
|
372
|
+
# * ...
|
373
|
+
# * <tt>can_turn_off_heater?</tt>
|
374
|
+
# * <tt>turn_off_heater</tt>
|
375
|
+
# * ..
|
376
|
+
# * <tt>heater_off?</tt>
|
377
|
+
# * <tt>heater_on?</tt>
|
378
|
+
#
|
379
|
+
# As shown, each method is unique to the state machine so that the states
|
380
|
+
# and events don't conflict. The same goes for the +alarm_state+ machine:
|
381
|
+
# * <tt>can_turn_on_alarm?</tt>
|
382
|
+
# * <tt>turn_on_alarm</tt>
|
383
|
+
# * ...
|
384
|
+
# * <tt>can_turn_off_alarm?</tt>
|
385
|
+
# * <tt>turn_off_alarm</tt>
|
386
|
+
# * ..
|
387
|
+
# * <tt>alarm_active?</tt>
|
388
|
+
# * <tt>alarm_off?</tt>
|
389
|
+
#
|
390
|
+
# == Scopes
|
391
|
+
#
|
392
|
+
# For integrations that support it, a group of default scope filters will
|
393
|
+
# be automatically created for assisting in finding objects that have the
|
394
|
+
# attribute set to one of a given set of states.
|
395
|
+
#
|
396
|
+
# For example,
|
397
|
+
#
|
398
|
+
# Vehicle.with_state(:parked) # => All vehicles where the state is parked
|
399
|
+
# Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
|
400
|
+
#
|
401
|
+
# Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
|
402
|
+
# Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
|
403
|
+
#
|
404
|
+
# *Note* that if class methods already exist with those names (i.e.
|
405
|
+
# :with_state, :with_states, :without_state, or :without_states), then a
|
406
|
+
# scope will not be defined for that name.
|
407
|
+
#
|
408
|
+
# See StateMachine::Machine for more information about using integrations
|
409
|
+
# and the individual integration docs for information about the actual
|
410
|
+
# scopes that are generated.
|
411
|
+
def state_machine(*args, &block)
|
412
|
+
StateMachine::Machine.find_or_create(self, *args, &block)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
Class.class_eval do
|
418
|
+
include StateMachine::MacroMethods
|
419
|
+
end
|
420
|
+
|
421
|
+
require 'state_machine/initializers'
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.join("#{File.dirname(__FILE__)}/state_machine")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
namespace :state_machine do
|
2
|
+
desc 'Draws a set of state machines using GraphViz. Target files to load with FILE=x,y,z; Machine class with CLASS=x,y,z; Font name with FONT=x; Image format with FORMAT=x; Orientation with ORIENTATION=x'
|
3
|
+
task :draw do
|
4
|
+
if defined?(Rails)
|
5
|
+
Rake::Task['environment'].invoke
|
6
|
+
elsif defined?(Merb)
|
7
|
+
Rake::Task['merb_env'].invoke
|
8
|
+
|
9
|
+
# Fix ruby-graphviz being incompatible with Merb's process title
|
10
|
+
$0 = 'rake'
|
11
|
+
else
|
12
|
+
# Load the library
|
13
|
+
$:.unshift(File.dirname(__FILE__) + '/..')
|
14
|
+
require 'state_machine'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Build drawing options
|
18
|
+
options = {}
|
19
|
+
options[:file] = ENV['FILE'] if ENV['FILE']
|
20
|
+
options[:path] = ENV['TARGET'] if ENV['TARGET']
|
21
|
+
options[:format] = ENV['FORMAT'] if ENV['FORMAT']
|
22
|
+
options[:font] = ENV['FONT'] if ENV['FONT']
|
23
|
+
options[:orientation] = ENV['ORIENTATION'] if ENV['ORIENTATION']
|
24
|
+
|
25
|
+
StateMachine::Machine.draw(ENV['CLASS'], options)
|
26
|
+
end
|
27
|
+
end
|