state_machine 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 * ', '}]>"