state_machine 0.7.6 → 0.8.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 +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'
|