Skip to content

Commit

Permalink
Allow multiple whitelisted / blacklisted :to states when definining t…
Browse files Browse the repository at this point in the history
…ransitions. Closes pluginaweek#197
  • Loading branch information
obrie committed Mar 30, 2013
1 parent 8443093 commit c80508b
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 7 additions & 2 deletions lib/state_machine/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
159 changes: 153 additions & 6 deletions test/unit/event_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 4 additions & 5 deletions test/unit/machine_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit c80508b

Please sign in to comment.