public
Description: Adds support for creating state machines for attributes within a model
Homepage: http://www.pluginaweek.org
Clone URL: git://github.com/pluginaweek/state_machine.git
Search Repo:
Allow creating transitions with no from state (effectively allowing the 
transition for *any* from state)
Reduce the number of objects created for each transition
obrie (author)
Thu Jul 03 14:50:10 -0700 2008
commit  b90d731c157acfc564e61fedecb4819732536ec2
tree    7db5f5c0abd332d5603660455fdcc4b17933e460
parent  4038912091b9205e8eca10d2130a64f2653851c7
...
1
2
 
 
 
3
4
5
...
1
2
3
4
5
6
7
8
0
@@ -1,5 +1,8 @@
0
 == master
0
 
0
+* Allow creating transitions with no from state (effectively allowing the transition for *any* from state)
0
+* Reduce the number of objects created for each transition
0
+
0
 == 0.2.0 / 2008-06-29
0
 
0
 * Add a non-bang version of events (e.g. park) that will return a boolean value for success
...
31
32
33
34
 
35
36
37
38
39
 
40
41
42
...
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
 
 
 
 
 
 
 
 
 
 
 
 
 
68
69
70
...
31
32
33
 
34
35
36
37
38
39
40
41
42
43
...
51
52
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
0
@@ -31,12 +31,13 @@ module PluginAWeek #:nodoc:
0
       #
0
       # Configuration options:
0
       # * +to+ - The state that being transitioned to
0
- # * +from+ - A state or array of states that can be transitioned from
0
+ # * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state
0
       # * +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.
0
       # * +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.
0
       #
0
       # == Examples
0
       #
0
+ # transition :to => 'parked'
0
       # transition :to => 'parked', :from => 'first_gear'
0
       # transition :to => 'parked', :from => %w(first_gear reverse)
0
       # transition :to => 'parked', :from => 'first_gear', :if => :moving?
0
@@ -50,21 +51,19 @@ module PluginAWeek #:nodoc:
0
         to_state = options.delete(:to)
0
         from_states = Array(options.delete(:from))
0
         
0
- from_states.collect do |from_state|
0
- # Create the actual transition that will update records when performed
0
- transition = Transition.new(self, from_state, to_state)
0
-
0
- # Add the callback to the model. If the callback fails, then the next
0
- # available callback for the event will run until one is successful.
0
- callback = Proc.new {|record, *args| try_transition(transition, false, record, *args)}
0
- owner_class.send("transition_on_#{name}", callback, options)
0
-
0
- # Add the callback! to the model similar to above
0
- callback = Proc.new {|record, *args| try_transition(transition, true, record, *args)}
0
- owner_class.send("transition_bang_on_#{name}", callback, options)
0
-
0
- transition
0
- end
0
+ # Create the actual transition that will update records when performed
0
+ transition = Transition.new(self, to_state, *from_states)
0
+
0
+ # Add the callback to the model. If the callback fails, then the next
0
+ # available callback for the event will run until one is successful.
0
+ callback = Proc.new {|record, *args| try_transition(transition, false, record, *args)}
0
+ owner_class.send("transition_on_#{name}", callback, options)
0
+
0
+ # Add the callback! to the model similar to above
0
+ callback = Proc.new {|record, *args| try_transition(transition, true, record, *args)}
0
+ owner_class.send("transition_bang_on_#{name}", callback, options)
0
+
0
+ transition
0
       end
0
       
0
       # Attempts to perform one of the event's transitions for the given record
...
7
8
9
10
 
11
12
13
14
15
16
17
18
19
 
 
 
20
21
22
23
24
25
26
 
27
28
29
30
 
31
32
33
34
35
 
 
36
37
38
39
40
41
 
42
43
44
...
7
8
9
 
10
11
12
13
 
 
 
14
15
16
17
18
19
20
21
22
23
24
25
 
26
27
 
28
 
29
30
31
32
 
 
33
34
35
36
37
38
39
 
40
41
42
43
0
@@ -7,38 +7,37 @@ module PluginAWeek #:nodoc:
0
     # A transition indicates a state change and is described by a condition
0
     # that would need to be fulfilled to enable the transition. Transitions
0
     # consist of:
0
- # * The starting state
0
+ # * The starting state(s)
0
     # * The ending state
0
     # * A guard to check if the transition is allowed
0
     class Transition
0
- # The state from which the transition is being made
0
- attr_reader :from_state
0
-
0
       # The state to which the transition is being made
0
       attr_reader :to_state
0
       
0
+ # The states from which the transition can be made
0
+ attr_reader :from_states
0
+
0
       # The event that caused the transition
0
       attr_reader :event
0
       
0
       delegate :machine,
0
                   :to => :event
0
       
0
- def initialize(event, from_state, to_state) #:nodoc:
0
+ def initialize(event, to_state, *from_states) #:nodoc:
0
         @event = event
0
- @from_state = from_state
0
         @to_state = to_state
0
- @loopback = from_state == to_state
0
+ @from_states = from_states
0
       end
0
       
0
       # Whether or not this is a loopback transition (i.e. from and to state are the same)
0
- def loopback?(state = from_state)
0
- state == to_state
0
+ def loopback?(from_state)
0
+ from_state == to_state
0
       end
0
       
0
       # Determines whether or not this transition can be performed on the given
0
       # states
0
       def can_perform_on?(record)
0
- !from_state || from_state == record.send(machine.attribute)
0
+ from_states.empty? || from_states.include?(record.send(machine.attribute))
0
       end
0
       
0
       # Runs the actual transition and any callbacks associated with entering
...
69
70
71
 
 
 
 
72
73
 
74
75
76
77
 
78
79
80
...
124
125
126
127
 
 
 
 
 
 
 
128
129
130
131
132
 
 
 
 
 
 
133
134
135
...
69
70
71
72
73
74
75
76
 
77
78
79
80
 
81
82
83
84
...
128
129
130
 
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
0
@@ -69,12 +69,16 @@ class EventWithTransitionsTest < Test::Unit::TestCase
0
     assert_nothing_raised {@event.transition(:to => 'on')}
0
   end
0
   
0
+ def test_should_allow_transitioning_without_a_state
0
+ assert @event.transition(:to => 'on')
0
+ end
0
+
0
   def test_should_allow_transitioning_from_a_single_state
0
- assert_equal [%w(off on)], @event.transition(:to => 'on', :from => 'off').map {|t| [t.from_state, t.to_state]}
0
+ assert @event.transition(:to => 'on', :from => 'off')
0
   end
0
   
0
   def test_should_allow_transitioning_from_multiple_states
0
- assert_equal [%w(off on), %w(on on)], @event.transition(:to => 'on', :from => %w(off on)).map {|t| [t.from_state, t.to_state]}
0
+ assert @event.transition(:to => 'on', :from => %w(off on))
0
   end
0
   
0
   def teardown
0
@@ -124,12 +128,24 @@ class EventAfterBeingFiredWithTransitionsTest < Test::Unit::TestCase
0
     assert_equal 'off', @switch.state
0
   end
0
   
0
- def test_should_fire_if_transition_is_matched
0
+ def test_should_fire_if_transition_with_no_from_state_is_matched
0
+ @event.transition :to => 'on'
0
+ assert @event.fire(@switch)
0
+ assert_equal 'on', @switch.state
0
+ end
0
+
0
+ def test_should_fire_if_transition_with_from_state_is_matched
0
     @event.transition :to => 'on', :from => 'off'
0
     assert @event.fire(@switch)
0
     assert_equal 'on', @switch.state
0
   end
0
   
0
+ def test_should_fire_if_transition_with_multiple_from_states_is_matched
0
+ @event.transition :to => 'on', :from => %w(off on)
0
+ assert @event.fire(@switch)
0
+ assert_equal 'on', @switch.state
0
+ end
0
+
0
   def test_should_not_fire_if_validation_failed
0
     @event.transition :to => 'on', :from => 'off'
0
     @switch.fail_validation = true
...
4
5
6
7
 
8
9
10
11
 
 
12
13
14
15
 
 
16
17
18
19
 
 
20
21
22
23
24
 
 
25
26
27
 
28
29
 
 
 
30
31
32
 
33
34
 
 
 
35
36
37
...
63
64
65
66
 
67
68
69
70
 
71
72
73
74
 
 
 
75
76
77
78
79
80
 
81
82
 
83
84
85
86
 
87
88
89
90
 
91
92
93
94
 
95
96
97
98
 
 
 
99
100
101
102
 
 
 
103
104
105
 
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
108
109
110
 
 
 
 
111
112
113
114
115
116
 
 
117
 
 
 
118
119
120
...
122
123
124
125
 
126
127
128
...
162
163
164
165
 
166
167
168
...
295
296
297
298
 
299
300
301
...
4
5
6
 
7
8
9
 
 
10
11
12
13
 
 
14
15
16
17
 
 
18
19
20
21
 
 
 
22
23
24
25
 
26
27
28
29
30
31
32
33
 
34
35
36
37
38
39
40
41
42
...
68
69
70
 
71
72
73
74
 
75
76
77
 
 
78
79
80
81
82
 
 
 
 
83
84
 
85
86
87
88
 
89
90
91
92
 
93
94
95
96
 
97
98
99
 
 
100
101
102
103
104
 
 
105
106
107
108
109
 
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
 
132
133
134
135
136
137
138
139
 
 
140
141
142
143
144
145
146
147
148
...
150
151
152
 
153
154
155
156
...
190
191
192
 
193
194
195
196
...
323
324
325
 
326
327
328
329
0
@@ -4,34 +4,39 @@ class TransitionTest < Test::Unit::TestCase
0
   def setup
0
     @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
     @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'off', 'on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on')
0
   end
0
   
0
- def test_should_have_a_from_state
0
- assert_equal 'off', @transition.from_state
0
+ def test_should_not_have_any_from_states
0
+ assert @transition.from_states.empty?
0
   end
0
   
0
- def test_should_have_a_to_state
0
- assert_equal 'on', @transition.to_state
0
+ def test_should_not_be_a_loopback_if_from_state_is_different
0
+ assert !@transition.loopback?('off')
0
   end
0
   
0
- def test_should_not_be_a_loopback
0
- assert !@transition.loopback?
0
+ def test_should_have_a_to_state
0
+ assert_equal 'on', @transition.to_state
0
   end
0
   
0
- def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
0
- record = new_switch(:state => 'on')
0
- assert !@transition.can_perform_on?(record)
0
+ def test_should_be_loopback_if_from_state_is_same
0
+ assert @transition.loopback?('on')
0
   end
0
   
0
- def test_should_be_able_to_perform_if_record_state_is_from_state
0
+ def test_should_be_able_to_perform_on_all_states
0
     record = new_switch(:state => 'off')
0
     assert @transition.can_perform_on?(record)
0
+
0
+ record = new_switch(:state => 'on')
0
+ assert @transition.can_perform_on?(record)
0
   end
0
   
0
- def test_should_perform_for_valid_from_state
0
+ def test_should_perform_for_all_states
0
     record = new_switch(:state => 'off')
0
     assert @transition.perform(record)
0
+
0
+ record = new_switch(:state => 'on')
0
+ assert @transition.perform(record)
0
   end
0
   
0
   def test_should_not_raise_exception_if_not_valid_during_perform
0
@@ -63,58 +68,81 @@ class TransitionTest < Test::Unit::TestCase
0
   end
0
 end
0
 
0
-class TransitionWithoutFromStateTest < Test::Unit::TestCase
0
+class TransitionWithLoopbackTest < Test::Unit::TestCase
0
   def setup
0
     @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
     @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
- @transition = PluginAWeek::StateMachine::Transition.new(@event, nil, 'on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
0
   end
0
   
0
- def test_should_not_have_a_from_state
0
- assert_nil @transition.from_state
0
+ def test_should_be_able_to_perform
0
+ record = new_switch(:state => 'on')
0
+ assert @transition.can_perform_on?(record)
0
   end
0
   
0
- def test_should_be_able_to_perform_on_all_states
0
- record = new_switch(:state => 'off')
0
- assert @transition.can_perform_on?(record)
0
-
0
+ def test_should_perform_for_valid_from_state
0
     record = new_switch(:state => 'on')
0
- assert @transition.can_perform_on?(record)
0
+ assert @transition.perform(record)
0
   end
0
 end
0
 
0
-class TransitionWithLoopbackTest < Test::Unit::TestCase
0
+class TransitionWithFromStateTest < Test::Unit::TestCase
0
   def setup
0
     @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
     @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off')
0
   end
0
   
0
   def test_should_have_a_from_state
0
- assert_equal 'on', @transition.from_state
0
+ assert_equal ['off'], @transition.from_states
0
   end
0
   
0
- def test_should_have_a_to_state
0
- assert_equal 'on', @transition.to_state
0
+ def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
0
+ record = new_switch(:state => 'on')
0
+ assert !@transition.can_perform_on?(record)
0
   end
0
   
0
- def test_should_be_a_loopback
0
- assert @transition.loopback?
0
+ def test_should_be_able_to_perform_if_record_state_is_from_state
0
+ record = new_switch(:state => 'off')
0
+ assert @transition.can_perform_on?(record)
0
   end
0
   
0
- def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
0
+ def test_should_perform_for_valid_from_state
0
     record = new_switch(:state => 'off')
0
+ assert @transition.perform(record)
0
+ end
0
+end
0
+
0
+class TransitionWithMultipleFromStatesTest < Test::Unit::TestCase
0
+ def setup
0
+ @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
+ @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off', 'on')
0
+ end
0
+
0
+ def test_should_have_multiple_from_states
0
+ assert_equal ['off', 'on'], @transition.from_states
0
+ end
0
+
0
+ def test_should_not_be_able_to_perform_if_record_state_is_not_from_state
0
+ record = new_switch(:state => 'unknown')
0
     assert !@transition.can_perform_on?(record)
0
   end
0
   
0
- def test_should_be_able_to_perform_if_record_is_in_from_state
0
+ def test_should_be_able_to_perform_if_record_state_is_any_from_state
0
+ record = new_switch(:state => 'off')
0
+ assert @transition.can_perform_on?(record)
0
+
0
     record = new_switch(:state => 'on')
0
     assert @transition.can_perform_on?(record)
0
   end
0
   
0
- def test_should_perform_for_valid_from_state
0
- record = new_switch(:state => 'on')
0
+ def test_should_perform_for_any_valid_from_state
0
+ record = new_switch(:state => 'off')
0
     assert @transition.perform(record)
0
+
0
+ record = new_switch(:state => 'on')
0
+ assert @transition.can_perform_on?(record)
0
   end
0
 end
0
 
0
@@ -122,7 +150,7 @@ class TransitionAfterBeingPerformedTest < Test::Unit::TestCase
0
   def setup
0
     @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
     @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'off', 'on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off')
0
     
0
     @record = create_switch(:state => 'off')
0
     @transition.perform(@record)
0
@@ -162,7 +190,7 @@ class TransitionWithCallbacksTest < Test::Unit::TestCase
0
   def setup
0
     @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
     @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
- @transition = PluginAWeek::StateMachine::Transition.new(@event, 'off', 'on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on', 'off')
0
     @record = create_switch(:state => 'off')
0
     
0
     Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on
0
@@ -295,7 +323,7 @@ class TransitionWithoutFromStateAndCallbacksTest < Test::Unit::TestCase
0
   def setup
0
     @machine = PluginAWeek::StateMachine::Machine.new(Switch, 'state', :initial => 'off')
0
     @event = PluginAWeek::StateMachine::Event.new(@machine, 'turn_on')
0
- @transition = PluginAWeek::StateMachine::Transition.new(@event, nil, 'on')
0
+ @transition = PluginAWeek::StateMachine::Transition.new(@event, 'on')
0
     @record = create_switch(:state => 'off')
0
     
0
     Switch.define_callbacks :before_exit_state_off, :before_enter_state_on, :after_exit_state_off, :after_enter_state_on

Comments

    No one has commented yet.