state_machine 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +20 -0
- data/LICENSE +1 -1
- data/README.rdoc +74 -4
- data/Rakefile +3 -3
- data/lib/state_machine.rb +51 -24
- data/lib/state_machine/{guard.rb → branch.rb} +34 -40
- data/lib/state_machine/callback.rb +13 -18
- data/lib/state_machine/error.rb +13 -0
- data/lib/state_machine/eval_helpers.rb +3 -0
- data/lib/state_machine/event.rb +67 -30
- data/lib/state_machine/event_collection.rb +20 -3
- data/lib/state_machine/extensions.rb +3 -3
- data/lib/state_machine/integrations.rb +7 -0
- data/lib/state_machine/integrations/active_model.rb +149 -59
- data/lib/state_machine/integrations/active_model/versions.rb +30 -0
- data/lib/state_machine/integrations/active_record.rb +74 -148
- data/lib/state_machine/integrations/active_record/locale.rb +0 -7
- data/lib/state_machine/integrations/active_record/versions.rb +149 -0
- data/lib/state_machine/integrations/base.rb +64 -0
- data/lib/state_machine/integrations/data_mapper.rb +50 -39
- data/lib/state_machine/integrations/data_mapper/observer.rb +47 -12
- data/lib/state_machine/integrations/data_mapper/versions.rb +62 -0
- data/lib/state_machine/integrations/mongo_mapper.rb +37 -64
- data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
- data/lib/state_machine/integrations/mongo_mapper/versions.rb +102 -0
- data/lib/state_machine/integrations/mongoid.rb +297 -0
- data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
- data/lib/state_machine/integrations/mongoid/versions.rb +18 -0
- data/lib/state_machine/integrations/sequel.rb +99 -55
- data/lib/state_machine/integrations/sequel/versions.rb +40 -0
- data/lib/state_machine/machine.rb +273 -136
- data/lib/state_machine/machine_collection.rb +21 -13
- data/lib/state_machine/node_collection.rb +6 -1
- data/lib/state_machine/path.rb +120 -0
- data/lib/state_machine/path_collection.rb +90 -0
- data/lib/state_machine/state.rb +28 -9
- data/lib/state_machine/state_collection.rb +1 -1
- data/lib/state_machine/transition.rb +65 -6
- data/lib/state_machine/transition_collection.rb +1 -1
- data/test/files/en.yml +8 -0
- data/test/functional/state_machine_test.rb +15 -2
- data/test/unit/branch_test.rb +890 -0
- data/test/unit/callback_test.rb +9 -36
- data/test/unit/error_test.rb +43 -0
- data/test/unit/event_collection_test.rb +67 -33
- data/test/unit/event_test.rb +165 -38
- data/test/unit/integrations/active_model_test.rb +103 -3
- data/test/unit/integrations/active_record_test.rb +90 -43
- data/test/unit/integrations/base_test.rb +87 -0
- data/test/unit/integrations/data_mapper_test.rb +105 -44
- data/test/unit/integrations/mongo_mapper_test.rb +261 -64
- data/test/unit/integrations/mongoid_test.rb +1529 -0
- data/test/unit/integrations/sequel_test.rb +33 -49
- data/test/unit/integrations_test.rb +4 -0
- data/test/unit/invalid_event_test.rb +15 -2
- data/test/unit/invalid_parallel_transition_test.rb +18 -0
- data/test/unit/invalid_transition_test.rb +72 -2
- data/test/unit/machine_collection_test.rb +55 -61
- data/test/unit/machine_test.rb +388 -26
- data/test/unit/node_collection_test.rb +14 -4
- data/test/unit/path_collection_test.rb +266 -0
- data/test/unit/path_test.rb +485 -0
- data/test/unit/state_collection_test.rb +30 -0
- data/test/unit/state_test.rb +82 -35
- data/test/unit/transition_collection_test.rb +48 -44
- data/test/unit/transition_test.rb +198 -41
- metadata +111 -74
- data/test/unit/guard_test.rb +0 -909
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'state_machine/
|
1
|
+
require 'state_machine/branch'
|
2
2
|
require 'state_machine/eval_helpers'
|
3
3
|
|
4
4
|
module StateMachine
|
@@ -67,6 +67,7 @@ module StateMachine
|
|
67
67
|
# * +before+
|
68
68
|
# * +after+
|
69
69
|
# * +around+
|
70
|
+
# * +failure+
|
70
71
|
attr_accessor :type
|
71
72
|
|
72
73
|
# An optional block for determining whether to cancel the callback chain
|
@@ -98,17 +99,17 @@ module StateMachine
|
|
98
99
|
# end
|
99
100
|
attr_reader :terminator
|
100
101
|
|
101
|
-
# The
|
102
|
+
# The branch that determines whether or not this callback can be invoked
|
102
103
|
# based on the context of the transition. The event, from state, and
|
103
|
-
# to state must all match in order for the
|
104
|
+
# to state must all match in order for the branch to pass.
|
104
105
|
#
|
105
|
-
# See StateMachine::
|
106
|
-
attr_reader :
|
106
|
+
# See StateMachine::Branch for more information.
|
107
|
+
attr_reader :branch
|
107
108
|
|
108
109
|
# Creates a new callback that can get called based on the configured
|
109
110
|
# options.
|
110
111
|
#
|
111
|
-
# In addition to the possible configuration options for
|
112
|
+
# In addition to the possible configuration options for branches, the
|
112
113
|
# following options can be configured:
|
113
114
|
# * <tt>:bind_to_object</tt> - Whether to bind the callback to the object involved.
|
114
115
|
# If set to false, the object will be passed as a parameter instead.
|
@@ -121,7 +122,7 @@ module StateMachine
|
|
121
122
|
# callback can be found in their attribute definitions.
|
122
123
|
def initialize(type, *args, &block)
|
123
124
|
@type = type
|
124
|
-
raise ArgumentError, 'Type must be :before, :after, or :
|
125
|
+
raise ArgumentError, 'Type must be :before, :after, :around, or :failure' unless [:before, :after, :around, :failure].include?(type)
|
125
126
|
|
126
127
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
127
128
|
@methods = args
|
@@ -138,23 +139,23 @@ module StateMachine
|
|
138
139
|
end
|
139
140
|
|
140
141
|
@terminator = options.delete(:terminator)
|
141
|
-
@
|
142
|
+
@branch = Branch.new(options)
|
142
143
|
end
|
143
144
|
|
144
145
|
# Gets a list of the states known to this callback by looking at the
|
145
|
-
#
|
146
|
+
# branch's known states
|
146
147
|
def known_states
|
147
|
-
|
148
|
+
branch.known_states
|
148
149
|
end
|
149
150
|
|
150
|
-
# Runs the callback as long as the transition context matches the
|
151
|
+
# Runs the callback as long as the transition context matches the branch
|
151
152
|
# requirements configured for this callback. If a block is provided, it
|
152
153
|
# will be called when the last method has run.
|
153
154
|
#
|
154
155
|
# If a terminator has been configured and it matches the result from the
|
155
156
|
# evaluated method, then the callback chain should be halted.
|
156
157
|
def call(object, context = {}, *args, &block)
|
157
|
-
if @
|
158
|
+
if @branch.matches?(object, context)
|
158
159
|
run_methods(object, context, 0, *args, &block)
|
159
160
|
true
|
160
161
|
else
|
@@ -162,12 +163,6 @@ module StateMachine
|
|
162
163
|
end
|
163
164
|
end
|
164
165
|
|
165
|
-
# Verifies that the success requirement for this callback matches the given
|
166
|
-
# value
|
167
|
-
def matches_success?(success)
|
168
|
-
guard.success_requirement.matches?(success)
|
169
|
-
end
|
170
|
-
|
171
166
|
private
|
172
167
|
# Runs all of the methods configured for this callback.
|
173
168
|
#
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module StateMachine
|
2
|
+
# An error occurred during a state machine invocation
|
3
|
+
class Error < StandardError
|
4
|
+
# The object that failed
|
5
|
+
attr_reader :object
|
6
|
+
|
7
|
+
def initialize(object, message = nil) #:nodoc:
|
8
|
+
@object = object
|
9
|
+
|
10
|
+
super(message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -64,9 +64,12 @@ module StateMachine
|
|
64
64
|
# supports yielding within blocks)
|
65
65
|
if block_given? && Proc === method && arity != 0
|
66
66
|
if [1, 2].include?(arity)
|
67
|
+
# Force the block to be either the only argument or the 2nd one
|
68
|
+
# after the object (may mean additional arguments get discarded)
|
67
69
|
limit = arity
|
68
70
|
args.insert(limit - 1, block)
|
69
71
|
else
|
72
|
+
# Tack the block to the end of the args
|
70
73
|
limit += 1 unless limit < 0
|
71
74
|
args.push(block)
|
72
75
|
end
|
data/lib/state_machine/event.rb
CHANGED
@@ -1,16 +1,25 @@
|
|
1
1
|
require 'state_machine/transition'
|
2
|
-
require 'state_machine/
|
2
|
+
require 'state_machine/branch'
|
3
3
|
require 'state_machine/assertions'
|
4
4
|
require 'state_machine/matcher_helpers'
|
5
|
+
require 'state_machine/error'
|
5
6
|
|
6
7
|
module StateMachine
|
7
8
|
# An invalid event was specified
|
8
|
-
class InvalidEvent <
|
9
|
+
class InvalidEvent < Error
|
10
|
+
# The event that was attempted to be run
|
11
|
+
attr_reader :event
|
12
|
+
|
13
|
+
def initialize(object, event_name) #:nodoc:
|
14
|
+
@event = event_name
|
15
|
+
|
16
|
+
super(object, "#{event.inspect} is an unknown state machine event")
|
17
|
+
end
|
9
18
|
end
|
10
19
|
|
11
20
|
# An event defines an action that transitions an attribute from one state to
|
12
21
|
# another. The state that an attribute is transitioned to depends on the
|
13
|
-
#
|
22
|
+
# branches configured for the event.
|
14
23
|
class Event
|
15
24
|
include Assertions
|
16
25
|
include MatcherHelpers
|
@@ -27,12 +36,12 @@ module StateMachine
|
|
27
36
|
# The human-readable name for the event
|
28
37
|
attr_writer :human_name
|
29
38
|
|
30
|
-
# The list of
|
39
|
+
# The list of branches that determine what state this event transitions
|
31
40
|
# objects to when fired
|
32
|
-
attr_reader :
|
41
|
+
attr_reader :branches
|
33
42
|
|
34
43
|
# A list of all of the states known to this event using the configured
|
35
|
-
#
|
44
|
+
# branches/transitions as the source
|
36
45
|
attr_reader :known_states
|
37
46
|
|
38
47
|
# Creates a new event within the context of the given machine
|
@@ -46,17 +55,23 @@ module StateMachine
|
|
46
55
|
@name = name
|
47
56
|
@qualified_name = machine.namespace ? :"#{name}_#{machine.namespace}" : name
|
48
57
|
@human_name = options[:human_name] || @name.to_s.tr('_', ' ')
|
49
|
-
@
|
58
|
+
@branches = []
|
50
59
|
@known_states = []
|
51
60
|
|
52
|
-
|
61
|
+
# Output a warning if another event has a conflicting qualified name
|
62
|
+
if conflict = machine.owner_class.state_machines.detect {|name, other_machine| other_machine != @machine && other_machine.events[qualified_name, :qualified_name]}
|
63
|
+
name, other_machine = conflict
|
64
|
+
warn "Event #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
|
65
|
+
else
|
66
|
+
add_actions
|
67
|
+
end
|
53
68
|
end
|
54
69
|
|
55
70
|
# Creates a copy of this event in addition to the list of associated
|
56
|
-
#
|
71
|
+
# branches to prevent conflicts across events within a class hierarchy.
|
57
72
|
def initialize_copy(orig) #:nodoc:
|
58
73
|
super
|
59
|
-
@
|
74
|
+
@branches = @branches.dup
|
60
75
|
@known_states = @known_states.dup
|
61
76
|
end
|
62
77
|
|
@@ -101,6 +116,9 @@ module StateMachine
|
|
101
116
|
# transition :parked => same # Loops :parked back to :parked
|
102
117
|
# transition [:parked, :stalled] => same # Loops either :parked or :stalled back to the same state
|
103
118
|
# transition all - :parked => same # Loops every state but :parked back to the same state
|
119
|
+
#
|
120
|
+
# # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
|
121
|
+
# transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear
|
104
122
|
#
|
105
123
|
# == Verbose transitions
|
106
124
|
#
|
@@ -146,6 +164,7 @@ module StateMachine
|
|
146
164
|
#
|
147
165
|
# transition :parked => :idling, :if => :moving?
|
148
166
|
# transition :parked => :idling, :unless => :stopped?
|
167
|
+
# transition :idling => :first_gear, :first_gear => :second_gear, :if => :seatbelt_on?
|
149
168
|
#
|
150
169
|
# transition :from => :parked, :to => :idling, :if => :moving?
|
151
170
|
# transition :from => :parked, :to => :idling, :unless => :stopped?
|
@@ -162,27 +181,36 @@ module StateMachine
|
|
162
181
|
# requirements
|
163
182
|
assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
|
164
183
|
|
165
|
-
|
166
|
-
@known_states |=
|
167
|
-
|
184
|
+
branches << branch = Branch.new(options.merge(:on => name))
|
185
|
+
@known_states |= branch.known_states
|
186
|
+
branch
|
168
187
|
end
|
169
188
|
|
170
189
|
# Determines whether any transitions can be performed for this event based
|
171
190
|
# on the current state of the given object.
|
172
191
|
#
|
173
192
|
# If the event can't be fired, then this will return false, otherwise true.
|
174
|
-
def can_fire?(object)
|
175
|
-
!transition_for(object).nil?
|
193
|
+
def can_fire?(object, requirements = {})
|
194
|
+
!transition_for(object, requirements).nil?
|
176
195
|
end
|
177
196
|
|
178
197
|
# Finds and builds the next transition that can be performed on the given
|
179
198
|
# object. If no transitions can be made, then this will return nil.
|
199
|
+
#
|
200
|
+
# Valid requirement options:
|
201
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
202
|
+
# are specified, then this will be the object's current state.
|
203
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
204
|
+
# specified, then this will match any to state.
|
205
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
206
|
+
# conditionals defined for each one. Default is true.
|
180
207
|
def transition_for(object, requirements = {})
|
208
|
+
assert_valid_keys(requirements, :from, :to, :guard)
|
181
209
|
requirements[:from] = machine.states.match!(object).name unless custom_from_state = requirements.include?(:from)
|
182
210
|
|
183
|
-
|
184
|
-
if match =
|
185
|
-
#
|
211
|
+
branches.each do |branch|
|
212
|
+
if match = branch.match(object, requirements)
|
213
|
+
# Branch allows for the transition to occur
|
186
214
|
from = requirements[:from]
|
187
215
|
to = match[:to].values.empty? ? from : match[:to].values.first
|
188
216
|
|
@@ -206,19 +234,28 @@ module StateMachine
|
|
206
234
|
if transition = transition_for(object)
|
207
235
|
transition.perform(*args)
|
208
236
|
else
|
209
|
-
|
237
|
+
on_failure(object)
|
210
238
|
false
|
211
239
|
end
|
212
240
|
end
|
213
241
|
|
242
|
+
# Marks the object as invalid and runs any failure callbacks associated with
|
243
|
+
# this event. This should get called anytime this event fails to transition.
|
244
|
+
def on_failure(object)
|
245
|
+
machine.invalidate(object, :state, :invalid_transition, [[:event, human_name(object.class)]])
|
246
|
+
|
247
|
+
state = machine.states.match!(object).name
|
248
|
+
Transition.new(object, machine, name, state, state).run_callbacks(:before => false)
|
249
|
+
end
|
250
|
+
|
214
251
|
# Draws a representation of this event on the given graph. This will
|
215
|
-
# create 1 or more edges on the graph for each
|
252
|
+
# create 1 or more edges on the graph for each branch (i.e. transition)
|
216
253
|
# configured.
|
217
254
|
#
|
218
255
|
# A collection of the generated edges will be returned.
|
219
256
|
def draw(graph)
|
220
257
|
valid_states = machine.states.by_priority.map {|state| state.name}
|
221
|
-
|
258
|
+
branches.collect {|branch| branch.draw(graph, name, valid_states)}.flatten
|
222
259
|
end
|
223
260
|
|
224
261
|
# Generates a nicely formatted description of this event's contents.
|
@@ -229,8 +266,8 @@ module StateMachine
|
|
229
266
|
# event.transition all - :idling => :parked, :idling => same
|
230
267
|
# event # => #<StateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
|
231
268
|
def inspect
|
232
|
-
transitions =
|
233
|
-
|
269
|
+
transitions = branches.map do |branch|
|
270
|
+
branch.state_requirements.map do |state_requirement|
|
234
271
|
"#{state_requirement[:from].description} => #{state_requirement[:to].description}"
|
235
272
|
end * ', '
|
236
273
|
end
|
@@ -243,24 +280,24 @@ module StateMachine
|
|
243
280
|
# the current event
|
244
281
|
def add_actions
|
245
282
|
# Checks whether the event can be fired on the current object
|
246
|
-
machine.
|
247
|
-
machine.event(name).can_fire?(object)
|
283
|
+
machine.define_helper(:instance, "can_#{qualified_name}?") do |machine, object, _super, *args|
|
284
|
+
machine.event(name).can_fire?(object, *args)
|
248
285
|
end
|
249
286
|
|
250
287
|
# Gets the next transition that would be performed if the event were
|
251
288
|
# fired now
|
252
|
-
machine.
|
253
|
-
machine.event(name).transition_for(object)
|
289
|
+
machine.define_helper(:instance, "#{qualified_name}_transition") do |machine, object, _super, *args|
|
290
|
+
machine.event(name).transition_for(object, *args)
|
254
291
|
end
|
255
292
|
|
256
293
|
# Fires the event
|
257
|
-
machine.
|
294
|
+
machine.define_helper(:instance, qualified_name) do |machine, object, _super, *args|
|
258
295
|
machine.event(name).fire(object, *args)
|
259
296
|
end
|
260
297
|
|
261
298
|
# Fires the event, raising an exception if it fails
|
262
|
-
machine.
|
263
|
-
object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition,
|
299
|
+
machine.define_helper(:instance, "#{qualified_name}!") do |machine, object, _super, *args|
|
300
|
+
object.send(qualified_name, *args) || raise(StateMachine::InvalidTransition.new(object, machine, name))
|
264
301
|
end
|
265
302
|
end
|
266
303
|
end
|
@@ -7,6 +7,16 @@ module StateMachine
|
|
7
7
|
|
8
8
|
# Gets the list of events that can be fired on the given object.
|
9
9
|
#
|
10
|
+
# Valid requirement options:
|
11
|
+
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
12
|
+
# are specified, then this will be the object's current state.
|
13
|
+
# * <tt>:to</tt> - One or more states being transitioned to. If none are
|
14
|
+
# specified, then this will match any to state.
|
15
|
+
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
16
|
+
# are specified, then this will match any event.
|
17
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
18
|
+
# conditionals defined for each one. Default is true.
|
19
|
+
#
|
10
20
|
# == Examples
|
11
21
|
#
|
12
22
|
# class Vehicle
|
@@ -28,8 +38,8 @@ module StateMachine
|
|
28
38
|
#
|
29
39
|
# vehicle.state = 'idling'
|
30
40
|
# events.valid_for(vehicle) # => [#<StateMachine::Event name=:park transitions=[:idling => :parked]>]
|
31
|
-
def valid_for(object)
|
32
|
-
select {|event| event.can_fire?(object)}
|
41
|
+
def valid_for(object, requirements = {})
|
42
|
+
match(requirements).select {|event| event.can_fire?(object, requirements)}
|
33
43
|
end
|
34
44
|
|
35
45
|
# Gets the list of transitions that can be run on the given object.
|
@@ -41,6 +51,8 @@ module StateMachine
|
|
41
51
|
# specified, then this will match any to state.
|
42
52
|
# * <tt>:on</tt> - One or more events that fire the transition. If none
|
43
53
|
# are specified, then this will match any event.
|
54
|
+
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
55
|
+
# conditionals defined for each one. Default is true.
|
44
56
|
#
|
45
57
|
# == Examples
|
46
58
|
#
|
@@ -67,7 +79,7 @@ module StateMachine
|
|
67
79
|
# # Search for explicit transitions regardless of the current state
|
68
80
|
# events.transitions_for(vehicle, :from => :parked) # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
69
81
|
def transitions_for(object, requirements = {})
|
70
|
-
map {|event| event.transition_for(object, requirements)}.compact
|
82
|
+
match(requirements).map {|event| event.transition_for(object, requirements)}.compact
|
71
83
|
end
|
72
84
|
|
73
85
|
# Gets the transition that should be performed for the event stored in the
|
@@ -118,5 +130,10 @@ module StateMachine
|
|
118
130
|
|
119
131
|
result
|
120
132
|
end
|
133
|
+
|
134
|
+
private
|
135
|
+
def match(requirements) #:nodoc:
|
136
|
+
requirements && requirements[:on] ? [fetch(requirements.delete(:on))] : self
|
137
|
+
end
|
121
138
|
end
|
122
139
|
end
|
@@ -138,12 +138,12 @@ module StateMachine
|
|
138
138
|
# vehicle.fire_events!(:ignite, :disable_alarm) # => StateMachine::InvalidTranstion: Cannot run events in parallel: ignite, disable_alarm
|
139
139
|
def fire_events!(*events)
|
140
140
|
run_action = [true, false].include?(events.last) ? events.pop : true
|
141
|
-
fire_events(*(events + [run_action])) || raise(StateMachine::
|
141
|
+
fire_events(*(events + [run_action])) || raise(StateMachine::InvalidParallelTransition.new(self, events))
|
142
142
|
end
|
143
143
|
|
144
144
|
protected
|
145
|
-
def initialize_state_machines(options = {}) #:nodoc:
|
146
|
-
self.class.state_machines.initialize_states(self, options)
|
145
|
+
def initialize_state_machines(options = {}, &block) #:nodoc:
|
146
|
+
self.class.state_machines.initialize_states(self, options, &block)
|
147
147
|
end
|
148
148
|
end
|
149
149
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# Load each available integration
|
2
|
+
require 'state_machine/integrations/base'
|
2
3
|
Dir["#{File.dirname(__FILE__)}/integrations/*.rb"].sort.each do |path|
|
3
4
|
require "state_machine/integrations/#{File.basename(path)}"
|
4
5
|
end
|
@@ -45,6 +46,10 @@ module StateMachine
|
|
45
46
|
# include DataMapper::Resource
|
46
47
|
# end
|
47
48
|
#
|
49
|
+
# class MongoidVehicle
|
50
|
+
# include Mongoid::Document
|
51
|
+
# end
|
52
|
+
#
|
48
53
|
# class MongoMapperVehicle
|
49
54
|
# include MongoMapper::Document
|
50
55
|
# end
|
@@ -56,6 +61,7 @@ module StateMachine
|
|
56
61
|
# StateMachine::Integrations.match(ActiveModelVehicle) # => StateMachine::Integrations::ActiveModel
|
57
62
|
# StateMachine::Integrations.match(ActiveRecordVehicle) # => StateMachine::Integrations::ActiveRecord
|
58
63
|
# StateMachine::Integrations.match(DataMapperVehicle) # => StateMachine::Integrations::DataMapper
|
64
|
+
# StateMachine::Integrations.match(MongoidVehicle) # => StateMachine::Integrations::Mongoid
|
59
65
|
# StateMachine::Integrations.match(MongoMapperVehicle) # => StateMachine::Integrations::MongoMapper
|
60
66
|
# StateMachine::Integrations.match(SequelVehicle) # => StateMachine::Integrations::Sequel
|
61
67
|
def self.match(klass)
|
@@ -73,6 +79,7 @@ module StateMachine
|
|
73
79
|
# StateMachine::Integrations.find(:active_record) # => StateMachine::Integrations::ActiveRecord
|
74
80
|
# StateMachine::Integrations.find(:active_model) # => StateMachine::Integrations::ActiveModel
|
75
81
|
# StateMachine::Integrations.find(:data_mapper) # => StateMachine::Integrations::DataMapper
|
82
|
+
# StateMachine::Integrations.find(:mongoid) # => StateMachine::Integrations::Mongoid
|
76
83
|
# StateMachine::Integrations.find(:mongo_mapper) # => StateMachine::Integrations::MongoMapper
|
77
84
|
# StateMachine::Integrations.find(:sequel) # => StateMachine::Integrations::Sequel
|
78
85
|
# StateMachine::Integrations.find(:invalid) # => NameError: wrong constant name Invalid
|
@@ -8,6 +8,7 @@ module StateMachine
|
|
8
8
|
# following features need to be included in order for the integration to be
|
9
9
|
# detected:
|
10
10
|
# * ActiveModel::Dirty
|
11
|
+
# * ActiveModel::MassAssignmentSecurity
|
11
12
|
# * ActiveModel::Observing
|
12
13
|
# * ActiveModel::Validations
|
13
14
|
#
|
@@ -16,6 +17,7 @@ module StateMachine
|
|
16
17
|
#
|
17
18
|
# class Vehicle
|
18
19
|
# include ActiveModel::Dirty
|
20
|
+
# include ActiveModel::MassAssignmentSecurity
|
19
21
|
# include ActiveModel::Observing
|
20
22
|
# include ActiveModel::Validations
|
21
23
|
#
|
@@ -65,6 +67,44 @@ module StateMachine
|
|
65
67
|
# vehicle.ignite # => false
|
66
68
|
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
|
67
69
|
#
|
70
|
+
# === Security implications
|
71
|
+
#
|
72
|
+
# Beware that public event attributes mean that events can be fired
|
73
|
+
# whenever mass-assignment is being used. If you want to prevent malicious
|
74
|
+
# users from tampering with events through URLs / forms, the attribute
|
75
|
+
# should be protected like so:
|
76
|
+
#
|
77
|
+
# class Vehicle
|
78
|
+
# include ActiveModel::MassAssignmentSecurity
|
79
|
+
# attr_accessor :state
|
80
|
+
#
|
81
|
+
# attr_protected :state_event
|
82
|
+
# # attr_accessible ... # Alternative technique
|
83
|
+
#
|
84
|
+
# state_machine do
|
85
|
+
# ...
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# If you want to only have *some* events be able to fire via mass-assignment,
|
90
|
+
# you can build two state machines (one public and one protected) like so:
|
91
|
+
#
|
92
|
+
# class Vehicle
|
93
|
+
# include ActiveModel::MassAssignmentSecurity
|
94
|
+
# attr_accessor :state
|
95
|
+
#
|
96
|
+
# attr_protected :state_event # Prevent access to events in the first machine
|
97
|
+
#
|
98
|
+
# state_machine do
|
99
|
+
# # Define private events here
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# # Public machine targets the same state as the private machine
|
103
|
+
# state_machine :public_state, :attribute => :state do
|
104
|
+
# # Define public events here
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
#
|
68
108
|
# == Callbacks
|
69
109
|
#
|
70
110
|
# All before/after transition callbacks defined for ActiveModel models
|
@@ -108,15 +148,15 @@ module StateMachine
|
|
108
148
|
# hooks *are* supported. For example, if a transition for a object's
|
109
149
|
# +state+ attribute changes the state from +parked+ to +idling+ via the
|
110
150
|
# +ignite+ event, the following observer methods are supported:
|
111
|
-
# * before/
|
112
|
-
# * before/
|
113
|
-
# * before/
|
114
|
-
# * before/
|
115
|
-
# * before/
|
116
|
-
# * before/
|
117
|
-
# * before/
|
118
|
-
# * before/
|
119
|
-
# * before/
|
151
|
+
# * before/after/after_failure_to-_ignite_from_parked_to_idling
|
152
|
+
# * before/after/after_failure_to-_ignite_from_parked
|
153
|
+
# * before/after/after_failure_to-_ignite_to_idling
|
154
|
+
# * before/after/after_failure_to-_ignite
|
155
|
+
# * before/after/after_failure_to-_transition_state_from_parked_to_idling
|
156
|
+
# * before/after/after_failure_to-_transition_state_from_parked
|
157
|
+
# * before/after/after_failure_to-_transition_state_to_idling
|
158
|
+
# * before/after/after_failure_to-_transition_state
|
159
|
+
# * before/after/after_failure_to-_transition
|
120
160
|
#
|
121
161
|
# The following class shows an example of some of these hooks:
|
122
162
|
#
|
@@ -135,6 +175,10 @@ module StateMachine
|
|
135
175
|
# def after_transition(vehicle, transition)
|
136
176
|
# Audit.log(vehicle, transition)
|
137
177
|
# end
|
178
|
+
#
|
179
|
+
# def after_failure_to_transition(vehicle, transition)
|
180
|
+
# Audit.error(vehicle, transition)
|
181
|
+
# end
|
138
182
|
# end
|
139
183
|
#
|
140
184
|
# More flexible transition callbacks can be defined directly within the
|
@@ -200,7 +244,7 @@ module StateMachine
|
|
200
244
|
# end
|
201
245
|
#
|
202
246
|
# protected
|
203
|
-
# def
|
247
|
+
# def runs_validations_on_action?
|
204
248
|
# action == :persist
|
205
249
|
# end
|
206
250
|
#
|
@@ -214,47 +258,35 @@ module StateMachine
|
|
214
258
|
# must add these independent of the ActiveModel integration. See the
|
215
259
|
# ActiveRecord implementation for examples of these customizations.
|
216
260
|
module ActiveModel
|
217
|
-
module ClassMethods
|
218
|
-
# The default options to use for state machines using this integration
|
219
|
-
attr_reader :defaults
|
220
|
-
|
221
|
-
# Loads additional files specific to ActiveModel
|
222
|
-
def extended(base) #:nodoc:
|
223
|
-
require 'state_machine/integrations/active_model/observer'
|
224
|
-
|
225
|
-
if defined?(I18n)
|
226
|
-
locale = "#{File.dirname(__FILE__)}/active_model/locale.rb"
|
227
|
-
I18n.load_path.unshift(locale) unless I18n.load_path.include?(locale)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
261
|
def self.included(base) #:nodoc:
|
233
|
-
base.
|
234
|
-
extend ClassMethods
|
235
|
-
end
|
262
|
+
base.versions.unshift(*versions)
|
236
263
|
end
|
237
264
|
|
265
|
+
include Base
|
238
266
|
extend ClassMethods
|
239
267
|
|
268
|
+
require 'state_machine/integrations/active_model/versions'
|
269
|
+
|
270
|
+
@defaults = {}
|
271
|
+
|
240
272
|
# Should this integration be used for state machines in the given class?
|
241
|
-
# Classes that include ActiveModel::Dirty, ActiveModel::
|
242
|
-
# ActiveModel::Validations will automatically
|
243
|
-
# integration.
|
273
|
+
# Classes that include ActiveModel::Dirty, ActiveModel::MassAssignmentSecurity,
|
274
|
+
# ActiveModel::Observing, or ActiveModel::Validations will automatically
|
275
|
+
# use the ActiveModel integration.
|
244
276
|
def self.matches?(klass)
|
245
|
-
features = %w(Dirty Observing Validations)
|
277
|
+
features = %w(Dirty MassAssignmentSecurity Observing Validations)
|
246
278
|
defined?(::ActiveModel) && features.any? {|feature| ::ActiveModel.const_defined?(feature) && klass <= ::ActiveModel.const_get(feature)}
|
247
279
|
end
|
248
280
|
|
249
|
-
@defaults = {}
|
250
|
-
|
251
281
|
# Forces the change in state to be recognized regardless of whether the
|
252
282
|
# state value actually changed
|
253
|
-
def write(object, attribute, value)
|
283
|
+
def write(object, attribute, value, *args)
|
254
284
|
result = super
|
255
|
-
|
285
|
+
|
286
|
+
if (attribute == :state || attribute == :event && value) && supports_dirty_tracking?(object) && !object.send("#{self.attribute}_changed?")
|
256
287
|
object.send("#{self.attribute}_will_change!")
|
257
288
|
end
|
289
|
+
|
258
290
|
result
|
259
291
|
end
|
260
292
|
|
@@ -278,6 +310,11 @@ module StateMachine
|
|
278
310
|
end
|
279
311
|
|
280
312
|
protected
|
313
|
+
# The name of this integration
|
314
|
+
def integration
|
315
|
+
:active_model
|
316
|
+
end
|
317
|
+
|
281
318
|
# Whether observers are supported in the integration. Only true if
|
282
319
|
# ActiveModel::Observer is available.
|
283
320
|
def supports_observers?
|
@@ -303,14 +340,40 @@ module StateMachine
|
|
303
340
|
defined?(::ActiveModel::Dirty) && owner_class <= ::ActiveModel::Dirty && object.respond_to?("#{self.attribute}_changed?")
|
304
341
|
end
|
305
342
|
|
343
|
+
# Whether the protection of attributes via mass-assignment is supported
|
344
|
+
# in this integration. Only true if the ActiveModel feature is enabled
|
345
|
+
# on the owner class.
|
346
|
+
def supports_mass_assignment_security?
|
347
|
+
defined?(::ActiveModel::MassAssignmentSecurity) && owner_class <= ::ActiveModel::MassAssignmentSecurity
|
348
|
+
end
|
349
|
+
|
306
350
|
# Gets the terminator to use for callbacks
|
307
351
|
def callback_terminator
|
308
352
|
@terminator ||= lambda {|result| result == false}
|
309
353
|
end
|
310
354
|
|
311
355
|
# Determines the base scope to use when looking up translations
|
312
|
-
def i18n_scope
|
313
|
-
|
356
|
+
def i18n_scope(klass)
|
357
|
+
klass.i18n_scope
|
358
|
+
end
|
359
|
+
|
360
|
+
# Only allows state initialization on new records that aren't being
|
361
|
+
# created with a set of attributes that includes this machine's
|
362
|
+
# attribute.
|
363
|
+
def initialize_state?(object, options)
|
364
|
+
if supports_mass_assignment_security?
|
365
|
+
attributes = (options[:attributes] || {}).dup.stringify_keys!
|
366
|
+
ignore = filter_attributes(object, attributes).keys
|
367
|
+
!ignore.map {|attribute| attribute.to_sym}.include?(attribute)
|
368
|
+
else
|
369
|
+
super
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Filters attributes that cannot be assigned through the initialization
|
374
|
+
# of the object
|
375
|
+
def filter_attributes(object, attributes)
|
376
|
+
object.send(:sanitize_for_mass_assignment, attributes)
|
314
377
|
end
|
315
378
|
|
316
379
|
# The default options to use when generating messages for validation
|
@@ -322,7 +385,7 @@ module StateMachine
|
|
322
385
|
# Translates the given key / value combo. Translation keys are looked
|
323
386
|
# up in the following order:
|
324
387
|
# * <tt>#{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}</tt>
|
325
|
-
# * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}
|
388
|
+
# * <tt>#{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}</tt>
|
326
389
|
# * <tt>#{i18n_scope}.state_machines.#{plural_key}.#{value}</tt>
|
327
390
|
#
|
328
391
|
# If no keys are found, then the humanized value will be the fallback.
|
@@ -334,7 +397,7 @@ module StateMachine
|
|
334
397
|
# Generate all possible translation keys
|
335
398
|
translations = ancestors.map {|ancestor| :"#{ancestor.model_name.underscore}.#{name}.#{group}.#{value}"}
|
336
399
|
translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
|
337
|
-
I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope, :state_machines])
|
400
|
+
I18n.translate(translations.shift, :default => translations, :scope => [i18n_scope(klass), :state_machines])
|
338
401
|
end
|
339
402
|
|
340
403
|
# Build a list of ancestors for the given class to use when
|
@@ -343,12 +406,34 @@ module StateMachine
|
|
343
406
|
klass.lookup_ancestors
|
344
407
|
end
|
345
408
|
|
346
|
-
#
|
347
|
-
# before/after a transition has been performed.
|
409
|
+
# Initializes class-level extensions and defaults for this machine
|
348
410
|
def after_initialize
|
411
|
+
load_locale
|
412
|
+
load_observer_extensions
|
413
|
+
add_default_callbacks
|
414
|
+
end
|
415
|
+
|
416
|
+
# Loads any locale files needed for translating validation errors
|
417
|
+
def load_locale
|
418
|
+
I18n.load_path.unshift(locale_path) unless I18n.load_path.include?(locale_path)
|
419
|
+
end
|
420
|
+
|
421
|
+
# The path to the locale file containing state_machine translations
|
422
|
+
def locale_path
|
423
|
+
"#{File.dirname(__FILE__)}/#{integration}/locale.rb"
|
424
|
+
end
|
425
|
+
|
426
|
+
# Loads extensions to ActiveModel's Observers
|
427
|
+
def load_observer_extensions
|
428
|
+
require 'state_machine/integrations/active_model/observer'
|
429
|
+
end
|
430
|
+
|
431
|
+
# Adds a set of default callbacks that utilize the Observer extensions
|
432
|
+
def add_default_callbacks
|
349
433
|
if supports_observers?
|
350
434
|
callbacks[:before] << Callback.new(:before) {|object, transition| notify(:before, object, transition)}
|
351
435
|
callbacks[:after] << Callback.new(:after) {|object, transition| notify(:after, object, transition)}
|
436
|
+
callbacks[:failure] << Callback.new(:failure) {|object, transition| notify(:after_failure_to, object, transition)}
|
352
437
|
end
|
353
438
|
end
|
354
439
|
|
@@ -363,15 +448,20 @@ module StateMachine
|
|
363
448
|
end
|
364
449
|
|
365
450
|
# Adds hooks into validation for automatically firing events
|
366
|
-
def define_action_helpers
|
451
|
+
def define_action_helpers
|
367
452
|
super
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
453
|
+
define_validation_hook if runs_validations_on_action?
|
454
|
+
end
|
455
|
+
|
456
|
+
# Hooks into validations by defining around callbacks for the
|
457
|
+
# :validation event
|
458
|
+
def define_validation_hook
|
459
|
+
owner_class.set_callback(:validation, :around, self, :prepend => true)
|
460
|
+
end
|
461
|
+
|
462
|
+
# Runs state events around the object's validation process
|
463
|
+
def around_validation(object)
|
464
|
+
object.class.state_machines.transitions(object, action, :after => false).perform { yield }
|
375
465
|
end
|
376
466
|
|
377
467
|
# Creates a new callback in the callback chain, always inserting it
|
@@ -406,15 +496,15 @@ module StateMachine
|
|
406
496
|
# Notifies observers on the given object that a callback occurred
|
407
497
|
# involving the given transition. This will attempt to call the
|
408
498
|
# following methods on observers:
|
409
|
-
# *
|
410
|
-
# *
|
411
|
-
# *
|
412
|
-
# *
|
413
|
-
# *
|
414
|
-
# *
|
415
|
-
# *
|
416
|
-
# *
|
417
|
-
# *
|
499
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}_to_#{to}</tt>
|
500
|
+
# * <tt>#{type}_#{qualified_event}_from_#{from}</tt>
|
501
|
+
# * <tt>#{type}_#{qualified_event}_to_#{to}</tt>
|
502
|
+
# * <tt>#{type}_#{qualified_event}</tt>
|
503
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}_to_#{to}</tt>
|
504
|
+
# * <tt>#{type}_transition_#{machine_name}_from_#{from}</tt>
|
505
|
+
# * <tt>#{type}_transition_#{machine_name}_to_#{to}</tt>
|
506
|
+
# * <tt>#{type}_transition_#{machine_name}</tt>
|
507
|
+
# * <tt>#{type}_transition</tt>
|
418
508
|
#
|
419
509
|
# This will always return true regardless of the results of the
|
420
510
|
# callbacks.
|