state_machine 0.7.6 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +17 -0
- data/Rakefile +1 -1
- data/lib/state_machine.rb +11 -8
- data/lib/state_machine/callback.rb +1 -1
- data/lib/state_machine/event.rb +9 -8
- data/lib/state_machine/event_collection.rb +21 -10
- data/lib/state_machine/extensions.rb +2 -11
- data/lib/state_machine/guard.rb +12 -1
- data/lib/state_machine/integrations/active_record.rb +37 -1
- data/lib/state_machine/integrations/data_mapper.rb +17 -2
- data/lib/state_machine/integrations/sequel.rb +32 -1
- data/lib/state_machine/machine.rb +56 -16
- data/lib/state_machine/machine_collection.rb +7 -5
- data/lib/state_machine/state.rb +1 -1
- data/lib/state_machine/transition.rb +41 -14
- data/test/unit/assertions_test.rb +3 -3
- data/test/unit/eval_helpers_test.rb +13 -22
- data/test/unit/event_collection_test.rb +24 -0
- data/test/unit/event_test.rb +94 -0
- data/test/unit/guard_test.rb +46 -0
- data/test/unit/integrations/active_record_test.rb +247 -18
- data/test/unit/integrations/data_mapper_test.rb +143 -1
- data/test/unit/integrations/sequel_test.rb +279 -10
- data/test/unit/machine_collection_test.rb +42 -19
- data/test/unit/machine_test.rb +55 -0
- data/test/unit/state_test.rb +1 -1
- data/test/unit/transition_test.rb +106 -7
- metadata +2 -2
@@ -4,10 +4,12 @@ module StateMachine
|
|
4
4
|
# Initializes the state of each machine in the given object. Initial
|
5
5
|
# values are only set if the machine's attribute doesn't already exist
|
6
6
|
# (which must mean the defaults are being skipped)
|
7
|
-
def initialize_states(object)
|
7
|
+
def initialize_states(object, options = {})
|
8
8
|
each_value do |machine|
|
9
|
-
|
10
|
-
|
9
|
+
if !options.include?(:dynamic) || machine.dynamic_initial_state? == options[:dynamic]
|
10
|
+
value = machine.read(object, :state)
|
11
|
+
machine.write(object, :state, machine.initial_state(object).value) if value.nil? || value.respond_to?(:empty?) && value.empty?
|
12
|
+
end
|
11
13
|
end
|
12
14
|
end
|
13
15
|
|
@@ -45,7 +47,7 @@ module StateMachine
|
|
45
47
|
end
|
46
48
|
|
47
49
|
# Runs one or more event attributes in parallel during the invocation of
|
48
|
-
# an action on the given object.
|
50
|
+
# an action on the given object. after_transition callbacks can be
|
49
51
|
# optionally disabled if the events are being only partially fired (for
|
50
52
|
# example, when validating records in ORM integrations).
|
51
53
|
#
|
@@ -56,7 +58,7 @@ module StateMachine
|
|
56
58
|
#
|
57
59
|
# class Vehicle
|
58
60
|
# include DataMapper::Resource
|
59
|
-
# property :id,
|
61
|
+
# property :id, Serial
|
60
62
|
#
|
61
63
|
# state_machine :initial => :parked do
|
62
64
|
# event :ignite do
|
data/lib/state_machine/state.rb
CHANGED
@@ -191,7 +191,7 @@ module StateMachine
|
|
191
191
|
context_method.bind(object).call(*args, &block)
|
192
192
|
else
|
193
193
|
# Raise exception as if the method never existed on the original object
|
194
|
-
raise NoMethodError, "undefined method '#{method}' for #{object}
|
194
|
+
raise NoMethodError, "undefined method '#{method}' for #{object} with #{name || 'nil'} #{machine.name}"
|
195
195
|
end
|
196
196
|
end
|
197
197
|
|
@@ -16,8 +16,8 @@ module StateMachine
|
|
16
16
|
# 1. Before callbacks
|
17
17
|
# 2. Persist state
|
18
18
|
# 3. Invoke action
|
19
|
-
# 4. After callbacks if configured
|
20
|
-
# 5. Rollback if action is unsuccessful
|
19
|
+
# 4. After callbacks (if configured)
|
20
|
+
# 5. Rollback (if action is unsuccessful)
|
21
21
|
#
|
22
22
|
# Configuration options:
|
23
23
|
# * <tt>:action</tt> - Whether to run the action configured for each transition
|
@@ -46,7 +46,7 @@ module StateMachine
|
|
46
46
|
# Block was given: use the result for each transition
|
47
47
|
result = yield
|
48
48
|
transitions.each {|transition| results[transition.action] = result}
|
49
|
-
result
|
49
|
+
!!result
|
50
50
|
elsif options[:action] == false
|
51
51
|
# Skip the action
|
52
52
|
true
|
@@ -64,8 +64,9 @@ module StateMachine
|
|
64
64
|
raise
|
65
65
|
end
|
66
66
|
|
67
|
-
#
|
68
|
-
|
67
|
+
# Run after callbacks even when the actions failed. The :after option
|
68
|
+
# is ignored if the transitions were unsuccessful.
|
69
|
+
transitions.each {|transition| transition.after(results[transition.action], success)} unless options[:after] == false && success
|
69
70
|
|
70
71
|
# Rollback the transitions if the transaction was unsuccessful
|
71
72
|
transitions.each {|transition| transition.rollback} unless success
|
@@ -124,7 +125,7 @@ module StateMachine
|
|
124
125
|
attr_reader :result
|
125
126
|
|
126
127
|
# Creates a new, specific transition
|
127
|
-
def initialize(object, machine, event, from_name, to_name) #:nodoc:
|
128
|
+
def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
|
128
129
|
@object = object
|
129
130
|
@machine = machine
|
130
131
|
@args = []
|
@@ -136,7 +137,7 @@ module StateMachine
|
|
136
137
|
|
137
138
|
# From state information
|
138
139
|
from_state = machine.states.fetch(from_name)
|
139
|
-
@from = machine.read(object, :state)
|
140
|
+
@from = read_state ? machine.read(object, :state) : from_state.value
|
140
141
|
@from_name = from_state.name
|
141
142
|
@qualified_from_name = from_state.qualified_name
|
142
143
|
|
@@ -218,6 +219,9 @@ module StateMachine
|
|
218
219
|
# callbacks that are configured to match the event, from state, and to
|
219
220
|
# state will be invoked.
|
220
221
|
#
|
222
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
223
|
+
# is reset.
|
224
|
+
#
|
221
225
|
# == Example
|
222
226
|
#
|
223
227
|
# class Vehicle
|
@@ -233,7 +237,11 @@ module StateMachine
|
|
233
237
|
result = false
|
234
238
|
|
235
239
|
catch(:halt) do
|
236
|
-
|
240
|
+
unless @before_run
|
241
|
+
callback(:before)
|
242
|
+
@before_run = true
|
243
|
+
end
|
244
|
+
|
237
245
|
result = true
|
238
246
|
end
|
239
247
|
|
@@ -241,7 +249,8 @@ module StateMachine
|
|
241
249
|
end
|
242
250
|
|
243
251
|
# Transitions the current value of the state to that specified by the
|
244
|
-
# transition.
|
252
|
+
# transition. Once the state is persisted, it cannot be persisted again
|
253
|
+
# until this transition is reset.
|
245
254
|
#
|
246
255
|
# == Example
|
247
256
|
#
|
@@ -259,16 +268,22 @@ module StateMachine
|
|
259
268
|
#
|
260
269
|
# vehicle.state # => 'idling'
|
261
270
|
def persist
|
262
|
-
|
271
|
+
unless @persisted
|
272
|
+
machine.write(object, :state, to)
|
273
|
+
@persisted = true
|
274
|
+
end
|
263
275
|
end
|
264
276
|
|
265
277
|
# Runs the machine's +after+ callbacks for this transition. Only
|
266
278
|
# callbacks that are configured to match the event, from state, and to
|
267
279
|
# state will be invoked.
|
268
280
|
#
|
269
|
-
# The result
|
281
|
+
# The result can be used to indicate whether the associated machine action
|
270
282
|
# was executed successfully.
|
271
283
|
#
|
284
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
285
|
+
# is reset.
|
286
|
+
#
|
272
287
|
# == Halting
|
273
288
|
#
|
274
289
|
# If any callback throws a <tt>:halt</tt> exception, it will be caught
|
@@ -291,11 +306,14 @@ module StateMachine
|
|
291
306
|
# vehicle = Vehicle.new
|
292
307
|
# transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
293
308
|
# transition.after(true)
|
294
|
-
def after(result = nil)
|
309
|
+
def after(result = nil, success = true)
|
295
310
|
@result = result
|
296
311
|
|
297
312
|
catch(:halt) do
|
298
|
-
|
313
|
+
unless @after_run
|
314
|
+
callback(:after, :success => success)
|
315
|
+
@after_run = true
|
316
|
+
end
|
299
317
|
end
|
300
318
|
|
301
319
|
true
|
@@ -326,9 +344,16 @@ module StateMachine
|
|
326
344
|
# transition.rollback
|
327
345
|
# vehicle.state # => "parked"
|
328
346
|
def rollback
|
347
|
+
reset
|
329
348
|
machine.write(object, :state, from)
|
330
349
|
end
|
331
350
|
|
351
|
+
# Resets any tracking of which callbacks have already been run and whether
|
352
|
+
# the state has already been persisted
|
353
|
+
def reset
|
354
|
+
@before_run = @persisted = @after_run = false
|
355
|
+
end
|
356
|
+
|
332
357
|
# Generates a nicely formatted description of this transitions's contents.
|
333
358
|
#
|
334
359
|
# For example,
|
@@ -358,7 +383,9 @@ module StateMachine
|
|
358
383
|
#
|
359
384
|
# Additional callback parameters can be specified. By default, this
|
360
385
|
# transition is also passed into callbacks.
|
361
|
-
def callback(type)
|
386
|
+
def callback(type, context = {})
|
387
|
+
context = self.context.merge(context)
|
388
|
+
|
362
389
|
machine.callbacks[type].each do |callback|
|
363
390
|
callback.call(object, context, self)
|
364
391
|
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
2
|
|
3
|
-
class
|
3
|
+
class AssertionsBaseTest < Test::Unit::TestCase
|
4
4
|
include StateMachine::Assertions
|
5
5
|
|
6
6
|
def default_test
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
-
class AssertValidKeysTest <
|
10
|
+
class AssertValidKeysTest < AssertionsBaseTest
|
11
11
|
def test_should_not_raise_exception_if_key_is_valid
|
12
12
|
assert_nothing_raised { assert_valid_keys({:name => 'foo', :value => 'bar'}, :name, :value, :force) }
|
13
13
|
end
|
@@ -18,7 +18,7 @@ class AssertValidKeysTest < AssertionsTest
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
class AssertExclusiveKeysTest <
|
21
|
+
class AssertExclusiveKeysTest < AssertionsBaseTest
|
22
22
|
def test_should_not_raise_exception_if_no_keys_found
|
23
23
|
assert_nothing_raised { assert_exclusive_keys({:on => :park}, :only, :except) }
|
24
24
|
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
|
2
2
|
|
3
|
-
class
|
3
|
+
class EvalHelpersBaseTest < Test::Unit::TestCase
|
4
4
|
include StateMachine::EvalHelpers
|
5
5
|
|
6
|
+
def default_test
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class EvalHelpersTest < EvalHelpersBaseTest
|
6
11
|
def setup
|
7
12
|
@object = Object.new
|
8
13
|
end
|
@@ -13,9 +18,7 @@ class EvalHelpersTest < Test::Unit::TestCase
|
|
13
18
|
end
|
14
19
|
end
|
15
20
|
|
16
|
-
class EvalHelpersSymbolTest <
|
17
|
-
include StateMachine::EvalHelpers
|
18
|
-
|
21
|
+
class EvalHelpersSymbolTest < EvalHelpersBaseTest
|
19
22
|
def setup
|
20
23
|
class << (@object = Object.new)
|
21
24
|
def callback
|
@@ -29,9 +32,7 @@ class EvalHelpersSymbolTest < Test::Unit::TestCase
|
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
32
|
-
class EvalHelpersSymbolWithArgumentsTest <
|
33
|
-
include StateMachine::EvalHelpers
|
34
|
-
|
35
|
+
class EvalHelpersSymbolWithArgumentsTest < EvalHelpersBaseTest
|
35
36
|
def setup
|
36
37
|
class << (@object = Object.new)
|
37
38
|
def callback(*args)
|
@@ -45,9 +46,7 @@ class EvalHelpersSymbolWithArgumentsTest < Test::Unit::TestCase
|
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
|
-
class EvalHelpersSymbolTaintedMethodTest <
|
49
|
-
include StateMachine::EvalHelpers
|
50
|
-
|
49
|
+
class EvalHelpersSymbolTaintedMethodTest < EvalHelpersBaseTest
|
51
50
|
def setup
|
52
51
|
class << (@object = Object.new)
|
53
52
|
def callback
|
@@ -63,9 +62,7 @@ class EvalHelpersSymbolTaintedMethodTest < Test::Unit::TestCase
|
|
63
62
|
end
|
64
63
|
end
|
65
64
|
|
66
|
-
class EvalHelpersStringTest <
|
67
|
-
include StateMachine::EvalHelpers
|
68
|
-
|
65
|
+
class EvalHelpersStringTest < EvalHelpersBaseTest
|
69
66
|
def setup
|
70
67
|
@object = Object.new
|
71
68
|
end
|
@@ -84,9 +81,7 @@ class EvalHelpersStringTest < Test::Unit::TestCase
|
|
84
81
|
end
|
85
82
|
end
|
86
83
|
|
87
|
-
class EvalHelpersProcTest <
|
88
|
-
include StateMachine::EvalHelpers
|
89
|
-
|
84
|
+
class EvalHelpersProcTest < EvalHelpersBaseTest
|
90
85
|
def setup
|
91
86
|
@object = Object.new
|
92
87
|
@proc = lambda {|obj| obj}
|
@@ -97,9 +92,7 @@ class EvalHelpersProcTest < Test::Unit::TestCase
|
|
97
92
|
end
|
98
93
|
end
|
99
94
|
|
100
|
-
class EvalHelpersProcWithoutArgumentsTest <
|
101
|
-
include StateMachine::EvalHelpers
|
102
|
-
|
95
|
+
class EvalHelpersProcWithoutArgumentsTest < EvalHelpersBaseTest
|
103
96
|
def setup
|
104
97
|
@object = Object.new
|
105
98
|
@proc = lambda {|*args| args}
|
@@ -115,9 +108,7 @@ class EvalHelpersProcWithoutArgumentsTest < Test::Unit::TestCase
|
|
115
108
|
end
|
116
109
|
end
|
117
110
|
|
118
|
-
class EvalHelpersProcWithArgumentsTest <
|
119
|
-
include StateMachine::EvalHelpers
|
120
|
-
|
111
|
+
class EvalHelpersProcWithArgumentsTest < EvalHelpersBaseTest
|
121
112
|
def setup
|
122
113
|
@object = Object.new
|
123
114
|
@proc = lambda {|*args| args}
|
@@ -81,6 +81,12 @@ class EventCollectionWithEventsWithTransitionsTest < Test::Unit::TestCase
|
|
81
81
|
object.state = 'idling'
|
82
82
|
assert_equal [], @events.transitions_for(object)
|
83
83
|
end
|
84
|
+
|
85
|
+
def test_should_filter_valid_transitions_for_an_object_if_requirements_specified
|
86
|
+
object = @klass.new
|
87
|
+
assert_equal [{:object => object, :attribute => :state, :event => :ignite, :from => 'stalled', :to => 'idling'}], @events.transitions_for(object, :from => :stalled).map {|transition| transition.attributes}
|
88
|
+
assert_equal [], @events.transitions_for(object, :from => :idling).map {|transition| transition.attributes}
|
89
|
+
end
|
84
90
|
end
|
85
91
|
|
86
92
|
class EventCollectionWithMultipleEventsTest < Test::Unit::TestCase
|
@@ -166,6 +172,13 @@ class EventCollectionAttributeWithMachineActionTest < Test::Unit::TestCase
|
|
166
172
|
|
167
173
|
assert_instance_of StateMachine::Transition, @events.attribute_transition_for(@object)
|
168
174
|
end
|
175
|
+
|
176
|
+
def test_should_have_valid_transition_if_already_defined_in_transition_cache
|
177
|
+
@object.state_event = nil
|
178
|
+
@object.send(:state_event_transition=, transition = @ignite.transition_for(@object))
|
179
|
+
|
180
|
+
assert_equal transition, @events.attribute_transition_for(@object)
|
181
|
+
end
|
169
182
|
end
|
170
183
|
|
171
184
|
class EventCollectionAttributeWithNamespacedMachineTest < Test::Unit::TestCase
|
@@ -249,6 +262,17 @@ class EventCollectionWithValidationsTest < Test::Unit::TestCase
|
|
249
262
|
assert_equal ['cannot transition when idling'], @object.errors
|
250
263
|
end
|
251
264
|
|
265
|
+
def test_should_invalidate_with_friendly_name_if_invalid_event_specified
|
266
|
+
# Add a valid nil state
|
267
|
+
@machine.state nil
|
268
|
+
|
269
|
+
@object.state = nil
|
270
|
+
@object.state_event = 'ignite'
|
271
|
+
@events.attribute_transition_for(@object, true)
|
272
|
+
|
273
|
+
assert_equal ['cannot transition when nil'], @object.errors
|
274
|
+
end
|
275
|
+
|
252
276
|
def test_should_not_invalidate_event_can_be_fired
|
253
277
|
@ignite.transition :parked => :idling
|
254
278
|
@object.state_event = 'ignite'
|
data/test/unit/event_test.rb
CHANGED
@@ -201,6 +201,12 @@ class EventTransitionsTest < Test::Unit::TestCase
|
|
201
201
|
assert_equal 'Invalid key(s): on', exception.message
|
202
202
|
end
|
203
203
|
|
204
|
+
def test_should_automatically_set_on_option
|
205
|
+
guard = @event.transition(:to => :idling)
|
206
|
+
assert_instance_of StateMachine::WhitelistMatcher, guard.event_requirement
|
207
|
+
assert_equal [:ignite], guard.event_requirement.values
|
208
|
+
end
|
209
|
+
|
204
210
|
def test_should_not_allow_except_to_option
|
205
211
|
exception = assert_raise(ArgumentError) {@event.transition(:except_to => :parked)}
|
206
212
|
assert_equal 'Invalid key(s): except_to', exception.message
|
@@ -542,6 +548,7 @@ class EventWithMultipleTransitionsTest < Test::Unit::TestCase
|
|
542
548
|
@event = StateMachine::Event.new(@machine, :ignite)
|
543
549
|
@event.transition(:idling => :idling)
|
544
550
|
@event.transition(:parked => :idling) # This one should get used
|
551
|
+
@event.transition(:parked => :parked)
|
545
552
|
|
546
553
|
@object = @klass.new
|
547
554
|
@object.state = 'parked'
|
@@ -559,6 +566,32 @@ class EventWithMultipleTransitionsTest < Test::Unit::TestCase
|
|
559
566
|
assert_equal :ignite, transition.event
|
560
567
|
end
|
561
568
|
|
569
|
+
def test_should_allow_specific_transition_selection_using_from
|
570
|
+
transition = @event.transition_for(@object, :from => :idling)
|
571
|
+
|
572
|
+
assert_not_nil transition
|
573
|
+
assert_equal 'idling', transition.from
|
574
|
+
assert_equal 'idling', transition.to
|
575
|
+
assert_equal :ignite, transition.event
|
576
|
+
end
|
577
|
+
|
578
|
+
def test_should_allow_specific_transition_selection_using_to
|
579
|
+
transition = @event.transition_for(@object, :from => :parked, :to => :parked)
|
580
|
+
|
581
|
+
assert_not_nil transition
|
582
|
+
assert_equal 'parked', transition.from
|
583
|
+
assert_equal 'parked', transition.to
|
584
|
+
assert_equal :ignite, transition.event
|
585
|
+
end
|
586
|
+
|
587
|
+
def test_should_allow_specific_transition_selection_using_on
|
588
|
+
transition = @event.transition_for(@object, :on => :park)
|
589
|
+
assert_nil transition
|
590
|
+
|
591
|
+
transition = @event.transition_for(@object, :on => :ignite)
|
592
|
+
assert_not_nil transition
|
593
|
+
end
|
594
|
+
|
562
595
|
def test_should_fire
|
563
596
|
assert @event.fire(@object)
|
564
597
|
end
|
@@ -569,6 +602,67 @@ class EventWithMultipleTransitionsTest < Test::Unit::TestCase
|
|
569
602
|
end
|
570
603
|
end
|
571
604
|
|
605
|
+
class EventWithMachineActionTest < Test::Unit::TestCase
|
606
|
+
def setup
|
607
|
+
@klass = Class.new do
|
608
|
+
attr_reader :saved
|
609
|
+
|
610
|
+
def save
|
611
|
+
@saved = true
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
@machine = StateMachine::Machine.new(@klass, :action => :save)
|
616
|
+
@machine.state :parked, :idling
|
617
|
+
|
618
|
+
@machine.events << @event = StateMachine::Event.new(@machine, :ignite)
|
619
|
+
@event.transition(:parked => :idling)
|
620
|
+
|
621
|
+
@object = @klass.new
|
622
|
+
@object.state = 'parked'
|
623
|
+
end
|
624
|
+
|
625
|
+
def test_should_run_action_on_fire
|
626
|
+
@event.fire(@object)
|
627
|
+
assert @object.saved
|
628
|
+
end
|
629
|
+
|
630
|
+
def test_should_not_run_action_if_configured_to_skip
|
631
|
+
@event.fire(@object, false)
|
632
|
+
assert !@object.saved
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
class EventWithInvalidCurrentStateTest < Test::Unit::TestCase
|
637
|
+
def setup
|
638
|
+
@klass = Class.new
|
639
|
+
@machine = StateMachine::Machine.new(@klass)
|
640
|
+
@machine.state :parked, :idling
|
641
|
+
@machine.event :ignite
|
642
|
+
|
643
|
+
@event = StateMachine::Event.new(@machine, :ignite)
|
644
|
+
@event.transition(:parked => :idling)
|
645
|
+
|
646
|
+
@object = @klass.new
|
647
|
+
@object.state = 'invalid'
|
648
|
+
end
|
649
|
+
|
650
|
+
def test_should_raise_exception_when_checking_availability
|
651
|
+
exception = assert_raise(ArgumentError) { @event.can_fire?(@object) }
|
652
|
+
assert_equal '"invalid" is not a known state value', exception.message
|
653
|
+
end
|
654
|
+
|
655
|
+
def test_should_raise_exception_when_finding_transition
|
656
|
+
exception = assert_raise(ArgumentError) { @event.transition_for(@object) }
|
657
|
+
assert_equal '"invalid" is not a known state value', exception.message
|
658
|
+
end
|
659
|
+
|
660
|
+
def test_should_raise_exception_when_firing
|
661
|
+
exception = assert_raise(ArgumentError) { @event.fire(@object) }
|
662
|
+
assert_equal '"invalid" is not a known state value', exception.message
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
572
666
|
begin
|
573
667
|
# Load library
|
574
668
|
require 'rubygems'
|