Skip to content

Commit

Permalink
Add :except_from option for transitions if you want to blacklist states
Browse files Browse the repository at this point in the history
  • Loading branch information
obrie committed Jul 3, 2008
1 parent 26d60af commit 7f1b4d6
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rdoc
@@ -1,5 +1,6 @@
== master

* Add :except_from option for transitions if you want to blacklist states
* Add PluginAWeek::StateMachine::Machine#states
* Add PluginAWeek::StateMachine::Event#transitions
* Allow creating transitions with no from state (effectively allowing the transition for *any* from state)
Expand Down
17 changes: 7 additions & 10 deletions lib/state_machine/event.rb
Expand Up @@ -36,6 +36,7 @@ def initialize(machine, name, options = {})
# Configuration options:
# * +to+ - The state that being transitioned to
# * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state
# * +except_from+ - A state or array of states that *cannot* be transitioned from.
# * +if+ - Specifies a method, proc or string to call to determine if the validation should occur (e.g. :if => :moving?, or :if => Proc.new {|car| car.speed > 60}). The method, proc or string should return or evaluate to a true or false value.
# * +unless+ - Specifies a method, proc or string to call to determine if the transition should not occur (e.g. :unless => :stopped?, or :unless => Proc.new {|car| car.speed <= 60}). The method, proc or string should return or evaluate to a true or false value.
#
Expand All @@ -46,26 +47,22 @@ def initialize(machine, name, options = {})
# transition :to => 'parked', :from => %w(first_gear reverse)
# transition :to => 'parked', :from => 'first_gear', :if => :moving?
# transition :to => 'parked', :from => 'first_gear', :unless => :stopped?
# transition :to => 'parked', :except_from => 'parked'
def transition(options = {})
# Slice out the callback options
options.symbolize_keys!
options.assert_valid_keys(:to, :from, :if, :unless)
raise ArgumentError, ':to state must be specified' unless options.include?(:to)
callback_options = {:if => options.delete(:if), :unless => options.delete(:unless)}

# Get the states involved in the transition
to_state = options.delete(:to)
from_states = Array(options.delete(:from))

# Create the actual transition that will update records when performed
transition = Transition.new(self, to_state, *from_states)
transition = Transition.new(self, options)

# Add the callback to the model. If the callback fails, then the next
# available callback for the event will run until one is successful.
callback = Proc.new {|record, *args| try_transition(transition, false, record, *args)}
owner_class.send("transition_on_#{name}", callback, options)
owner_class.send("transition_on_#{name}", callback, callback_options)

# Add the callback! to the model similar to above
callback = Proc.new {|record, *args| try_transition(transition, true, record, *args)}
owner_class.send("transition_bang_on_#{name}", callback, options)
owner_class.send("transition_bang_on_#{name}", callback, callback_options)

transitions << transition
transition
Expand Down
16 changes: 12 additions & 4 deletions lib/state_machine/transition.rb
Expand Up @@ -23,10 +23,18 @@ class Transition
delegate :machine,
:to => :event

def initialize(event, to_state, *from_states) #:nodoc:
def initialize(event, options) #:nodoc:
@event = event
@to_state = to_state
@from_states = from_states

options.assert_valid_keys(:to, :from, :except_from)
raise ArgumentError, ':to state must be specified' unless options.include?(:to)

# Get the states involved in the transition
@to_state = options[:to]
@from_states = Array(options[:from] || options[:except_from])

# Should we be matching the from states?
@require_match = !options[:from].nil?
end

# Whether or not this is a loopback transition (i.e. from and to state are the same)
Expand All @@ -37,7 +45,7 @@ def loopback?(from_state)
# Determines whether or not this transition can be performed on the given
# states
def can_perform_on?(record)
from_states.empty? || from_states.include?(record.send(machine.attribute))
from_states.empty? || from_states.include?(record.send(machine.attribute)) == @require_match
end

# Runs the actual transition and any callbacks associated with entering
Expand Down
58 changes: 49 additions & 9 deletions test/unit/transition_test.rb
Expand Up @@ -4,7 +4,7 @@ class TransitionTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on')
end

def test_should_not_have_any_from_states
Expand Down Expand Up @@ -66,13 +66,21 @@ def test_should_raise_exception_if_not_saved_during_perform!

assert_raise(ActiveRecord::RecordNotSaved) {@transition.perform!(record)}
end

def test_should_raise_exception_if_invalid_option_specified
assert_raise(ArgumentError) {PluginAWeek::StateMachine::Transition.new(@event, :invalid => true)}
end

def test_should_raise_exception_if_to_option_not_specified
assert_raise(ArgumentError) {PluginAWeek::StateMachine::Transition.new(@event, :from => 'off')}
end
end

class TransitionWithLoopbackTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'on')
end

def test_should_be_able_to_perform
Expand All @@ -90,7 +98,7 @@ class TransitionWithFromStateTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'off')
end

def test_should_have_a_from_state
Expand All @@ -117,7 +125,7 @@ class TransitionWithMultipleFromStatesTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off', 'on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => %w(off on))
end

def test_should_have_multiple_from_states
Expand All @@ -142,15 +150,47 @@ def test_should_perform_for_any_valid_from_state
assert @transition.perform(record)

record = new_switch(:state => 'on')
assert @transition.perform(record)
end
end

class TransitionWithMismatchedFromStatesRequiredTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :except_from => 'on')
end

def test_should_have_a_from_state
assert_equal ['on'], @transition.from_states
end

def test_should_be_able_to_perform_if_record_state_is_not_from_state
record = new_switch(:state => 'off')
assert @transition.can_perform_on?(record)
end

def test_should_not_be_able_to_perform_if_record_state_is_from_state
record = new_switch(:state => 'on')
assert !@transition.can_perform_on?(record)
end

def test_should_perform_for_valid_from_state
record = new_switch(:state => 'off')
assert @transition.perform(record)
end

def test_should_not_perform_for_invalid_from_state
record = new_switch(:state => 'on')
assert !@transition.can_perform_on?(record)
end
end

class TransitionAfterBeingPerformedTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'off')

@record = create_switch(:state => 'off')
@transition.perform(@record)
Expand All @@ -170,7 +210,7 @@ class TransitionWithLoopbackAfterBeingPerformedTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'on')

@record = create_switch(:state => 'on')
@transition.perform(@record)
Expand All @@ -190,7 +230,7 @@ class TransitionWithCallbacksTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'off')
@record = create_switch(:state => 'off')

Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
Expand Down Expand Up @@ -323,7 +363,7 @@ class TransitionWithoutFromStateAndCallbacksTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on')
@record = create_switch(:state => 'off')

Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
Expand Down Expand Up @@ -425,7 +465,7 @@ class TransitionWithLoopbackAndCallbacksTest < Test::Unit::TestCase
def setup
@machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
@event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
@transition = PluginAWeek::StateMachine::Transition.new(@event, :to => 'on', :from => 'on')
@record = create_switch(:state => 'on')

Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
Expand Down

0 comments on commit 7f1b4d6

Please sign in to comment.