diff --git a/CHANGELOG.md b/CHANGELOG.md index 12be876e..38201993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # master +* Allow multiple whitelisted / blacklisted :to states when definining transitions * Fix event attributes not being processed when saved via association autosaving * Fix Mongoid integration not setting initial state attributes properly for associations * Completely rewrite ORM action hooks to behave more consistently across the board diff --git a/lib/state_machine/event.rb b/lib/state_machine/event.rb index 8552d672..9024e13a 100644 --- a/lib/state_machine/event.rb +++ b/lib/state_machine/event.rb @@ -107,7 +107,7 @@ def transition(options) # Only a certain subset of explicit options are allowed for transition # requirements - 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? + assert_valid_keys(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty? branches << branch = Branch.new(options.merge(:on => name)) @known_states |= branch.known_states @@ -144,7 +144,12 @@ def transition_for(object, requirements = {}) if match = branch.match(object, requirements) # Branch allows for the transition to occur from = requirements[:from] - to = match[:to].values.empty? ? from : match[:to].values.first + to = if match[:to].is_a?(LoopbackMatcher) + from + else + values = requirements.include?(:to) ? [requirements[:to]].flatten : [from] | machine.states.keys(:name) + match[:to].filter(values).first + end return Transition.new(object, machine, name, from, to, !custom_from_state) end diff --git a/test/unit/event_test.rb b/test/unit/event_test.rb index aa1b14d6..1bf92cc4 100644 --- a/test/unit/event_test.rb +++ b/test/unit/event_test.rb @@ -376,11 +376,6 @@ def test_should_automatically_set_on_option assert_equal [:ignite], branch.event_requirement.values end - def test_should_not_allow_except_to_option - exception = assert_raise(ArgumentError) {@event.transition(:except_to => :parked)} - assert_equal 'Invalid key(s): except_to', exception.message - end - def test_should_not_allow_except_on_option exception = assert_raise(ArgumentError) {@event.transition(:except_on => :ignite)} assert_equal 'Invalid key(s): except_on', exception.message @@ -398,6 +393,10 @@ def test_should_allow_except_from_option assert_nothing_raised {@event.transition(:except_from => :idling)} end + def test_should_allow_except_to_option + assert_nothing_raised {@event.transition(:except_to => :idling)} + end + def test_should_allow_transitioning_from_a_single_state assert @event.transition(:parked => :idling) end @@ -405,6 +404,10 @@ def test_should_allow_transitioning_from_a_single_state def test_should_allow_transitioning_from_multiple_states assert @event.transition([:parked, :idling] => :idling) end + + def test_should_allow_transitions_to_multiple_states + assert @event.transition(:parked => [:parked, :idling]) + end def test_should_have_transitions branch = @event.transition(:to => :idling) @@ -775,6 +778,150 @@ def test_should_not_change_the_current_state end end +class EventWithTransitionWithLoopbackStateTest < Test::Unit::TestCase + def setup + @klass = Class.new + @machine = StateMachine::Machine.new(@klass) + @machine.state :parked + + @machine.events << @event = StateMachine::Event.new(@machine, :park) + @event.transition(:from => :parked, :to => StateMachine::LoopbackMatcher.instance) + + @object = @klass.new + @object.state = 'parked' + end + + def test_should_be_able_to_fire + assert @event.can_fire?(@object) + end + + def test_should_have_a_transition + transition = @event.transition_for(@object) + assert_not_nil transition + assert_equal 'parked', transition.from + assert_equal 'parked', transition.to + assert_equal :park, transition.event + end + + def test_should_fire + assert @event.fire(@object) + end + + def test_should_not_change_the_current_state + @event.fire(@object) + assert_equal 'parked', @object.state + end +end + +class EventWithTransitionWithBlacklistedToStateTest < Test::Unit::TestCase + def setup + @klass = Class.new + @machine = StateMachine::Machine.new(@klass, :initial => :parked) + @machine.state :parked, :idling, :first_gear, :second_gear + + @machine.events << @event = StateMachine::Event.new(@machine, :ignite) + @event.transition(:from => :parked, :to => StateMachine::BlacklistMatcher.new([:parked, :idling])) + + @object = @klass.new + @object.state = 'parked' + end + + def test_should_be_able_to_fire + assert @event.can_fire?(@object) + end + + def test_should_have_a_transition + transition = @event.transition_for(@object) + assert_not_nil transition + assert_equal 'parked', transition.from + assert_equal 'first_gear', transition.to + assert_equal :ignite, transition.event + end + + def test_should_allow_loopback_first_when_possible + @event.transition(:from => :second_gear, :to => StateMachine::BlacklistMatcher.new([:parked, :idling])) + @object.state = 'second_gear' + + transition = @event.transition_for(@object) + assert_not_nil transition + assert_equal 'second_gear', transition.from + assert_equal 'second_gear', transition.to + assert_equal :ignite, transition.event + end + + def test_should_allow_specific_transition_selection_using_to + transition = @event.transition_for(@object, :from => :parked, :to => :second_gear) + + assert_not_nil transition + assert_equal 'parked', transition.from + assert_equal 'second_gear', transition.to + assert_equal :ignite, transition.event + end + + def test_should_not_allow_transition_selection_if_not_matching + transition = @event.transition_for(@object, :from => :parked, :to => :parked) + assert_nil transition + end + + def test_should_fire + assert @event.fire(@object) + end + + def test_should_change_the_current_state + @event.fire(@object) + assert_equal 'first_gear', @object.state + end +end + +class EventWithTransitionWithWhitelistedToStateTest < Test::Unit::TestCase + def setup + @klass = Class.new + @machine = StateMachine::Machine.new(@klass, :initial => :parked) + @machine.state :parked, :idling, :first_gear, :second_gear + + @machine.events << @event = StateMachine::Event.new(@machine, :ignite) + @event.transition(:from => :parked, :to => StateMachine::WhitelistMatcher.new([:first_gear, :second_gear])) + + @object = @klass.new + @object.state = 'parked' + end + + def test_should_be_able_to_fire + assert @event.can_fire?(@object) + end + + def test_should_have_a_transition + transition = @event.transition_for(@object) + assert_not_nil transition + assert_equal 'parked', transition.from + assert_equal 'first_gear', transition.to + assert_equal :ignite, transition.event + end + + def test_should_allow_specific_transition_selection_using_to + transition = @event.transition_for(@object, :from => :parked, :to => :second_gear) + + assert_not_nil transition + assert_equal 'parked', transition.from + assert_equal 'second_gear', transition.to + assert_equal :ignite, transition.event + end + + def test_should_not_allow_transition_selection_if_not_matching + transition = @event.transition_for(@object, :from => :parked, :to => :parked) + assert_nil transition + end + + def test_should_fire + assert @event.fire(@object) + end + + def test_should_change_the_current_state + @event.fire(@object) + assert_equal 'first_gear', @object.state + end +end + class EventWithMultipleTransitionsTest < Test::Unit::TestCase def setup @klass = Class.new @@ -783,7 +930,7 @@ def setup @machine.events << @event = StateMachine::Event.new(@machine, :ignite) @event.transition(:idling => :idling) - @event.transition(:parked => :idling) # This one should get used + @event.transition(:parked => :idling) @event.transition(:parked => :parked) @object = @klass.new diff --git a/test/unit/machine_test.rb b/test/unit/machine_test.rb index 0177637c..cb9e8edb 100644 --- a/test/unit/machine_test.rb +++ b/test/unit/machine_test.rb @@ -2505,11 +2505,6 @@ def test_should_require_on_event assert_equal 'Must specify :on event', exception.message end - def test_should_not_allow_except_to_option - exception = assert_raise(ArgumentError) {@machine.transition(:except_to => :parked, :on => :ignite)} - assert_equal 'Invalid key(s): except_to', exception.message - end - def test_should_not_allow_except_on_option exception = assert_raise(ArgumentError) {@machine.transition(:except_on => :ignite, :on => :ignite)} assert_equal 'Invalid key(s): except_on', exception.message @@ -2527,6 +2522,10 @@ def test_should_allow_except_from_option assert_nothing_raised {@machine.transition(:except_from => :idling, :on => :ignite)} end + def test_should_allow_except_to_option + assert_nothing_raised {@machine.transition(:except_to => :parked, :on => :ignite)} + end + def test_should_allow_implicit_options branch = @machine.transition(:first_gear => :second_gear, :on => :shift_up) assert_instance_of StateMachine::Branch, branch