state_machine 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,14 @@
1
1
  == master
2
2
 
3
+ == 0.6.0 / 2009-03-03
4
+
5
+ * Allow multiple conditions for callbacks / class behaviors
6
+ * Add support for state-driven class behavior with :if/:unless options
7
+ * Alias Machine#event as Machine#on
8
+ * Fix nil from/to states not being handled properly
9
+ * Simplify hooking callbacks into loopbacks
10
+ * Add simplified transition/callback requirement syntax
11
+
3
12
  == 0.5.2 / 2009-02-17
4
13
 
5
14
  * Improve pretty-print of events
data/README.rdoc CHANGED
@@ -42,7 +42,7 @@ Some brief, high-level features include:
42
42
  * DataMapper integration
43
43
  * Sequel integration
44
44
  * State predicates
45
- * State-driven behavior
45
+ * State-driven instance / class behavior
46
46
  * State values of any data type
47
47
  * Dynamically-generated state values
48
48
  * Inheritance
@@ -60,7 +60,7 @@ Below is an example of many of the features offered by this plugin, including:
60
60
  * Namespaced states
61
61
  * Transition callbacks
62
62
  * Conditional transitions
63
- * State-driven behavior
63
+ * State-driven instance behavior
64
64
  * Customized state values
65
65
 
66
66
  Class definition:
@@ -69,43 +69,39 @@ Class definition:
69
69
  attr_accessor :seatbelt_on
70
70
 
71
71
  state_machine :state, :initial => :parked do
72
- before_transition :from => [:parked, :idling], :do => :put_on_seatbelt
72
+ before_transition any => [:parked, :idling], :do => :put_on_seatbelt
73
73
  after_transition :on => :crash, :do => :tow
74
74
  after_transition :on => :repair, :do => :fix
75
- after_transition :to => :parked do |vehicle, transition|
75
+ after_transition any => :parked do |vehicle, transition|
76
76
  vehicle.seatbelt_on = false
77
77
  end
78
78
 
79
79
  event :park do
80
- transition :to => :parked, :from => [:idling, :first_gear]
80
+ transition [:idling, :first_gear] => :parked
81
81
  end
82
82
 
83
83
  event :ignite do
84
- transition :to => :stalled, :from => :stalled
85
- transition :to => :idling, :from => :parked
84
+ transition :stalled => :stalled, :parked => :idling
86
85
  end
87
86
 
88
87
  event :idle do
89
- transition :to => :idling, :from => :first_gear
88
+ transition :first_gear => :idling
90
89
  end
91
90
 
92
91
  event :shift_up do
93
- transition :to => :first_gear, :from => :idling
94
- transition :to => :second_gear, :from => :first_gear
95
- transition :to => :third_gear, :from => :second_gear
92
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
96
93
  end
97
94
 
98
95
  event :shift_down do
99
- transition :to => :second_gear, :from => :third_gear
100
- transition :to => :first_gear, :from => :second_gear
96
+ transition :third_gear => :second_gear, :second_gear => :first_gear
101
97
  end
102
98
 
103
99
  event :crash do
104
- transition :to => :stalled, :from => [:first_gear, :second_gear, :third_gear], :unless => :auto_shop_busy?
100
+ transition [:first_gear, :second_gear, :third_gear] => :stalled, :unless => :auto_shop_busy?
105
101
  end
106
102
 
107
103
  event :repair do
108
- transition :to => :parked, :from => :stalled, :if => :auto_shop_busy?
104
+ transition :stalled => :parked, :if => :auto_shop_busy?
109
105
  end
110
106
 
111
107
  state :parked do
@@ -129,11 +125,11 @@ Class definition:
129
125
 
130
126
  state_machine :hood_state, :initial => :closed, :namespace => 'hood' do
131
127
  event :open do
132
- transition :to => :opened
128
+ transition all => :opened
133
129
  end
134
130
 
135
131
  event :close do
136
- transition :to => :closed
132
+ transition all => :closed
137
133
  end
138
134
 
139
135
  state :opened, :value => 1
@@ -232,13 +228,17 @@ saving the record, named scopes, and observers. For example,
232
228
 
233
229
  class Vehicle < ActiveRecord::Base
234
230
  state_machine :initial => :parked do
235
- before_transition :to => :idling, :do => :put_on_seatbelt
236
- after_transition :to => :parked do |vehicle, transition|
231
+ before_transition any => :idling, :do => :put_on_seatbelt
232
+ after_transition any => :parked do |vehicle, transition|
237
233
  vehicle.seatbelt = 'off'
238
234
  end
239
235
 
240
236
  event :ignite do
241
- transition :to => :idling, :from => :parked
237
+ transition :parked => :idling
238
+ end
239
+
240
+ state :first_gear, :second_gear do
241
+ validates_presence_of :seatbelt_on
242
242
  end
243
243
  end
244
244
 
@@ -275,13 +275,17 @@ callbacks, and observers. For example,
275
275
  property :state, String
276
276
 
277
277
  state_machine :initial => :parked do
278
- before_transition :to => :idling, :do => :put_on_seatbelt
279
- after_transition :to => :parked do |transition|
278
+ before_transition any => :idling, :do => :put_on_seatbelt
279
+ after_transition any => :parked do |transition|
280
280
  self.seatbelt = 'off' # self is the record
281
281
  end
282
282
 
283
283
  event :ignite do
284
- transition :to => :idling, :from => :parked
284
+ transition :parked => :idling
285
+ end
286
+
287
+ state :first_gear, :second_gear do
288
+ validates_present :seatbelt_on
285
289
  end
286
290
  end
287
291
 
@@ -320,13 +324,17 @@ callbacks. For example,
320
324
 
321
325
  class Vehicle < Sequel::Model
322
326
  state_machine :initial => :parked do
323
- before_transition :to => :idling, :do => :put_on_seatbelt
324
- after_transition :to => :parked do |transition|
327
+ before_transition any => :idling, :do => :put_on_seatbelt
328
+ after_transition any => :parked do |transition|
325
329
  self.seatbelt = 'off' # self is the record
326
330
  end
327
331
 
328
332
  event :ignite do
329
- transition :to => :idling, :from => :parked
333
+ transition :parked => :idling
334
+ end
335
+
336
+ state :first_gear, :second_gear do
337
+ validates_presence_of :seatbelt_on
330
338
  end
331
339
  end
332
340
 
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/contrib/sshpublisher'
5
5
 
6
6
  spec = Gem::Specification.new do |s|
7
7
  s.name = 'state_machine'
8
- s.version = '0.5.2'
8
+ s.version = '0.6.0'
9
9
  s.platform = Gem::Platform::RUBY
10
10
  s.summary = 'Adds support for creating state machines for attributes on any Ruby class'
11
11
 
@@ -1,11 +1,11 @@
1
1
  class AutoShop
2
2
  state_machine :initial => :available do
3
3
  event :tow_vehicle do
4
- transition :to => :busy, :from => :available
4
+ transition :available => :busy
5
5
  end
6
6
 
7
7
  event :fix_vehicle do
8
- transition :to => :available, :from => :busy
8
+ transition :busy => :available
9
9
  end
10
10
  end
11
11
  end
data/examples/car.rb CHANGED
@@ -1,19 +1,19 @@
1
1
  class Car < Vehicle
2
2
  state_machine do
3
3
  event :reverse do
4
- transition :to => :backing_up, :from => [:parked, :idling, :first_gear]
4
+ transition [:parked, :idling, :first_gear] => :backing_up
5
5
  end
6
6
 
7
7
  event :park do
8
- transition :to => :parked, :from => :backing_up
8
+ transition :backing_up => :parked
9
9
  end
10
10
 
11
11
  event :idle do
12
- transition :to => :idling, :from => :backing_up
12
+ transition :backing_up => :idling
13
13
  end
14
14
 
15
15
  event :shift_up do
16
- transition :to => :first_gear, :from => :backing_up
16
+ transition :backing_up => :first_gear
17
17
  end
18
18
  end
19
19
  end
@@ -1,9 +1,7 @@
1
1
  class TrafficLight
2
2
  state_machine :initial => :stop do
3
3
  event :cycle do
4
- transition :to => :proceed, :from => :stop
5
- transition :to => :caution, :from => :proceed
6
- transition :to => :stop, :from => :caution
4
+ transition :stop => :proceed, :proceed => :caution, :caution => :stop
7
5
  end
8
6
  end
9
7
  end
data/examples/vehicle.rb CHANGED
@@ -1,35 +1,31 @@
1
1
  class Vehicle
2
2
  state_machine :initial => :parked do
3
3
  event :park do
4
- transition :to => :parked, :from => [:idling, :first_gear]
4
+ transition [:idling, :first_gear] => :parked
5
5
  end
6
6
 
7
7
  event :ignite do
8
- transition :to => :stalled, :from => :stalled
9
- transition :to => :idling, :from => :parked
8
+ transition :stalled => same, :parked => :idling
10
9
  end
11
10
 
12
11
  event :idle do
13
- transition :to => :idling, :from => :first_gear
12
+ transition :first_gear => :idling
14
13
  end
15
14
 
16
15
  event :shift_up do
17
- transition :to => :first_gear, :from => :idling
18
- transition :to => :second_gear, :from => :first_gear
19
- transition :to => :third_gear, :from => :second_gear
16
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
20
17
  end
21
18
 
22
19
  event :shift_down do
23
- transition :to => :second_gear, :from => :third_gear
24
- transition :to => :first_gear, :from => :second_gear
20
+ transition :third_gear => :second_gear, :second_gear => :first_gear
25
21
  end
26
22
 
27
23
  event :crash do
28
- transition :to => :stalled, :from => [:first_gear, :second_gear, :third_gear]
24
+ transition [:first_gear, :second_gear, :third_gear] => :stalled
29
25
  end
30
26
 
31
27
  event :repair do
32
- transition :to => :parked, :from => :stalled
28
+ transition :stalled => :parked
33
29
  end
34
30
  end
35
31
  end
data/lib/state_machine.rb CHANGED
@@ -199,7 +199,7 @@ module StateMachine
199
199
  # class Vehicle
200
200
  # state_machine :initial => :parked do
201
201
  # event :ignite do
202
- # transition :to => :idling
202
+ # transition all => :idling
203
203
  # end
204
204
  # end
205
205
  # end
@@ -242,21 +242,21 @@ module StateMachine
242
242
  # class Vehicle
243
243
  # state_machine :heater_state, :initial => :off :namespace => 'heater' do
244
244
  # event :turn_on do
245
- # transition :to => :on
245
+ # transition all => :on
246
246
  # end
247
247
  #
248
248
  # event :turn_off do
249
- # transition :to => :off
249
+ # transition all => :off
250
250
  # end
251
251
  # end
252
252
  #
253
253
  # state_machine :hood_state, :initial => :closed, :namespace => 'hood' do
254
254
  # event :open do
255
- # transition :to => :opened
255
+ # transition all => :opened
256
256
  # end
257
257
  #
258
258
  # event :close do
259
- # transition :to => :closed
259
+ # transition all => :closed
260
260
  # end
261
261
  # end
262
262
  # end
@@ -127,7 +127,7 @@ module StateMachine
127
127
  end
128
128
 
129
129
  # Gets a list of the states known to this callback by looking at the
130
- # guard's requirements
130
+ # guard's known states
131
131
  def known_states
132
132
  guard.known_states
133
133
  end
@@ -0,0 +1,94 @@
1
+ require 'state_machine/eval_helpers'
2
+
3
+ module StateMachine
4
+ # Represents a type of module in which class-level methods are proxied to
5
+ # another class, injecting a custom :if condition along with method.
6
+ #
7
+ # This is used for being able to automatically include conditionals which
8
+ # check the current state in class-level methods that have configuration
9
+ # options.
10
+ #
11
+ # == Examples
12
+ #
13
+ # class Vehicle
14
+ # class << self
15
+ # attr_accessor :validations
16
+ #
17
+ # def validate(options, &block)
18
+ # validations << options
19
+ # end
20
+ # end
21
+ #
22
+ # self.validations = []
23
+ # attr_accessor :state, :simulate
24
+ #
25
+ # def moving?
26
+ # self.class.validations.all? {|validation| validation[:if].call(self)}
27
+ # end
28
+ # end
29
+ #
30
+ # In the above class, a simple set of validation behaviors have been defined.
31
+ # Each validation consists of a configuration like so:
32
+ #
33
+ # Vehicle.validate :unless => :simulate
34
+ # Vehicle.validate :if => lambda {|vehicle| ...}
35
+ #
36
+ # In order to scope conditions, a condition proxy can be created to the
37
+ # Vehicle class. For example,
38
+ #
39
+ # proxy = StateMachine::ConditionProxy.new(Vehicle, lambda {|vehicle| vehicle.state == 'first_gear'})
40
+ # proxy.validate(:unless => :simulate)
41
+ #
42
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
43
+ # vehicle.moving? # => false
44
+ #
45
+ # vehicle.state = 'first_gear'
46
+ # vehicle.moving? # => true
47
+ #
48
+ # vehicle.simulate = true
49
+ # vehicle.moving? # => false
50
+ class ConditionProxy < Module
51
+ include EvalHelpers
52
+
53
+ # Creates a new proxy to the given class, merging in the given condition
54
+ def initialize(klass, condition)
55
+ @klass = klass
56
+ @condition = condition
57
+ end
58
+
59
+ # Hooks in condition merging to methods that don't exist in this module
60
+ def method_missing(*args, &block)
61
+ # Get the configuration
62
+ if args.last.is_a?(Hash)
63
+ options = args.last
64
+ else
65
+ args << options = {}
66
+ end
67
+
68
+ # Get any existing condition that may need to be merged
69
+ if_condition = options.delete(:if)
70
+ unless_condition = options.delete(:unless)
71
+
72
+ # Provide scope access to configuration in case the block is evaluated
73
+ # within the object instance
74
+ proxy = self
75
+ proxy_condition = @condition
76
+
77
+ # Replace the configuration condition with the one configured for this
78
+ # proxy, merging together any existing conditions
79
+ options[:if] = lambda do |*args|
80
+ # Block may be executed within the context of the actual object, so it'll
81
+ # either be the first argument or the executing context
82
+ object = args.first || self
83
+
84
+ proxy.evaluate_method(object, proxy_condition) &&
85
+ Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
86
+ !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
87
+ end
88
+
89
+ # Evaluate the method on the original class with the condition proxied
90
+ # through
91
+ @klass.send(*args, &block)
92
+ end
93
+ end
94
+ end
@@ -1,6 +1,7 @@
1
1
  require 'state_machine/transition'
2
2
  require 'state_machine/guard'
3
3
  require 'state_machine/assertions'
4
+ require 'state_machine/matcher_helpers'
4
5
 
5
6
  module StateMachine
6
7
  # An event defines an action that transitions an attribute from one state to
@@ -8,6 +9,7 @@ module StateMachine
8
9
  # guards configured for the event.
9
10
  class Event
10
11
  include Assertions
12
+ include MatcherHelpers
11
13
 
12
14
  # The state machine for which this event is defined
13
15
  attr_accessor :machine
@@ -41,15 +43,75 @@ module StateMachine
41
43
  @known_states = @known_states.dup
42
44
  end
43
45
 
44
- # Creates a new transition that will be evaluated when the event is fired.
46
+ # Creates a new transition that determines what to change the current state
47
+ # to when this event fires.
45
48
  #
46
- # Configuration options:
49
+ # == Defining transitions
50
+ #
51
+ # The options for a new transition uses the Hash syntax to map beginning
52
+ # states to ending states. For example,
53
+ #
54
+ # transition :parked => :idling, :idling => :first_gear
55
+ #
56
+ # In this case, when the event is fired, this transition will cause the
57
+ # state to be +idling+ if it's current state is +parked+ or +first_gear+ if
58
+ # it's current state is +idling+.
59
+ #
60
+ # To help defining these implicit transitions, a set of helpers are available
61
+ # for defining slightly more complex matching:
62
+ # * <tt>all</tt> - Matches every state in the machine
63
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
64
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
65
+ # * <tt>same</tt> - Matches the same state being transitioned from
66
+ #
67
+ # See StateMachine::MatcherHelpers for more information.
68
+ #
69
+ # Examples:
70
+ #
71
+ # transition all => nil # Transitions to nil regardless of the current state
72
+ # transition all => :idling # Transitions to :idling regardless of the current state
73
+ # transition all - [:idling, :first_gear] => :idling # Transitions every state but :idling and :first_gear to :idling
74
+ # transition nil => :idling # Transitions to :idling from the nil state
75
+ # transition :parked => :idling # Transitions to :idling if :parked
76
+ # transition [:parked, :stalled] => :idling # Transitions to :idling if :parked or :stalled
77
+ #
78
+ # transition :parked => same # Loops :parked back to :parked
79
+ # transition [:parked, :stalled] => same # Loops either :parked or :stalled back to the same state
80
+ # transition all - :parked => same # Loops every state but :parked back to the same state
81
+ #
82
+ # == Verbose transitions
83
+ #
84
+ # Transitions can also be defined use an explicit set of deprecated
85
+ # configuration options:
47
86
  # * <tt>:from</tt> - A state or array of states that can be transitioned from.
48
87
  # If not specified, then the transition can occur for *any* state.
49
88
  # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
50
89
  # then the transition will simply loop back (i.e. the state will not change).
51
90
  # * <tt>:except_from</tt> - A state or array of states that *cannot* be
52
91
  # transitioned from.
92
+ #
93
+ # Examples:
94
+ #
95
+ # transition :to => nil
96
+ # transition :to => :idling
97
+ # transition :except_from => [:idling, :first_gear], :to => :idling
98
+ # transition :from => nil, :to => :idling
99
+ # transition :from => [:parked, :stalled], :to => :idling
100
+ #
101
+ # transition :from => :parked
102
+ # transition :from => [:parked, :stalled]
103
+ # transition :except_from => :parked
104
+ #
105
+ # Notice that the above examples are the verbose equivalent of the examples
106
+ # described initially.
107
+ #
108
+ # == Conditions
109
+ #
110
+ # In addition to the state requirements for each transition, a condition
111
+ # can also be defined to help determine whether that transition is
112
+ # available. These options will work on both the normal and verbose syntax.
113
+ #
114
+ # Configuration options:
53
115
  # * <tt>:if</tt> - A method, proc or string to call to determine if the
54
116
  # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
55
117
  # The condition should return or evaluate to true or false.
@@ -57,26 +119,25 @@ module StateMachine
57
119
  # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
58
120
  # The condition should return or evaluate to true or false.
59
121
  #
122
+ # Examples:
123
+ #
124
+ # transition :parked => :idling, :if => :moving?
125
+ # transition :parked => :idling, :unless => :stopped?
126
+ #
127
+ # transition :from => :parked, :to => :idling, :if => :moving?
128
+ # transition :from => :parked, :to => :idling, :unless => :stopped?
129
+ #
60
130
  # == Order of operations
61
131
  #
62
132
  # Transitions are evaluated in the order in which they're defined. As a
63
133
  # result, if more than one transition applies to a given object, then the
64
134
  # first transition that matches will be performed.
65
- #
66
- # == Examples
67
- #
68
- # transition :from => nil, :to => :parked
69
- # transition :from => [:first_gear, :reverse]
70
- # transition :except_from => :parked
71
- # transition :to => nil
72
- # transition :to => :parked
73
- # transition :to => :parked, :from => :first_gear
74
- # transition :to => :parked, :from => [:first_gear, :reverse]
75
- # transition :to => :parked, :from => :first_gear, :if => :moving?
76
- # transition :to => :parked, :from => :first_gear, :unless => :stopped?
77
- # transition :to => :parked, :except_from => :parked
78
135
  def transition(options)
79
- assert_valid_keys(options, :from, :to, :except_from, :if, :unless)
136
+ raise ArgumentError, 'Must specify as least one transition requirement' if options.empty?
137
+
138
+ # Only a certain subset of explicit options are allowed for transition
139
+ # requirements
140
+ assert_valid_keys(options, :from, :to, :except_from, :if, :unless) if (options.keys - [:from, :to, :on, :except_from, :except_to, :except_on, :if, :unless]).empty?
80
141
 
81
142
  guards << guard = Guard.new(options)
82
143
  @known_states |= guard.known_states
@@ -96,11 +157,17 @@ module StateMachine
96
157
  def next_transition(object)
97
158
  from = machine.state_for(object).name
98
159
 
99
- if guard = guards.find {|guard| guard.matches?(object, :from => from)}
100
- # Guard allows for the transition to occur
101
- to = guard.state_requirement[:to].values.any? ? guard.state_requirement[:to].values.first : from
102
- Transition.new(object, machine, name, from, to)
160
+ guards.each do |guard|
161
+ if match = guard.match(object, :from => from)
162
+ # Guard allows for the transition to occur
163
+ to = match[:to].values.empty? ? from : match[:to].values.first
164
+
165
+ return Transition.new(object, machine, name, from, to)
166
+ end
103
167
  end
168
+
169
+ # No transition matched
170
+ nil
104
171
  end
105
172
 
106
173
  # Attempts to perform the next available transition on the given object.
@@ -139,11 +206,13 @@ module StateMachine
139
206
  # For example,
140
207
  #
141
208
  # event = StateMachine::Event.new(machine, :park)
142
- # event.transition :to => :parked, :from => :idling
143
- # event # => #<StateMachine::Event name=:park transitions=[:idling => :parked]>
209
+ # event.transition all - :idling => :parked, :idling => same
210
+ # event # => #<StateMachine::Event name=:park transitions=[all - :idling => :parked, :idling => same]>
144
211
  def inspect
145
212
  transitions = guards.map do |guard|
146
- "#{guard.state_requirement[:from].description} => #{guard.state_requirement[:to].description}"
213
+ guard.state_requirements.map do |state_requirement|
214
+ "#{state_requirement[:from].description} => #{state_requirement[:to].description}"
215
+ end * ', '
147
216
  end
148
217
 
149
218
  "#<#{self.class} name=#{name.inspect} transitions=[#{transitions * ', '}]>"