diff --git a/activemodel/lib/active_model/state_machine/event.rb b/activemodel/lib/active_model/state_machine/event.rb index cc7d5632149d1..ea4df343de26c 100644 --- a/activemodel/lib/active_model/state_machine/event.rb +++ b/activemodel/lib/active_model/state_machine/event.rb @@ -3,9 +3,8 @@ module StateMachine class Event attr_reader :name, :success - def initialize(name, options = {}, &block) - @name, @transitions = name, [] - machine = options.delete(:machine) + def initialize(machine, name, options = {}, &block) + @machine, @name, @transitions = machine, name, [] if machine machine.klass.send(:define_method, "#{name.to_s}!") do |*args| machine.fire_event(name, self, true, *args) @@ -19,7 +18,7 @@ def initialize(name, options = {}, &block) end def fire(obj, to_state = nil, *args) - transitions = @transitions.select { |t| t.from == obj.current_state } + transitions = @transitions.select { |t| t.from == obj.current_state(@machine ? @machine.name : nil) } raise InvalidTransition if transitions.size == 0 next_state = nil diff --git a/activemodel/lib/active_model/state_machine/machine.rb b/activemodel/lib/active_model/state_machine/machine.rb index 1da48290c5cd5..53ce71794f7b7 100644 --- a/activemodel/lib/active_model/state_machine/machine.rb +++ b/activemodel/lib/active_model/state_machine/machine.rb @@ -19,12 +19,21 @@ def update(options = {}, &block) self end - def fire_event(name, record, persist, *args) - state_index[record.current_state].call_action(:exit, record) - if new_state = @events[name].fire(record, *args) + def fire_event(event, record, persist, *args) + state_index[record.current_state(@name)].call_action(:exit, record) + if new_state = @events[event].fire(record, *args) state_index[new_state].call_action(:enter, record) + + if record.respond_to?(event_fired_callback) + record.send(event_fired_callback, record.current_state, new_state) + end + record.current_state(@name, new_state) else + if record.respond_to?(event_failed_callback) + record.send(event_failed_callback, event) + end + false end end @@ -37,13 +46,22 @@ def events_for(state) events = @events.values.select { |event| event.transitions_from_state?(state) } events.map! { |event| event.name } end + private def state(name, options = {}) @states << (state_index[name] ||= State.new(name, :machine => self)).update(options) end def event(name, options = {}, &block) - (@events[name] ||= Event.new(name, :machine => self)).update(options, &block) + (@events[name] ||= Event.new(self, name)).update(options, &block) + end + + def event_fired_callback + @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired' + end + + def event_failed_callback + @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed' end end end diff --git a/activemodel/test/state_machine/event_test.rb b/activemodel/test/state_machine/event_test.rb index 01f3464cf283a..7db4f8d8876e8 100644 --- a/activemodel/test/state_machine/event_test.rb +++ b/activemodel/test/state_machine/event_test.rb @@ -7,7 +7,7 @@ def setup end def new_event - @event = ActiveModel::StateMachine::Event.new(@name, {:success => @success}) do + @event = ActiveModel::StateMachine::Event.new(nil, @name, {:success => @success}) do transitions :to => :closed, :from => [:open, :received] end end @@ -31,7 +31,7 @@ def new_event class EventBeingFiredTest < ActiveModel::TestCase test 'should raise an AASM::InvalidTransition error if the transitions are empty' do - event = ActiveModel::StateMachine::Event.new(:event) + event = ActiveModel::StateMachine::Event.new(nil, :event) assert_raises ActiveModel::StateMachine::InvalidTransition do event.fire(nil) @@ -39,7 +39,7 @@ class EventBeingFiredTest < ActiveModel::TestCase end test 'should return the state of the first matching transition it finds' do - event = ActiveModel::StateMachine::Event.new(:event) do + event = ActiveModel::StateMachine::Event.new(nil, :event) do transitions :to => :closed, :from => [:open, :received] end diff --git a/activemodel/test/state_machine/machine_test.rb b/activemodel/test/state_machine/machine_test.rb index 64dea42b1f2f2..2cdfcd9554dc3 100644 --- a/activemodel/test/state_machine/machine_test.rb +++ b/activemodel/test/state_machine/machine_test.rb @@ -36,6 +36,8 @@ class StateMachineMachineTest < ActiveModel::TestCase end test "finds events for given state" do - assert_equal [:shutdown, :timeout], MachineTestSubject.state_machine.events_for(:open) + events = MachineTestSubject.state_machine.events_for(:open) + assert events.include?(:shutdown) + assert events.include?(:timeout) end end \ No newline at end of file diff --git a/activemodel/test/state_machine_test.rb b/activemodel/test/state_machine_test.rb index 963ce84248142..2f08b522d953f 100644 --- a/activemodel/test/state_machine_test.rb +++ b/activemodel/test/state_machine_test.rb @@ -186,48 +186,50 @@ def subj.aasm_write_state # foo.aasm_current_state # end #end - -#describe AASM, '- event callbacks' do -# it 'should call aasm_event_fired if defined and successful for bang fire' do -# foo = Foo.new -# def foo.aasm_event_fired(from, to) -# end -# -# foo.should_receive(:aasm_event_fired) -# -# foo.close! -# end -# -# it 'should call aasm_event_fired if defined and successful for non-bang fire' do -# foo = Foo.new -# def foo.aasm_event_fired(from, to) -# end -# -# foo.should_receive(:aasm_event_fired) -# -# foo.close -# end -# -# it 'should call aasm_event_failed if defined and transition failed for bang fire' do -# foo = Foo.new -# def foo.aasm_event_failed(event) -# end -# -# foo.should_receive(:aasm_event_failed) -# -# foo.null! -# end -# -# it 'should call aasm_event_failed if defined and transition failed for non-bang fire' do -# foo = Foo.new -# def foo.aasm_event_failed(event) -# end -# -# foo.should_receive(:aasm_event_failed) -# -# foo.null -# end -#end + +uses_mocha 'StateMachineEventCallbacksTest' do + class StateMachineEventCallbacksTest < ActiveModel::TestCase + test 'should call aasm_event_fired if defined and successful for bang fire' do + subj = StateMachineSubject.new + def subj.aasm_event_fired(from, to) + end + + subj.expects(:event_fired) + + subj.close! + end + + test 'should call aasm_event_fired if defined and successful for non-bang fire' do + subj = StateMachineSubject.new + def subj.aasm_event_fired(from, to) + end + + subj.expects(:event_fired) + + subj.close + end + + test 'should call aasm_event_failed if defined and transition failed for bang fire' do + subj = StateMachineSubject.new + def subj.event_failed(event) + end + + subj.expects(:event_failed) + + subj.null! + end + + test 'should call aasm_event_failed if defined and transition failed for non-bang fire' do + subj = StateMachineSubject.new + def subj.aasm_event_failed(event) + end + + subj.expects(:event_failed) + + subj.null + end + end +end uses_mocha 'StateMachineStateActionsTest' do class StateMachineStateActionsTest < ActiveModel::TestCase @@ -254,72 +256,70 @@ class StateMachineInheritanceTest < ActiveModel::TestCase assert_equal StateMachineSubject.state_machine.events, StateMachineSubjectSubclass.state_machine.events end end -# -# -#class ChetanPatil -# include AASM -# aasm_initial_state :sleeping -# aasm_state :sleeping -# aasm_state :showering -# aasm_state :working -# aasm_state :dating -# -# aasm_event :wakeup do -# transitions :from => :sleeping, :to => [:showering, :working] -# end -# -# aasm_event :dress do -# transitions :from => :sleeping, :to => :working, :on_transition => :wear_clothes -# transitions :from => :showering, :to => [:working, :dating], :on_transition => Proc.new { |obj, *args| obj.wear_clothes(*args) } -# end -# -# def wear_clothes(shirt_color, trouser_type) -# end -#end -# -# -#describe ChetanPatil do -# it 'should transition to specified next state (sleeping to showering)' do -# cp = ChetanPatil.new -# cp.wakeup! :showering -# -# cp.aasm_current_state.should == :showering -# end -# -# it 'should transition to specified next state (sleeping to working)' do -# cp = ChetanPatil.new -# cp.wakeup! :working -# -# cp.aasm_current_state.should == :working -# end -# -# it 'should transition to default (first or showering) state' do -# cp = ChetanPatil.new -# cp.wakeup! -# -# cp.aasm_current_state.should == :showering -# end -# -# it 'should transition to default state when on_transition invoked' do -# cp = ChetanPatil.new -# cp.dress!(nil, 'purple', 'dressy') -# -# cp.aasm_current_state.should == :working -# end -# -# it 'should call on_transition method with args' do -# cp = ChetanPatil.new -# cp.wakeup! :showering -# -# cp.should_receive(:wear_clothes).with('blue', 'jeans') -# cp.dress! :working, 'blue', 'jeans' -# end -# -# it 'should call on_transition proc' do -# cp = ChetanPatil.new -# cp.wakeup! :showering -# -# cp.should_receive(:wear_clothes).with('purple', 'slacks') -# cp.dress!(:dating, 'purple', 'slacks') -# end -#end \ No newline at end of file + +class StateMachineSubject + state_machine :chetan_patil, :initial => :sleeping do + state :sleeping + state :showering + state :working + state :dating + + event :wakeup do + transitions :from => :sleeping, :to => [:showering, :working] + end + + event :dress do + transitions :from => :sleeping, :to => :working, :on_transition => :wear_clothes + transitions :from => :showering, :to => [:working, :dating], :on_transition => Proc.new { |obj, *args| obj.wear_clothes(*args) } + end + end + + def wear_clothes(shirt_color, trouser_type) + end +end + +class StateMachineWithComplexTransitionsTest < ActiveModel::TestCase + def setup + @subj = StateMachineSubject.new + end + + test 'transitions to specified next state (sleeping to showering)' do + @subj.wakeup! :showering + + assert_equal :showering, @subj.current_state(:chetan_patil) + end + + test 'transitions to specified next state (sleeping to working)' do + @subj.wakeup! :working + + assert_equal :working, @subj.current_state(:chetan_patil) + end + + test 'transitions to default (first or showering) state' do + @subj.wakeup! + + assert_equal :showering, @subj.current_state(:chetan_patil) + end + + test 'transitions to default state when on_transition invoked' do + @subj.dress!(nil, 'purple', 'dressy') + + assert_equal :working, @subj.current_state(:chetan_patil) + end + + uses_mocha "StateMachineWithComplexTransitionsTest on_transition tests" do + test 'calls on_transition method with args' do + @subj.wakeup! :showering + + @subj.expects(:wear_clothes).with('blue', 'jeans') + @subj.dress! :working, 'blue', 'jeans' + end + + test 'calls on_transition proc' do + @subj.wakeup! :showering + + @subj.expects(:wear_clothes).with('purple', 'slacks') + @subj.dress!(:dating, 'purple', 'slacks') + end + end +end \ No newline at end of file