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.
@@ -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
- value = machine.read(object, :state)
10
- machine.write(object, :state, machine.initial_state(object).value) if value.nil? || value.respond_to?(:empty?) && value.empty?
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. After transition callbacks can be
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, Integer, :serial => true
61
+ # property :id, Serial
60
62
  #
61
63
  # state_machine :initial => :parked do
62
64
  # event :ignite do
@@ -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} in state #{machine.states.match(object).name.inspect}"
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
- # Always run after callbacks regardless of whether the actions failed
68
- transitions.each {|transition| transition.after(results[transition.action])} unless options[:after] == false
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
- callback(:before)
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
- machine.write(object, :state, to)
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 is used to indicate whether the associated machine action
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
- callback(:after)
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 AssertionsTest < Test::Unit::TestCase
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 < AssertionsTest
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 < AssertionsTest
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 EvalHelpersTest < Test::Unit::TestCase
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 < Test::Unit::TestCase
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 < Test::Unit::TestCase
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 < Test::Unit::TestCase
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 < Test::Unit::TestCase
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 < Test::Unit::TestCase
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 < Test::Unit::TestCase
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 < Test::Unit::TestCase
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'
@@ -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'