Skip to content
This repository
Browse code

Axe AM state machine

We're going do it eventually, get it done before 3.0 is final.
  • Loading branch information...
commit db49c706b62e7ea2ab93f05399dbfddf5087ee0c 1 parent 657d855
Joshua Peek authored January 30, 2010
1  activemodel/lib/active_model.rb
@@ -43,7 +43,6 @@ module ActiveModel
43 43
   autoload :Observer, 'active_model/observing'
44 44
   autoload :Observing
45 45
   autoload :Serialization
46  
-  autoload :StateMachine
47 46
   autoload :TestCase
48 47
   autoload :Translation
49 48
   autoload :VERSION
70  activemodel/lib/active_model/state_machine.rb
... ...
@@ -1,70 +0,0 @@
1  
-module ActiveModel
2  
-  module StateMachine
3  
-    autoload :Event, 'active_model/state_machine/event'
4  
-    autoload :Machine, 'active_model/state_machine/machine'
5  
-    autoload :State, 'active_model/state_machine/state'
6  
-    autoload :StateTransition, 'active_model/state_machine/state_transition'
7  
-
8  
-    extend ActiveSupport::Concern
9  
-
10  
-    class InvalidTransition < Exception
11  
-    end
12  
-
13  
-    module ClassMethods
14  
-      def inherited(klass)
15  
-        super
16  
-        klass.state_machines = state_machines
17  
-      end
18  
-
19  
-      def state_machines
20  
-        @state_machines ||= {}
21  
-      end
22  
-
23  
-      def state_machines=(value)
24  
-        @state_machines = value ? value.dup : nil
25  
-      end
26  
-
27  
-      def state_machine(name = nil, options = {}, &block)
28  
-        if name.is_a?(Hash)
29  
-          options = name
30  
-          name    = nil
31  
-        end
32  
-        name ||= :default
33  
-        state_machines[name] ||= Machine.new(self, name)
34  
-        block ? state_machines[name].update(options, &block) : state_machines[name]
35  
-      end
36  
-
37  
-      def define_state_query_method(state_name)
38  
-        name = "#{state_name}?"
39  
-        undef_method(name) if method_defined?(name)
40  
-        class_eval "def #{name}; current_state.to_s == %(#{state_name}) end"
41  
-      end
42  
-    end
43  
-
44  
-    def current_state(name = nil, new_state = nil, persist = false)
45  
-      sm   = self.class.state_machine(name)
46  
-      ivar = sm.current_state_variable
47  
-      if name && new_state
48  
-        if persist && respond_to?(:write_state)
49  
-          write_state(sm, new_state)
50  
-        end
51  
-
52  
-        if respond_to?(:write_state_without_persistence)
53  
-          write_state_without_persistence(sm, new_state)
54  
-        end
55  
-
56  
-        instance_variable_set(ivar, new_state)
57  
-      else
58  
-        instance_variable_set(ivar, nil) unless instance_variable_defined?(ivar)
59  
-        value = instance_variable_get(ivar)
60  
-        return value if value
61  
-
62  
-        if respond_to?(:read_state)
63  
-          value = instance_variable_set(ivar, read_state(sm))
64  
-        end
65  
-
66  
-        value || sm.initial_state
67  
-      end
68  
-    end
69  
-  end
70  
-end
62  activemodel/lib/active_model/state_machine/event.rb
... ...
@@ -1,62 +0,0 @@
1  
-module ActiveModel
2  
-  module StateMachine
3  
-    class Event
4  
-      attr_reader :name, :success
5  
-
6  
-      def initialize(machine, name, options = {}, &block)
7  
-        @machine, @name, @transitions = machine, name, []
8  
-        if machine
9  
-          machine.klass.send(:define_method, "#{name}!") do |*args|
10  
-            machine.fire_event(name, self, true, *args)
11  
-          end
12  
-
13  
-          machine.klass.send(:define_method, name.to_s) do |*args|
14  
-            machine.fire_event(name, self, false, *args)
15  
-          end
16  
-        end
17  
-        update(options, &block)
18  
-      end
19  
-
20  
-      def fire(obj, to_state = nil, *args)
21  
-        transitions = @transitions.select { |t| t.from == obj.current_state(@machine ? @machine.name : nil) }
22  
-        raise InvalidTransition if transitions.size == 0
23  
-
24  
-        next_state = nil
25  
-        transitions.each do |transition|
26  
-          next if to_state && !Array(transition.to).include?(to_state)
27  
-          if transition.perform(obj)
28  
-            next_state = to_state || Array(transition.to).first
29  
-            transition.execute(obj, *args)
30  
-            break
31  
-          end
32  
-        end
33  
-        next_state
34  
-      end
35  
-
36  
-      def transitions_from_state?(state)
37  
-        @transitions.any? { |t| t.from? state }
38  
-      end
39  
-
40  
-      def ==(event)
41  
-        if event.is_a? Symbol
42  
-          name == event
43  
-        else
44  
-          name == event.name
45  
-        end
46  
-      end
47  
-
48  
-      def update(options = {}, &block)
49  
-        if options.key?(:success) then @success = options[:success] end
50  
-        if block                  then instance_eval(&block)        end
51  
-        self
52  
-      end
53  
-
54  
-      private
55  
-        def transitions(trans_opts)
56  
-          Array(trans_opts[:from]).each do |s|
57  
-            @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
58  
-          end
59  
-        end
60  
-    end
61  
-  end
62  
-end
75  activemodel/lib/active_model/state_machine/machine.rb
... ...
@@ -1,75 +0,0 @@
1  
-module ActiveModel
2  
-  module StateMachine
3  
-    class Machine
4  
-      attr_writer :initial_state
5  
-      attr_accessor :states, :events, :state_index
6  
-      attr_reader :klass, :name
7  
-
8  
-      def initialize(klass, name, options = {}, &block)
9  
-        @klass, @name, @states, @state_index, @events = klass, name, [], {}, {}
10  
-        update(options, &block)
11  
-      end
12  
-
13  
-      def initial_state
14  
-        @initial_state ||= (states.first ? states.first.name : nil)
15  
-      end
16  
-
17  
-      def update(options = {}, &block)
18  
-        if options.key?(:initial) then @initial_state = options[:initial] end
19  
-        if block                  then instance_eval(&block)              end
20  
-        self
21  
-      end
22  
-
23  
-      def fire_event(event, record, persist, *args)
24  
-        state_index[record.current_state(@name)].call_action(:exit, record)
25  
-        if new_state = @events[event].fire(record, *args)
26  
-          state_index[new_state].call_action(:enter, record)
27  
-
28  
-          if record.respond_to?(event_fired_callback)
29  
-            record.send(event_fired_callback, record.current_state, new_state)
30  
-          end
31  
-
32  
-          record.current_state(@name, new_state, persist)
33  
-          record.send(@events[event].success) if @events[event].success
34  
-          true
35  
-        else
36  
-          if record.respond_to?(event_failed_callback)
37  
-            record.send(event_failed_callback, event)
38  
-          end
39  
-
40  
-          false
41  
-        end
42  
-      end
43  
-
44  
-      def states_for_select
45  
-        states.map { |st| [st.display_name, st.name.to_s] }
46  
-      end
47  
-
48  
-      def events_for(state)
49  
-        events = @events.values.select { |event| event.transitions_from_state?(state) }
50  
-        events.map! { |event| event.name }
51  
-      end
52  
-
53  
-      def current_state_variable
54  
-        "@#{@name}_current_state"
55  
-      end
56  
-
57  
-      private
58  
-        def state(name, options = {})
59  
-          @states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
60  
-        end
61  
-
62  
-        def event(name, options = {}, &block)
63  
-          (@events[name] ||= Event.new(self, name)).update(options, &block)
64  
-        end
65  
-
66  
-        def event_fired_callback
67  
-          @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired'
68  
-        end
69  
-
70  
-        def event_failed_callback
71  
-          @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
72  
-        end
73  
-    end
74  
-  end
75  
-end
47  activemodel/lib/active_model/state_machine/state.rb
... ...
@@ -1,47 +0,0 @@
1  
-module ActiveModel
2  
-  module StateMachine
3  
-    class State
4  
-      attr_reader :name, :options
5  
-
6  
-      def initialize(name, options = {})
7  
-        @name = name
8  
-        if machine = options.delete(:machine)
9  
-          machine.klass.define_state_query_method(name)
10  
-        end
11  
-        update(options)
12  
-      end
13  
-
14  
-      def ==(state)
15  
-        if state.is_a? Symbol
16  
-          name == state
17  
-        else
18  
-          name == state.name
19  
-        end
20  
-      end
21  
-
22  
-      def call_action(action, record)
23  
-        action = @options[action]
24  
-        case action
25  
-        when Symbol, String
26  
-          record.send(action)
27  
-        when Proc
28  
-          action.call(record)
29  
-        end
30  
-      end
31  
-
32  
-      def display_name
33  
-        @display_name ||= name.to_s.gsub(/_/, ' ').capitalize
34  
-      end
35  
-
36  
-      def for_select
37  
-        [display_name, name.to_s]
38  
-      end
39  
-
40  
-      def update(options = {})
41  
-        if options.key?(:display) then @display_name = options.delete(:display) end
42  
-        @options = options
43  
-        self
44  
-      end
45  
-    end
46  
-  end
47  
-end
40  activemodel/lib/active_model/state_machine/state_transition.rb
... ...
@@ -1,40 +0,0 @@
1  
-module ActiveModel
2  
-  module StateMachine
3  
-    class StateTransition
4  
-      attr_reader :from, :to, :options
5  
-
6  
-      def initialize(opts)
7  
-        @from, @to, @guard, @on_transition = opts[:from], opts[:to], opts[:guard], opts[:on_transition]
8  
-        @options = opts
9  
-      end
10  
-
11  
-      def perform(obj)
12  
-        case @guard
13  
-        when Symbol, String
14  
-          obj.send(@guard)
15  
-        when Proc
16  
-          @guard.call(obj)
17  
-        else
18  
-          true
19  
-        end
20  
-      end
21  
-
22  
-      def execute(obj, *args)
23  
-        case @on_transition
24  
-        when Symbol, String
25  
-          obj.send(@on_transition, *args)
26  
-        when Proc
27  
-          @on_transition.call(obj, *args)
28  
-        end
29  
-      end
30  
-
31  
-      def ==(obj)
32  
-        @from == obj.from && @to == obj.to
33  
-      end
34  
-
35  
-      def from?(value)
36  
-        @from == value
37  
-      end
38  
-    end
39  
-  end
40  
-end
49  activemodel/test/cases/state_machine/event_test.rb
... ...
@@ -1,49 +0,0 @@
1  
-require 'cases/helper'
2  
-
3  
-class EventTest < ActiveModel::TestCase
4  
-  def setup
5  
-    @state_name = :close_order
6  
-    @success = :success_callback
7  
-  end
8  
-
9  
-  def new_event
10  
-    @event = ActiveModel::StateMachine::Event.new(nil, @state_name, {:success => @success}) do
11  
-      transitions :to => :closed, :from => [:open, :received]
12  
-    end
13  
-  end
14  
-
15  
-  test 'should set the name' do
16  
-    assert_equal @state_name, new_event.name
17  
-  end
18  
-
19  
-  test 'should set the success option' do
20  
-    assert_equal @success, new_event.success
21  
-  end
22  
-
23  
-  test 'should create StateTransitions' do
24  
-    ActiveModel::StateMachine::StateTransition.expects(:new).with(:to => :closed, :from => :open)
25  
-    ActiveModel::StateMachine::StateTransition.expects(:new).with(:to => :closed, :from => :received)
26  
-    new_event
27  
-  end
28  
-end
29  
-
30  
-class EventBeingFiredTest < ActiveModel::TestCase
31  
-  test 'should raise an AASM::InvalidTransition error if the transitions are empty' do
32  
-    event = ActiveModel::StateMachine::Event.new(nil, :event)
33  
-
34  
-    assert_raise ActiveModel::StateMachine::InvalidTransition do
35  
-      event.fire(nil)
36  
-    end
37  
-  end
38  
-
39  
-  test 'should return the state of the first matching transition it finds' do
40  
-    event = ActiveModel::StateMachine::Event.new(nil, :event) do
41  
-      transitions :to => :closed, :from => [:open, :received]
42  
-    end
43  
-
44  
-    obj = stub
45  
-    obj.stubs(:current_state).returns(:open)
46  
-
47  
-    assert_equal :closed, event.fire(obj)
48  
-  end
49  
-end
43  activemodel/test/cases/state_machine/machine_test.rb
... ...
@@ -1,43 +0,0 @@
1  
-require 'cases/helper'
2  
-
3  
-class MachineTestSubject
4  
-  include ActiveModel::StateMachine
5  
-
6  
-  state_machine do
7  
-    state :open
8  
-    state :closed
9  
-  end
10  
-
11  
-  state_machine :initial => :foo do
12  
-    event :shutdown do
13  
-      transitions :from => :open, :to => :closed
14  
-    end
15  
-
16  
-    event :timeout do
17  
-      transitions :from => :open, :to => :closed
18  
-    end
19  
-  end
20  
-
21  
-  state_machine :extra, :initial => :bar do
22  
-  end
23  
-end
24  
-
25  
-class StateMachineMachineTest < ActiveModel::TestCase
26  
-  test "allows reuse of existing machines" do
27  
-    assert_equal 2, MachineTestSubject.state_machines.size
28  
-  end
29  
-
30  
-  test "sets #initial_state from :initial option" do
31  
-    assert_equal :bar, MachineTestSubject.state_machine(:extra).initial_state
32  
-  end
33  
-
34  
-  test "accesses non-default state machine" do
35  
-    assert_kind_of ActiveModel::StateMachine::Machine, MachineTestSubject.state_machine(:extra)
36  
-  end
37  
-
38  
-  test "finds events for given state" do
39  
-    events = MachineTestSubject.state_machine.events_for(:open)
40  
-    assert events.include?(:shutdown)
41  
-    assert events.include?(:timeout)
42  
-  end
43  
-end
72  activemodel/test/cases/state_machine/state_test.rb
... ...
@@ -1,72 +0,0 @@
1  
-require 'cases/helper'
2  
-
3  
-class StateTestSubject
4  
-  include ActiveModel::StateMachine
5  
-
6  
-  state_machine do
7  
-  end
8  
-end
9  
-
10  
-class StateTest < ActiveModel::TestCase
11  
-  def setup
12  
-    @state_name = :astate
13  
-    @machine = StateTestSubject.state_machine
14  
-    @options = { :crazy_custom_key => 'key', :machine => @machine }
15  
-  end
16  
-
17  
-  def new_state(options={})
18  
-    ActiveModel::StateMachine::State.new(@state_name, @options.merge(options))
19  
-  end
20  
-
21  
-  test 'sets the name' do
22  
-    assert_equal :astate, new_state.name
23  
-  end
24  
-
25  
-  test 'sets the display_name from name' do
26  
-    assert_equal "Astate", new_state.display_name
27  
-  end
28  
-
29  
-  test 'sets the display_name from options' do
30  
-    assert_equal "A State", new_state(:display => "A State").display_name
31  
-  end
32  
-  
33  
-  test 'sets the options and expose them as options' do
34  
-    @options.delete(:machine)
35  
-    assert_equal @options, new_state.options
36  
-  end
37  
-
38  
-  test 'equals a symbol of the same name' do
39  
-    assert_equal new_state, :astate
40  
-  end
41  
-
42  
-  test 'equals a State of the same name' do
43  
-    assert_equal new_state, new_state
44  
-  end
45  
-
46  
-  test 'should send a message to the record for an action if the action is present as a symbol' do
47  
-    state = new_state(:entering => :foo)
48  
-
49  
-    record = stub
50  
-    record.expects(:foo)
51  
-
52  
-    state.call_action(:entering, record)
53  
-  end
54  
-
55  
-  test 'should send a message to the record for an action if the action is present as a string' do
56  
-    state = new_state(:entering => 'foo')
57  
-
58  
-    record = stub
59  
-    record.expects(:foo)
60  
-
61  
-    state.call_action(:entering, record)
62  
-  end
63  
-
64  
-  test 'should call a proc, passing in the record for an action if the action is present' do
65  
-    state = new_state(:entering => Proc.new {|r| r.foobar})
66  
-
67  
-    record = stub
68  
-    record.expects(:foobar)
69  
-  
70  
-    state.call_action(:entering, record)
71  
-  end
72  
-end
84  activemodel/test/cases/state_machine/state_transition_test.rb
... ...
@@ -1,84 +0,0 @@
1  
-require 'cases/helper'
2  
-
3  
-class StateTransitionTest < ActiveModel::TestCase
4  
-  test 'should set from, to, and opts attr readers' do
5  
-    opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
6  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
7  
-
8  
-    assert_equal opts[:from], st.from
9  
-    assert_equal opts[:to],   st.to
10  
-    assert_equal opts,        st.options
11  
-  end
12  
-
13  
-  test 'should pass equality check if from and to are the same' do
14  
-    opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
15  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
16  
-
17  
-    obj = stub
18  
-    obj.stubs(:from).returns(opts[:from])
19  
-    obj.stubs(:to).returns(opts[:to])
20  
-
21  
-    assert_equal st, obj
22  
-  end
23  
-
24  
-  test 'should fail equality check if from are not the same' do
25  
-    opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
26  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
27  
-
28  
-    obj = stub
29  
-    obj.stubs(:from).returns('blah')
30  
-    obj.stubs(:to).returns(opts[:to])
31  
-
32  
-    assert_not_equal st, obj
33  
-  end
34  
-
35  
-  test 'should fail equality check if to are not the same' do
36  
-    opts = {:from => 'foo', :to => 'bar', :guard => 'g'}
37  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
38  
-
39  
-    obj = stub
40  
-    obj.stubs(:from).returns(opts[:from])
41  
-    obj.stubs(:to).returns('blah')
42  
-
43  
-    assert_not_equal st, obj
44  
-  end
45  
-end
46  
-
47  
-class StateTransitionGuardCheckTest < ActiveModel::TestCase
48  
-  test 'should return true of there is no guard' do
49  
-    opts = {:from => 'foo', :to => 'bar'}
50  
-      st = ActiveModel::StateMachine::StateTransition.new(opts)
51  
-
52  
-    assert st.perform(nil)
53  
-  end
54  
-
55  
-  test 'should call the method on the object if guard is a symbol' do
56  
-    opts = {:from => 'foo', :to => 'bar', :guard => :test_guard}
57  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
58  
-  
59  
-    obj = stub
60  
-    obj.expects(:test_guard)
61  
-    
62  
-    st.perform(obj)
63  
-  end
64  
-  
65  
-  test 'should call the method on the object if guard is a string' do
66  
-    opts = {:from => 'foo', :to => 'bar', :guard => 'test_guard'}
67  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
68  
-  
69  
-    obj = stub
70  
-    obj.expects(:test_guard)
71  
-    
72  
-    st.perform(obj)
73  
-  end
74  
-  
75  
-  test 'should call the proc passing the object if the guard is a proc' do
76  
-    opts = {:from => 'foo', :to => 'bar', :guard => Proc.new {|o| o.test_guard}}
77  
-    st = ActiveModel::StateMachine::StateTransition.new(opts)
78  
-  
79  
-    obj = stub
80  
-    obj.expects(:test_guard)
81  
-  
82  
-    st.perform(obj)
83  
-  end
84  
-end
312  activemodel/test/cases/state_machine_test.rb
... ...
@@ -1,312 +0,0 @@
1  
-require 'cases/helper'
2  
-
3  
-class StateMachineSubject
4  
-  include ActiveModel::StateMachine
5  
-
6  
-  state_machine do
7  
-    state :open,   :exit => :exit
8  
-    state :closed, :enter => :enter
9  
-    
10  
-    event :close, :success => :success_callback do
11  
-      transitions :to => :closed, :from => [:open]
12  
-    end
13  
-
14  
-    event :null do
15  
-      transitions :to => :closed, :from => [:open], :guard => :always_false
16  
-    end
17  
-  end
18  
-
19  
-  state_machine :bar do
20  
-    state :read
21  
-    state :ended
22  
-
23  
-    event :foo do
24  
-      transitions :to => :ended, :from => [:read]
25  
-    end
26  
-  end
27  
-
28  
-  def always_false
29  
-    false
30  
-  end
31  
- 
32  
-  def success_callback
33  
-  end
34  
- 
35  
-  def enter
36  
-  end
37  
-  def exit
38  
-  end
39  
-end
40  
-
41  
-class StateMachineSubjectSubclass < StateMachineSubject
42  
-end
43  
-
44  
-class StateMachineClassLevelTest < ActiveModel::TestCase
45  
-  test 'defines a class level #state_machine method on its including class' do
46  
-    assert StateMachineSubject.respond_to?(:state_machine)
47  
-  end
48  
-
49  
-  test 'defines a class level #state_machines method on its including class' do
50  
-    assert StateMachineSubject.respond_to?(:state_machines)
51  
-  end
52  
-
53  
-  test 'class level #state_machine returns machine instance' do
54  
-    assert_kind_of ActiveModel::StateMachine::Machine, StateMachineSubject.state_machine
55  
-  end
56  
-
57  
-  test 'class level #state_machine returns machine instance with given name' do
58  
-    assert_kind_of ActiveModel::StateMachine::Machine, StateMachineSubject.state_machine(:default)
59  
-  end
60  
-
61  
-  test 'class level #state_machines returns hash of machine instances' do
62  
-    assert_kind_of ActiveModel::StateMachine::Machine, StateMachineSubject.state_machines[:default]
63  
-  end
64  
-
65  
-  test "should return a select friendly array of states in the form of [['Friendly name', 'state_name']]" do
66  
-    assert_equal [['Open', 'open'], ['Closed', 'closed']], StateMachineSubject.state_machine.states_for_select
67  
-  end
68  
-end
69  
-
70  
-class StateMachineInstanceLevelTest < ActiveModel::TestCase
71  
-  def setup
72  
-    @foo = StateMachineSubject.new
73  
-  end
74  
- 
75  
-  test 'defines an accessor for the current state' do
76  
-    assert @foo.respond_to?(:current_state)
77  
-  end
78  
- 
79  
-  test 'defines a state querying instance method on including class' do
80  
-    assert @foo.respond_to?(:open?)
81  
-  end
82  
- 
83  
-  test 'defines an event! instance method' do
84  
-    assert @foo.respond_to?(:close!)
85  
-  end
86  
- 
87  
-  test 'defines an event instance method' do
88  
-    assert @foo.respond_to?(:close)
89  
-  end
90  
-end
91  
- 
92  
-class StateMachineInitialStatesTest < ActiveModel::TestCase
93  
-  def setup
94  
-    @foo = StateMachineSubject.new
95  
-  end
96  
- 
97  
-  test 'sets the initial state' do
98  
-    assert_equal :open, @foo.current_state
99  
-  end
100  
- 
101  
-  test '#open? should be initially true' do
102  
-    assert @foo.open?
103  
-  end
104  
-  
105  
-  test '#closed? should be initially false' do
106  
-    assert !@foo.closed?
107  
-  end
108  
- 
109  
-  test 'uses the first state defined if no initial state is given' do
110  
-    assert_equal :read, @foo.current_state(:bar)
111  
-  end
112  
-end
113  
- 
114  
-class StateMachineEventFiringWithPersistenceTest < ActiveModel::TestCase
115  
-  def setup
116  
-    @subj = StateMachineSubject.new
117  
-  end
118  
-
119  
-  test 'updates the current state' do
120  
-    @subj.close!
121  
-
122  
-    assert_equal :closed, @subj.current_state
123  
-  end
124  
-
125  
-  test 'fires the Event' do
126  
-    @subj.class.state_machine.events[:close].expects(:fire).with(@subj)
127  
-    @subj.close!
128  
-  end
129  
-
130  
-  test 'calls the success callback if one was provided' do
131  
-    @subj.expects(:success_callback)
132  
-    @subj.close!
133  
-  end
134  
-
135  
-  test 'attempts to persist if write_state is defined' do
136  
-    def @subj.write_state
137  
-    end
138  
-
139  
-    @subj.expects(:write_state)
140  
-    @subj.close!
141  
-  end
142  
-end
143  
-
144  
-class StateMachineEventFiringWithoutPersistence < ActiveModel::TestCase
145  
-  test 'updates the current state' do
146  
-    subj = StateMachineSubject.new
147  
-    assert_equal :open, subj.current_state
148  
-    subj.close
149  
-    assert_equal :closed, subj.current_state
150  
-  end
151  
-
152  
-  test 'fires the Event' do
153  
-    subj = StateMachineSubject.new
154  
-
155  
-    StateMachineSubject.state_machine.events[:close].expects(:fire).with(subj)
156  
-    subj.close
157  
-  end
158  
-
159  
-  test 'attempts to persist if write_state is defined' do
160  
-    subj = StateMachineSubject.new
161  
-
162  
-    def subj.write_state
163  
-    end
164  
-
165  
-    subj.expects(:write_state_without_persistence)
166  
-
167  
-    subj.close
168  
-  end
169  
-end
170  
-
171  
-class StateMachinePersistenceTest < ActiveModel::TestCase
172  
-  test 'reads the state if it has not been set and read_state is defined' do
173  
-    subj = StateMachineSubject.new
174  
-    def subj.read_state
175  
-    end
176  
-
177  
-    subj.expects(:read_state).with(StateMachineSubject.state_machine)
178  
-
179  
-    subj.current_state
180  
-  end
181  
-end
182  
-
183  
-class StateMachineEventCallbacksTest < ActiveModel::TestCase
184  
-  test 'should call aasm_event_fired if defined and successful for bang fire' do
185  
-    subj = StateMachineSubject.new
186  
-    def subj.aasm_event_fired(from, to)
187  
-    end
188  
-
189  
-    subj.expects(:event_fired)
190  
-
191  
-    subj.close!
192  
-  end
193  
-
194  
-  test 'should call aasm_event_fired if defined and successful for non-bang fire' do
195  
-    subj = StateMachineSubject.new
196  
-    def subj.aasm_event_fired(from, to)
197  
-    end
198  
-
199  
-    subj.expects(:event_fired)
200  
-
201  
-    subj.close
202  
-  end
203  
-
204  
-  test 'should call aasm_event_failed if defined and transition failed for bang fire' do
205  
-    subj = StateMachineSubject.new
206  
-    def subj.event_failed(event)
207  
-    end
208  
-
209  
-    subj.expects(:event_failed)
210  
-
211  
-    subj.null!
212  
-  end
213  
-
214  
-  test 'should call aasm_event_failed if defined and transition failed for non-bang fire' do
215  
-    subj = StateMachineSubject.new
216  
-    def subj.aasm_event_failed(event)
217  
-    end
218  
-
219  
-    subj.expects(:event_failed)
220  
-
221  
-    subj.null
222  
-  end
223  
-end
224  
-
225  
-class StateMachineStateActionsTest < ActiveModel::TestCase
226  
-  test "calls enter when entering state" do
227  
-    subj = StateMachineSubject.new
228  
-    subj.expects(:enter)
229  
-    subj.close
230  
-  end
231  
-
232  
-  test "calls exit when exiting state" do
233  
-    subj = StateMachineSubject.new
234  
-    subj.expects(:exit)
235  
-    subj.close
236  
-  end
237  
-end
238  
-
239  
-class StateMachineInheritanceTest < ActiveModel::TestCase
240  
-  test "has the same states as its parent" do
241  
-    assert_equal StateMachineSubject.state_machine.states, StateMachineSubjectSubclass.state_machine.states
242  
-  end
243  
- 
244  
-  test "has the same events as its parent" do
245  
-    assert_equal StateMachineSubject.state_machine.events, StateMachineSubjectSubclass.state_machine.events
246  
-  end
247  
-end
248  
-
249  
-class StateMachineSubject
250  
-  state_machine :chetan_patil, :initial => :sleeping do
251  
-    state :sleeping
252  
-    state :showering
253  
-    state :working
254  
-    state :dating
255  
- 
256  
-    event :wakeup do
257  
-      transitions :from => :sleeping, :to => [:showering, :working]
258  
-    end
259  
- 
260  
-    event :dress do
261  
-      transitions :from => :sleeping, :to => :working, :on_transition => :wear_clothes
262  
-      transitions :from => :showering, :to => [:working, :dating], :on_transition => Proc.new { |obj, *args| obj.wear_clothes(*args) }
263  
-    end
264  
-  end
265  
- 
266  
-  def wear_clothes(shirt_color, trouser_type)
267  
-  end
268  
-end
269  
-
270  
-class StateMachineWithComplexTransitionsTest < ActiveModel::TestCase
271  
-  def setup
272  
-    @subj = StateMachineSubject.new
273  
-  end
274  
-
275  
-  test 'transitions to specified next state (sleeping to showering)' do
276  
-    @subj.wakeup! :showering
277  
-    
278  
-    assert_equal :showering, @subj.current_state(:chetan_patil)
279  
-  end
280  
- 
281  
-  test 'transitions to specified next state (sleeping to working)' do
282  
-    @subj.wakeup! :working
283  
- 
284  
-    assert_equal :working, @subj.current_state(:chetan_patil)
285  
-  end
286  
- 
287  
-  test 'transitions to default (first or showering) state' do
288  
-    @subj.wakeup!
289  
- 
290  
-    assert_equal :showering, @subj.current_state(:chetan_patil)
291  
-  end
292  
- 
293  
-  test 'transitions to default state when on_transition invoked' do
294  
-    @subj.dress!(nil, 'purple', 'dressy')
295  
- 
296  
-    assert_equal :working, @subj.current_state(:chetan_patil)
297  
-  end
298  
-
299  
-  test 'calls on_transition method with args' do
300  
-    @subj.wakeup! :showering
301  
-
302  
-    @subj.expects(:wear_clothes).with('blue', 'jeans')
303  
-    @subj.dress! :working, 'blue', 'jeans'
304  
-  end
305  
-
306  
-  test 'calls on_transition proc' do
307  
-    @subj.wakeup! :showering
308  
-
309  
-    @subj.expects(:wear_clothes).with('purple', 'slacks')
310  
-    @subj.dress!(:dating, 'purple', 'slacks')
311  
-  end
312  
-end
1  activerecord/lib/active_record.rb
@@ -73,7 +73,6 @@ module ActiveRecord
73 73
     autoload :SchemaDumper
74 74
     autoload :Serialization
75 75
     autoload :SessionStore
76  
-    autoload :StateMachine
77 76
     autoload :Timestamp
78 77
     autoload :Transactions
79 78
     autoload :Validations
24  activerecord/lib/active_record/state_machine.rb
... ...
@@ -1,24 +0,0 @@
1  
-module ActiveRecord
2  
-  module StateMachine #:nodoc:
3  
-    extend ActiveSupport::Concern
4  
-    include ActiveModel::StateMachine
5  
-
6  
-    included do
7  
-      before_validation :set_initial_state
8  
-      validates_presence_of :state
9  
-    end
10  
-
11  
-    protected
12  
-      def write_state(state_machine, state)
13  
-        update_attributes! :state => state.to_s
14  
-      end
15  
-
16  
-      def read_state(state_machine)
17  
-        self.state.to_sym
18  
-      end
19  
-
20  
-      def set_initial_state
21  
-        self.state ||= self.class.state_machine.initial_state.to_s
22  
-      end
23  
-  end
24  
-end
42  activerecord/test/cases/state_machine_test.rb
... ...
@@ -1,42 +0,0 @@
1  
-require 'cases/helper'
2  
-require 'models/traffic_light'
3  
-
4  
-class StateMachineTest < ActiveRecord::TestCase
5  
-  def setup
6  
-    @light = TrafficLight.create!
7  
-  end
8  
-
9  
-  test "states initial state" do
10  
-    assert @light.off?
11  
-    assert_equal :off, @light.current_state
12  
-  end
13  
-
14  
-  test "transition to a valid state" do
15  
-    @light.reset
16  
-    assert @light.red?
17  
-    assert_equal :red, @light.current_state
18  
-
19  
-    @light.green_on
20  
-    assert @light.green?
21  
-    assert_equal :green, @light.current_state
22  
-  end
23  
-
24  
-  test "transition does not persist state" do
25  
-    @light.reset
26  
-    assert_equal :red, @light.current_state
27  
-    @light.reload
28  
-    assert_equal "off", @light.state
29  
-  end
30  
-
31  
-  test "transition does persists state" do
32  
-    @light.reset!
33  
-    assert_equal :red, @light.current_state
34  
-    @light.reload
35  
-    assert_equal "red", @light.state
36  
-  end
37  
-
38  
-  test "transition to an invalid state" do
39  
-    assert_raise(ActiveModel::StateMachine::InvalidTransition) { @light.yellow_on }
40  
-    assert_equal :off, @light.current_state
41  
-  end
42  
-end
27  activerecord/test/models/traffic_light.rb
... ...
@@ -1,27 +0,0 @@
1  
-class TrafficLight < ActiveRecord::Base
2  
-  include ActiveRecord::StateMachine
3  
-
4  
-  state_machine do
5  
-    state :off
6  
-
7  
-    state :red
8  
-    state :green
9  
-    state :yellow
10  
-
11  
-    event :red_on do
12  
-      transitions :to => :red, :from => [:yellow]
13  
-    end
14  
-
15  
-    event :green_on do
16  
-      transitions :to => :green, :from => [:red]
17  
-    end
18  
-
19  
-    event :yellow_on do
20  
-      transitions :to => :yellow, :from => [:green]
21  
-    end
22  
-
23  
-    event :reset do
24  
-      transitions :to => :red, :from => [:off]
25  
-    end
26  
-  end
27  
-end

9 notes on commit db49c70

Stephen Celis

Was this extracted? Or just given the boot?

AASM is broken in Rails 3 (though easy to fix), so is there no current state machine solution without some hacking?

David Bock

I hope to see this go back in - your comment is a bit ambiguous... will this be added back bafore 3.0 goes final? What was the problem that made you axe it from the beta?

Mikel Lindsaar
Collaborator

It was given the boot. Basically was not ready to ship as part of Rails 3.

Mainly in terms of the API it provided.

Please feel free to fix AASM so that we have a working State Machine implementation for Rails 3.

Mikel

Joshua Peek
Collaborator

The lib was totally fine, it was just out of scope for core. Its not coming back.

We could turn it into a plugin if anyone will volunteer to maintain it.

Kuba Kuźma

I've extracted the code to gem: http://github.com/qoobaa/state_machine

Still few things need to be done:

  • invent a different name (activemodel-state_machine? isn't it too long?)
  • put appropriate copyright notice (who is the author of the code?)
  • write a documentation
Joshua Peek
Collaborator

Rick Olson is the original author.

Kuba Kuźma

Thanks, I've put the copyright notices on the top of source files (I hope it was released on the MIT license as well).
Any suggestions about the name? :-)

Jonas Nicklas

qoobaa, you might want to give it a different name, since there's already state_machine by pluginaweek (http://github.com/pluginaweek/state_machine) It'd be very confusing to have two different plugins that do the same thing with the same name.

I really encourage everyone here to take a look at state_machine (the pluginaweek solution) it really kicks AASM's butt pretty comprehensively. It also has a lot of features that the ActiveModel solution didn't have.

IMHO it's a good thing to leave this in plugin land, don't put it back in.

Jacek Becela

fsm would be cool but we need to get this out of the way: http://rubygems.org/gems/fsm

Please sign in to comment.
Something went wrong with that request. Please try again.