Permalink
Browse files

Allow multiple whitelisted / blacklisted :to states when definining t…

…ransitions. Closes #197
  • Loading branch information...
1 parent 8443093 commit c80508be046fa28c1e3882ba9631693d53b9e3f3 @obrie obrie committed Mar 30, 2013
Showing with 165 additions and 13 deletions.
  1. +1 −0 CHANGELOG.md
  2. +7 −2 lib/state_machine/event.rb
  3. +153 −6 test/unit/event_test.rb
  4. +4 −5 test/unit/machine_test.rb
View
@@ -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
@@ -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
View
@@ -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,13 +393,21 @@ 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
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
@@ -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

0 comments on commit c80508b

Please sign in to comment.