提交 74cb0569 编写于 作者: R rick

add basic events and transitions. still more tests to convert

上级 b9528ad3
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
module ActiveModel module ActiveModel
module StateMachine module StateMachine
class InvalidTransition < Exception
end
def self.included(base) def self.included(base)
base.extend ClassMethods base.extend ClassMethods
end end
...@@ -34,10 +37,14 @@ def state_machine(name = nil, options = {}, &block) ...@@ -34,10 +37,14 @@ def state_machine(name = nil, options = {}, &block)
end end
end end
def current_state(name = nil) def current_state(name = nil, new_state = nil)
sm = self.class.state_machine(name) sm = self.class.state_machine(name)
ivar = "@#{sm.name}_current_state" ivar = "@#{sm.name}_current_state"
instance_variable_get(ivar) || instance_variable_set(ivar, sm.initial_state) if name && new_state
instance_variable_set(ivar, new_state)
else
instance_variable_get(ivar) || instance_variable_set(ivar, sm.initial_state)
end
end end
end end
end end
\ No newline at end of file
module ActiveModel
module StateMachine
class Event
attr_reader :name, :success
def initialize(name, options = {}, &block)
@name, @transitions = name, []
machine = options.delete(:machine)
if machine
machine.klass.send(:define_method, "#{name.to_s}!") do |*args|
machine.fire_event(name, self, true, *args)
end
machine.klass.send(:define_method, "#{name.to_s}") do |*args|
machine.fire_event(name, self, false, *args)
end
end
update(options, &block)
end
def fire(obj, to_state = nil, *args)
transitions = @transitions.select { |t| t.from == obj.current_state }
raise InvalidTransition if transitions.size == 0
next_state = nil
transitions.each do |transition|
next if to_state && !Array(transition.to).include?(to_state)
if transition.perform(obj)
next_state = to_state || Array(transition.to).first
transition.execute(obj, *args)
break
end
end
next_state
end
def transitions_from_state?(state)
@transitions.any? { |t| t.from? state }
end
def success?
!!@success
end
def ==(event)
if event.is_a? Symbol
name == event
else
name == event.name
end
end
def update(options = {}, &block)
if options.key?(:success) then @success = options[:success] end
if block then instance_eval(&block) end
self
end
private
def transitions(trans_opts)
Array(trans_opts[:from]).each do |s|
@transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
end
end
end
end
end
module ActiveModel module ActiveModel
module StateMachine module StateMachine
class Machine class Machine
attr_accessor :initial_state, :states, :event attr_accessor :initial_state, :states, :events, :state_index
attr_reader :klass, :name attr_reader :klass, :name
def initialize(klass, name) def initialize(klass, name, options = {}, &block)
@klass, @name, @states, @events = klass, name, [], {} @klass, @name, @states, @state_index, @events = klass, name, [], {}, {}
update(options, &block)
end
def initial_state
@initial_state ||= (states.first ? states.first.name : nil)
end
def update(options = {}, &block)
if options.key?(:initial) then @initial_state = options[:initial] end
if block then instance_eval(&block) end
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)
state_index[new_state].call_action(:enter, record)
record.current_state(@name, new_state)
else
false
end
end end
def states_for_select def states_for_select
states.map { |st| [st.display_name, st.name.to_s] } states.map { |st| [st.display_name, st.name.to_s] }
end end
def state(name, options = {}) def events_for(state)
@states << State.new(self, name, options) events = @events.values.select { |event| event.transitions_from_state?(state) }
events.map! { |event| event.name }
end end
private
def initial_state def state(name, options = {})
@initial_state ||= (states.first ? states.first.name : nil) @states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
end end
def update(options = {}, &block) def event(name, options = {}, &block)
@initial_state = options[:initial] (@events[name] ||= Event.new(name, :machine => self)).update(options, &block)
instance_eval(&block)
self
end end
end end
end end
......
...@@ -3,11 +3,15 @@ module StateMachine ...@@ -3,11 +3,15 @@ module StateMachine
class State class State
attr_reader :name, :options attr_reader :name, :options
def initialize(machine, name, options={}) def initialize(name, options = {})
@machine, @name, @options, @display_name = machine, name, options, options.delete(:display) @name = name
machine.klass.send(:define_method, "#{name}?") do machine = options.delete(:machine)
current_state.to_s == name.to_s if machine
machine.klass.send(:define_method, "#{name}?") do
current_state.to_s == name.to_s
end
end end
update(options)
end end
def ==(state) def ==(state)
...@@ -35,6 +39,12 @@ def display_name ...@@ -35,6 +39,12 @@ def display_name
def for_select def for_select
[display_name, name.to_s] [display_name, name.to_s]
end end
def update(options = {})
if options.key?(:display) then @display_name = options.delete(:display) end
@options = options
self
end
end end
end end
end end
module ActiveModel
module StateMachine
class StateTransition
attr_reader :from, :to, :options
def initialize(opts)
@from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
@options = opts
end
def perform(obj)
case @guard
when Symbol, String
obj.send(@guard)
when Proc
@guard.call(obj)
else
true
end
end
def execute(obj, *args)
case @on_transition
when Symbol, String
obj.send(@on_transition, *args)
when Proc
@on_transition.call(obj, *args)
end
end
def ==(obj)
@from == obj.from && @to == obj.to
end
def from?(value)
@from == value
end
end
end
end
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
class EventTest < ActiveModel::TestCase
def setup
@name = :close_order
@success = :success_callback
end
def new_event
@event = ActiveModel::StateMachine::Event.new(@name, {:success => @success}) do
transitions :to => :closed, :from => [:open, :received]
end
end
test 'should set the name' do
assert_equal @name, new_event.name
end
test 'should set the success option' do
assert new_event.success?
end
uses_mocha 'StateTransition creation' do
test 'should create StateTransitions' do
ActiveModel::StateMachine::StateTransition.expects(:new).with(:to => :closed, :from => :open)
ActiveModel::StateMachine::StateTransition.expects(:new).with(:to => :closed, :from => :received)
new_event
end
end
end
class EventBeingFiredTest < ActiveModel::TestCase
test 'should raise an AASM::InvalidTransition error if the transitions are empty' do
event = ActiveModel::StateMachine::Event.new(:event)
assert_raises ActiveModel::StateMachine::InvalidTransition do
event.fire(nil)
end
end
test 'should return the state of the first matching transition it finds' do
event = ActiveModel::StateMachine::Event.new(:event) do
transitions :to => :closed, :from => [:open, :received]
end
obj = stub
obj.stubs(:current_state).returns(:open)
assert_equal :closed, event.fire(obj)
end
end
...@@ -4,9 +4,18 @@ class MachineTestSubject ...@@ -4,9 +4,18 @@ class MachineTestSubject
include ActiveModel::StateMachine include ActiveModel::StateMachine
state_machine do state_machine do
state :open
state :closed
end end
state_machine :initial => :foo do state_machine :initial => :foo do
event :shutdown do
transitions :from => :open, :to => :closed
end
event :timeout do
transitions :from => :open, :to => :closed
end
end end
state_machine :extra, :initial => :bar do state_machine :extra, :initial => :bar do
...@@ -25,4 +34,8 @@ class StateMachineMachineTest < ActiveModel::TestCase ...@@ -25,4 +34,8 @@ class StateMachineMachineTest < ActiveModel::TestCase
test "accesses non-default state machine" do test "accesses non-default state machine" do
assert_kind_of ActiveModel::StateMachine::Machine, MachineTestSubject.state_machine(:extra) assert_kind_of ActiveModel::StateMachine::Machine, MachineTestSubject.state_machine(:extra)
end end
test "finds events for given state" do
assert_equal [:shutdown, :timeout], MachineTestSubject.state_machine.events_for(:open)
end
end end
\ No newline at end of file
...@@ -10,12 +10,12 @@ class StateTestSubject ...@@ -10,12 +10,12 @@ class StateTestSubject
class StateTest < ActiveModel::TestCase class StateTest < ActiveModel::TestCase
def setup def setup
@name = :astate @name = :astate
@options = { :crazy_custom_key => 'key' }
@machine = StateTestSubject.state_machine @machine = StateTestSubject.state_machine
@options = { :crazy_custom_key => 'key', :machine => @machine }
end end
def new_state(options={}) def new_state(options={})
ActiveModel::StateMachine::State.new(options.delete(:machine) || @machine, @name, @options.merge(options)) ActiveModel::StateMachine::State.new(@name, @options.merge(options))
end end
test 'sets the name' do test 'sets the name' do
...@@ -31,6 +31,7 @@ def new_state(options={}) ...@@ -31,6 +31,7 @@ def new_state(options={})
end end
test 'sets the options and expose them as options' do test 'sets the options and expose them as options' do
@options.delete(:machine)
assert_equal @options, new_state.options assert_equal @options, new_state.options
end end
......
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
class StateTransitionTest < ActiveModel::TestCase
test 'should set from, to, and opts attr readers' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = ActiveModel::StateMachine::StateTransition.new(opts)
assert_equal opts[:from], st.from
assert_equal opts[:to], st.to
assert_equal opts, st.options
end
uses_mocha 'checking ActiveModel StateMachine transitions' do
test 'should pass equality check if from and to are the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = ActiveModel::StateMachine::StateTransition.new(opts)
obj = stub
obj.stubs(:from).returns(opts[:from])
obj.stubs(:to).returns(opts[:to])
assert_equal st, obj
end
test 'should fail equality check if from are not the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = ActiveModel::StateMachine::StateTransition.new(opts)
obj = stub
obj.stubs(:from).returns('blah')
obj.stubs(:to).returns(opts[:to])
assert_not_equal st, obj
end
test 'should fail equality check if to are not the same' do
opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
st = ActiveModel::StateMachine::StateTransition.new(opts)
obj = stub
obj.stubs(:from).returns(opts[:from])
obj.stubs(:to).returns('blah')
assert_not_equal st, obj
end
end
end
class StateTransitionGuardCheckTest < ActiveModel::TestCase
test 'should return true of there is no guard' do
opts = {:from => 'foo', :to => 'bar'}
st = ActiveModel::StateMachine::StateTransition.new(opts)
assert st.perform(nil)
end
uses_mocha 'checking ActiveModel StateMachine transition guard checks' do
test 'should call the method on the object if guard is a symbol' do
opts = {:from => 'foo', :to => 'bar', :guard => :test_guard}
st = ActiveModel::StateMachine::StateTransition.new(opts)
obj = stub
obj.expects(:test_guard)
st.perform(obj)
end
test 'should call the method on the object if guard is a string' do
opts = {:from => 'foo', :to => 'bar', :guard => 'test_guard'}
st = ActiveModel::StateMachine::StateTransition.new(opts)
obj = stub
obj.expects(:test_guard)
st.perform(obj)
end
test 'should call the proc passing the object if the guard is a proc' do
opts = {:from => 'foo', :to => 'bar', :guard => Proc.new {|o| o.test_guard}}
st = ActiveModel::StateMachine::StateTransition.new(opts)
obj = stub
obj.expects(:test_guard)
st.perform(obj)
end
end
end
...@@ -7,22 +7,22 @@ class StateMachineSubject ...@@ -7,22 +7,22 @@ class StateMachineSubject
state :open, :exit => :exit state :open, :exit => :exit
state :closed, :enter => :enter state :closed, :enter => :enter
#event :close, :success => :success_callback do event :close, :success => :success_callback do
# transitions :to => :closed, :from => [:open] transitions :to => :closed, :from => [:open]
#end end
#
#event :null do event :null do
# transitions :to => :closed, :from => [:open], :guard => :always_false transitions :to => :closed, :from => [:open], :guard => :always_false
#end end
end end
state_machine :bar do state_machine :bar do
state :read state :read
state :ended state :ended
#event :foo do event :foo do
# transitions :to => :ended, :from => [:read] transitions :to => :ended, :from => [:read]
#end end
end end
def always_false def always_false
...@@ -80,9 +80,13 @@ def setup ...@@ -80,9 +80,13 @@ def setup
assert @foo.respond_to?(:open?) assert @foo.respond_to?(:open?)
end end
#test 'should define an event! inance method' do test 'defines an event! instance method' do
# assert @foo.respond_to?(:close!) assert @foo.respond_to?(:close!)
#end end
test 'defines an event instance method' do
assert @foo.respond_to?(:close)
end
end end
class StateMachineInitialStatesTest < ActiveModel::TestCase class StateMachineInitialStatesTest < ActiveModel::TestCase
...@@ -90,19 +94,19 @@ def setup ...@@ -90,19 +94,19 @@ def setup
@foo = StateMachineSubject.new @foo = StateMachineSubject.new
end end
test 'should set the initial state' do test 'sets the initial state' do
assert_equal :open, @foo.current_state assert_equal :open, @foo.current_state
end end
#test '#open? should be initially true' do test '#open? should be initially true' do
# @foo.open?.should be_true assert @foo.open?
#end end
#
#test '#closed? should be initially false' do test '#closed? should be initially false' do
# @foo.closed?.should be_false assert !@foo.closed?
#end end
test 'should use the first state defined if no initial state is given' do test 'uses the first state defined if no initial state is given' do
assert_equal :read, @foo.current_state(:bar) assert_equal :read, @foo.current_state(:bar)
end end
end end
...@@ -141,34 +145,36 @@ def setup ...@@ -141,34 +145,36 @@ def setup
# foo.close! # foo.close!
# end # end
#end #end
#
#describe AASM, '- event firing without persistence' do class StateMachineEventFiringWithoutPersistence < ActiveModel::TestCase
# it 'should fire the Event' do test 'updates the current state' do
# foo = Foo.new subj = StateMachineSubject.new
# assert_equal :open, subj.current_state
# Foo.aasm_events[:close].should_receive(:fire).with(foo) subj.close
# foo.close assert_equal :closed, subj.current_state
# end end
#
# it 'should update the current state' do uses_mocha 'StateMachineEventFiringWithoutPersistence' do
# foo = Foo.new test 'fires the Event' do
# foo.close subj = StateMachineSubject.new
#
# foo.aasm_current_state.should == :closed StateMachineSubject.state_machine.events[:close].expects(:fire).with(subj)
# end subj.close
# end
# it 'should attempt to persist if aasm_write_state is defined' do
# foo = Foo.new test 'should attempt to persist if aasm_write_state is defined' do
# subj = StateMachineSubject.new
# def foo.aasm_write_state
# end def subj.aasm_write_state
# end
# foo.should_receive(:aasm_write_state_without_persistence)
# subj.expects(:aasm_write_state_without_persistence)
# foo.close
# end subj.close
#end end
# end
end
#describe AASM, '- persistence' do #describe AASM, '- persistence' do
# it 'should read the state if it has not been set and aasm_read_state is defined' do # it 'should read the state if it has not been set and aasm_read_state is defined' do
# foo = Foo.new # foo = Foo.new
...@@ -180,22 +186,7 @@ def setup ...@@ -180,22 +186,7 @@ def setup
# foo.aasm_current_state # foo.aasm_current_state
# end # end
#end #end
#
#describe AASM, '- getting events for a state' do
# it '#aasm_events_for_current_state should use current state' do
# foo = Foo.new
# foo.should_receive(:aasm_current_state)
# foo.aasm_events_for_current_state
# end
#
# it '#aasm_events_for_current_state should use aasm_events_for_state' do
# foo = Foo.new
# foo.stub!(:aasm_current_state).and_return(:foo)
# foo.should_receive(:aasm_events_for_state).with(:foo)
# foo.aasm_events_for_current_state
# end
#end
#
#describe AASM, '- event callbacks' do #describe AASM, '- event callbacks' do
# it 'should call aasm_event_fired if defined and successful for bang fire' do # it 'should call aasm_event_fired if defined and successful for bang fire' do
# foo = Foo.new # foo = Foo.new
...@@ -237,32 +228,31 @@ def setup ...@@ -237,32 +228,31 @@ def setup
# foo.null # foo.null
# end # end
#end #end
#
#describe AASM, '- state actions' do uses_mocha 'StateMachineStateActionsTest' do
# it "should call enter when entering state" do class StateMachineStateActionsTest < ActiveModel::TestCase
# foo = Foo.new test "calls enter when entering state" do
# foo.should_receive(:enter) subj = StateMachineSubject.new
# subj.expects(:enter)
# foo.close subj.close
# end end
#
# it "should call exit when exiting state" do test "calls exit when exiting state" do
# foo = Foo.new subj = StateMachineSubject.new
# foo.should_receive(:exit) subj.expects(:exit)
# subj.close
# foo.close end
# end end
#end end
#
#
class StateMachineInheritanceTest < ActiveModel::TestCase class StateMachineInheritanceTest < ActiveModel::TestCase
test "should have the same states as it's parent" do test "has the same states as its parent" do
assert_equal StateMachineSubject.state_machine.states, StateMachineSubjectSubclass.state_machine.states assert_equal StateMachineSubject.state_machine.states, StateMachineSubjectSubclass.state_machine.states
end end
#test "should have the same events as it's parent" do test "has the same events as its parent" do
# StateMachineSubjectSubclass.aasm_events.should == Bar.aasm_events assert_equal StateMachineSubject.state_machine.events, StateMachineSubjectSubclass.state_machine.events
#end end
end end
# #
# #
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册