state_machine 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,5 +1,13 @@
1
1
  == master
2
2
 
3
+ == 0.4.2 / 2008-12-28
4
+
5
+ * Fix graphs not being drawn the same way consistently
6
+ * Add support for sharing transitions across multiple events
7
+ * Add support for state-driven behavior
8
+ * Simplify initialize hooks, requiring super to be called instead
9
+ * Add :namespace option for generated state predicates / event methods
10
+
3
11
  == 0.4.1 / 2008-12-16
4
12
 
5
13
  * Fix nil states not being handled properly in guards, known states, or visualizations
data/README.rdoc CHANGED
@@ -36,12 +36,14 @@ state machine is :)
36
36
  Some brief, high-level features include:
37
37
  * Defining state machines on any Ruby class
38
38
  * Multiple state machines on a single class
39
+ * Namespaced state machines
39
40
  * before/after transition hooks with explicit transition requirements
40
41
  * ActiveRecord integration
41
42
  * DataMapper integration
42
43
  * Sequel integration
43
44
  * States of any data type
44
45
  * State predicates
46
+ * State-driven behavior
45
47
  * GraphViz visualization creator
46
48
 
47
49
  Examples of the usage patterns for some of the above features are shown below.
@@ -51,10 +53,14 @@ You can find more detailed documentation in the actual API.
51
53
 
52
54
  === Example
53
55
 
54
- Below is an example of many of the features offered by this plugin, including
56
+ Below is an example of many of the features offered by this plugin, including:
55
57
  * Initial states
58
+ * Namespaced states
56
59
  * Transition callbacks
57
60
  * Conditional transitions
61
+ * State-driven behavior
62
+
63
+ Class definition:
58
64
 
59
65
  class Vehicle
60
66
  attr_accessor :seatbelt_on
@@ -98,10 +104,39 @@ Below is an example of many of the features offered by this plugin, including
98
104
  event :repair do
99
105
  transition :to => 'parked', :from => 'stalled', :if => :auto_shop_busy?
100
106
  end
107
+
108
+ state 'parked' do
109
+ def speed
110
+ 0
111
+ end
112
+ end
113
+
114
+ state 'idling', 'first_gear' do
115
+ def speed
116
+ 10
117
+ end
118
+ end
119
+
120
+ state 'second_gear' do
121
+ def speed
122
+ 20
123
+ end
124
+ end
125
+ end
126
+
127
+ state_machine :hood_state, :initial => 'closed', :namespace => 'hood' do
128
+ event :open do
129
+ transition :to => 'opened', :from => 'closed'
130
+ end
131
+
132
+ event :close do
133
+ transition :to => 'closed', :from => 'opened'
134
+ end
101
135
  end
102
136
 
103
137
  def initialize
104
138
  @seatbelt_on = false
139
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
105
140
  end
106
141
 
107
142
  def put_on_seatbelt
@@ -128,13 +163,20 @@ like so:
128
163
  vehicle.parked? # => true
129
164
  vehicle.can_ignite? # => true
130
165
  vehicle.next_ignite_transition # => #<StateMachine::Transition:0xb7c34cec ...>
166
+ vehicle.speed # => 0
167
+
131
168
  vehicle.ignite # => true
132
169
  vehicle.parked? # => false
133
170
  vehicle.idling? # => true
171
+ vehicle.speed # => 10
134
172
  vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
173
+
135
174
  vehicle.shift_up # => true
175
+ vehicle.speed # => 10
136
176
  vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
177
+
137
178
  vehicle.shift_up # => true
179
+ vehicle.speed # => 20
138
180
  vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
139
181
 
140
182
  # The bang (!) operator can raise exceptions if the event fails
@@ -143,6 +185,18 @@ like so:
143
185
  # Generic state predicates can raise exceptions if the value does not exist
144
186
  vehicle.state?('parked') # => true
145
187
  vehicle.state?('invalid') # => ArgumentError: "parked" is not a known state value
188
+
189
+ # Namespaced machines have uniquely-generated methods
190
+ vehicle.can_open_hood? # => true
191
+ vehicle.open_hood # => true
192
+ vehicle.can_close_hood? # => true
193
+
194
+ vehicle.hood_opened? # => true
195
+ vehicle.hood_closed? # => false
196
+
197
+ *Note* the comment made on the +initialize+ method in the class. In order for
198
+ state machine attributes to be properly initialized, <tt>super()</tt> must be called.
199
+ See StateMachine::MacroMethods for more information about this.
146
200
 
147
201
  == Integrations
148
202
 
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.4.1'
8
+ s.version = '0.4.2'
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
 
Binary file
Binary file
Binary file
Binary file
data/lib/state_machine.rb CHANGED
@@ -9,16 +9,17 @@ module StateMachine
9
9
  # attribute, if not specified, is "state".
10
10
  #
11
11
  # Configuration options:
12
- # * +initial+ - The initial value to set the attribute to. This can be a static value or a dynamic proc which will be evaluated at runtime. Default is nil.
12
+ # * +initial+ - The initial value to set the attribute to. This can be a static value or a lambda block which will be evaluated at runtime. Default is nil.
13
13
  # * +action+ - The action to invoke when an object transitions. Default is nil unless otherwise specified by the configured integration.
14
14
  # * +plural+ - The pluralized name of the attribute. By default, this will attempt to call +pluralize+ on the attribute, otherwise an "s" is appended.
15
- # * +integration+ - The name of the integration to use for adding library-specific behavior to the machine. Built-in integrations include :data_mapper and :active_record. By default, this is determined automatically.
15
+ # * +namespace+ - The name to use for namespacing all generated instance methods (e.g. "email" => "activate_email", "deactivate_email", etc.). Default is no namespace.
16
+ # * +integration+ - The name of the integration to use for adding library-specific behavior to the machine. Built-in integrations include :data_mapper, :active_record, and :sequel. By default, this is determined automatically.
16
17
  #
17
18
  # This also requires a block which will be used to actually configure the
18
- # events and transitions for the state machine. *Note* that this block
19
- # will be executed within the context of the state machine. As a result,
20
- # you will not be able to access any class methods unless you refer to
21
- # them directly (i.e. specifying the class name).
19
+ # states, events and transitions for the state machine. *Note* that this
20
+ # block will be executed within the context of the state machine. As a
21
+ # result, you will not be able to access any class methods unless you refer
22
+ # to them directly (i.e. specifying the class name).
22
23
  #
23
24
  # For examples on the types of configured state machines and blocks, see
24
25
  # the section below.
@@ -95,6 +96,82 @@ module StateMachine
95
96
  # end
96
97
  # end
97
98
  #
99
+ # == Attribute initialization
100
+ #
101
+ # For most classes, the initial values for state machine attributes are
102
+ # automatically assigned when a new object is created. However, this
103
+ # behavior will *not* work if the class defines an +initialize+ method
104
+ # without properly calling +super+.
105
+ #
106
+ # For example,
107
+ #
108
+ # class Vehicle
109
+ # state_machine :state, :initial => 'parked' do
110
+ # ...
111
+ # end
112
+ # end
113
+ #
114
+ # v = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
115
+ # v.state # => "parked"
116
+ #
117
+ # In the above example, no +initialize+ method is defined. As a result,
118
+ # the default behavior of initializing the state machine attributes is used.
119
+ #
120
+ # In the following example, a custom +initialize+ method is defined:
121
+ #
122
+ # class Vehicle
123
+ # state_machine :state, :initial => 'parked' do
124
+ # ...
125
+ # end
126
+ #
127
+ # def initialize
128
+ # end
129
+ # end
130
+ #
131
+ # v = Vehicle.new # => #<Vehicle:0xb7c77678>
132
+ # v.state # => nil
133
+ #
134
+ # Since the +initialize+ method is defined, the state machine attributes
135
+ # never get initialized. In order to ensure that all initialization hooks
136
+ # are called, the custom method *must* call +super+ without any arguments
137
+ # like so:
138
+ #
139
+ # class Vehicle
140
+ # state_machine :state, :initial => 'parked' do
141
+ # ...
142
+ # end
143
+ #
144
+ # def initialize(attributes = {})
145
+ # ...
146
+ # super()
147
+ # end
148
+ # end
149
+ #
150
+ # v = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
151
+ # v.state # => "parked"
152
+ #
153
+ # Because of the way the inclusion of modules works in Ruby, calling <tt>super()</tt>
154
+ # will not only call the superclass's +initialize+, but also +initialize+ on
155
+ # all included modules. This allows the original state machine hook to get
156
+ # called properly.
157
+ #
158
+ # If you want to avoid calling the superclass's constructor, but still want
159
+ # to initialize the state machine attributes:
160
+ #
161
+ # class Vehicle
162
+ # state_machine :state, :initial => 'parked' do
163
+ # ...
164
+ # end
165
+ #
166
+ # def initialize(attributes = {})
167
+ # ...
168
+ # initialize_state_machines
169
+ # end
170
+ # end
171
+ #
172
+ # v = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
173
+ # v.state # => "parked"
174
+ #
98
175
  # == States
99
176
  #
100
177
  # All of the valid states for the machine are automatically tracked based
@@ -103,8 +180,8 @@ module StateMachine
103
180
  # explicitly added using the StateMachine::Machine#other_states
104
181
  # helper.
105
182
  #
106
- # For each state tracked, a predicate method for that state is generated
107
- # on the class. For example,
183
+ # When using String or Symbol-based states, a predicate method for that
184
+ # state is generated on the class. For example,
108
185
  #
109
186
  # class Vehicle
110
187
  # state_machine :initial => 'parked' do
@@ -122,6 +199,11 @@ module StateMachine
122
199
  # Each predicate method will return true if it matches the object's
123
200
  # current state. Otherwise, it will return false.
124
201
  #
202
+ # When a namespace is configured for a state machine, the name will be
203
+ # prepended to each state predicate like so:
204
+ # * <tt>car_parked?</tt>
205
+ # * <tt>car_idling?</tt>
206
+ #
125
207
  # == Events and Transitions
126
208
  #
127
209
  # For more information about how to configure an event and its associated
@@ -130,10 +212,65 @@ module StateMachine
130
212
  # == Defining callbacks
131
213
  #
132
214
  # Within the +state_machine+ block, you can also define callbacks for
133
- # particular states. For more information about defining these callbacks,
215
+ # transitions. For more information about defining these callbacks,
134
216
  # see StateMachine::Machine#before_transition and
135
217
  # StateMachine::Machine#after_transition.
136
218
  #
219
+ # == Namespaces
220
+ #
221
+ # When a namespace is configured for a state machine, the name provided will
222
+ # be used in generating the instance methods for interacting with
223
+ # events/states in the machine. This is particularly useful when a class
224
+ # has multiple state machines and it would be difficult to differentiate
225
+ # between the various states / events.
226
+ #
227
+ # For example,
228
+ #
229
+ # class Vehicle
230
+ # state_machine :heater_state, :initial => 'off' :namespace => 'heater' do
231
+ # event :turn_on do
232
+ # transition :to => 'on', :from => 'off'
233
+ # end
234
+ #
235
+ # event :turn_off do
236
+ # transition :to => 'off', :from => 'on'
237
+ # end
238
+ # end
239
+ #
240
+ # state_machine :hood_state, :initial => 'closed', :namespace => 'hood' do
241
+ # event :open do
242
+ # transition :to => 'opened', :from => 'closed'
243
+ # end
244
+ #
245
+ # event :close do
246
+ # transition :to => 'closed', :from => 'opened'
247
+ # end
248
+ # end
249
+ # end
250
+ #
251
+ # The above class defines two state machines: +heater_state+ and +hood_state+.
252
+ # For the +heater_state+ machine, the following methods are generated since
253
+ # it's namespaced by "heater":
254
+ # * <tt>can_turn_on_heater?</tt>
255
+ # * <tt>turn_on_heater</tt>
256
+ # * ...
257
+ # * <tt>can_turn_off_heater?</tt>
258
+ # * <tt>turn_off_heater</tt>
259
+ # * ..
260
+ # * <tt>heater_off?</tt>
261
+ # * <tt>heater_on?</tt>
262
+ #
263
+ # As shown, each method is unique to the state machine so that the states
264
+ # and events don't conflict. The same goes for the +hood_state+ machine:
265
+ # * <tt>can_open_hood?</tt>
266
+ # * <tt>open_hood</tt>
267
+ # * ...
268
+ # * <tt>can_close_hood?</tt>
269
+ # * <tt>close_hood</tt>
270
+ # * ..
271
+ # * <tt>hood_open?</tt>
272
+ # * <tt>hood_closed?</tt>
273
+ #
137
274
  # == Scopes
138
275
  #
139
276
  # For integrations that support it, a group of default scope filters will
@@ -1,5 +1,5 @@
1
1
  module StateMachine
2
- # Provides a set of helper methods for making assertions about content of
2
+ # Provides a set of helper methods for making assertions about the content of
3
3
  # various objects
4
4
  module Assertions
5
5
  # Validates that all keys in the given hash *only* includes the specified
@@ -58,8 +58,8 @@ module StateMachine
58
58
  # An optional block for determining whether to cancel the callback chain
59
59
  # based on the return value of the callback. By default, the callback
60
60
  # chain never cancels based on the return value (i.e. there is no implicit
61
- # terminator). Certain integrations, such as ActiveRecord, change this
62
- # default value.
61
+ # terminator). Certain integrations, such as ActiveRecord and Sequel,
62
+ # change this default value.
63
63
  #
64
64
  # == Examples
65
65
  #
@@ -148,6 +148,7 @@ module StateMachine
148
148
  # when the callback is invoked
149
149
  def bound_method(block)
150
150
  # Generate a thread-safe unbound method that can be used on any object
151
+ # This is essentially a workaround for not having Ruby 1.9's instance_exec
151
152
  unbound_method = Object.class_eval do
152
153
  time = Time.now
153
154
  method_name = "__bind_#{time.to_i}_#{time.usec}"
@@ -44,7 +44,7 @@ module StateMachine
44
44
  # Creates a new transition that will be evaluated when the event is fired.
45
45
  #
46
46
  # Configuration options:
47
- # * +to+ - The state that being transitioned to. If not specified, then the transition will not change the state.
47
+ # * +to+ - The state that's being transitioned to. If not specified, then the transition will not change the state.
48
48
  # * +from+ - A state or array of states that can be transitioned from. If not specified, then the transition can occur for *any* from state.
49
49
  # * +except_from+ - A state or array of states that *cannot* be transitioned from.
50
50
  # * +if+ - Specifies a method, proc or string to call to determine if the transition 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.
@@ -119,32 +119,43 @@ module StateMachine
119
119
  end
120
120
  end
121
121
 
122
+ # Draws a representation of this event on the given graph. This will
123
+ # create 1 or more edges on the graph for each guard (i.e. transition)
124
+ # configured.
125
+ #
126
+ # A collection of the generated edges will be returned.
127
+ def draw(graph)
128
+ valid_states = machine.states_order
129
+ guards.collect {|guard| guard.draw(graph, name, valid_states)}.flatten
130
+ end
131
+
122
132
  protected
123
133
  # Add the various instance methods that can transition the object using
124
134
  # the current event
125
135
  def add_actions
126
136
  attribute = machine.attribute
127
- name = self.name
137
+ qualified_name = name = self.name
138
+ qualified_name = "#{name}_#{machine.namespace}" if machine.namespace
128
139
 
129
140
  machine.owner_class.class_eval do
130
141
  # Checks whether the event can be fired on the current object
131
- define_method("can_#{name}?") do
132
- self.class.state_machines[attribute].events[name].can_fire?(self)
142
+ define_method("can_#{qualified_name}?") do
143
+ self.class.state_machines[attribute].event(name).can_fire?(self)
133
144
  end
134
145
 
135
146
  # Gets the next transition that would be performed if the event were to be fired now
136
- define_method("next_#{name}_transition") do
137
- self.class.state_machines[attribute].events[name].next_transition(self)
147
+ define_method("next_#{qualified_name}_transition") do
148
+ self.class.state_machines[attribute].event(name).next_transition(self)
138
149
  end
139
150
 
140
151
  # Fires the event
141
- define_method(name) do |*args|
142
- self.class.state_machines[attribute].events[name].fire(self, *args)
152
+ define_method(qualified_name) do |*args|
153
+ self.class.state_machines[attribute].event(name).fire(self, *args)
143
154
  end
144
155
 
145
156
  # Fires the event, raising an exception if it fails to transition
146
- define_method("#{name}!") do |*args|
147
- send(name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition via :#{name} from #{send(attribute).inspect}")
157
+ define_method("#{qualified_name}!") do |*args|
158
+ send(qualified_name, *args) || raise(StateMachine::InvalidTransition, "Cannot transition #{attribute} via :#{name} from #{send(attribute).inspect}")
148
159
  end
149
160
  end
150
161
  end
@@ -3,46 +3,6 @@ module StateMachine
3
3
  def self.extended(base) #:nodoc:
4
4
  base.class_eval do
5
5
  @state_machines = {}
6
-
7
- # method_added may get defined by the class, so instead it's chained
8
- class << self
9
- alias_method :method_added_without_state_machine, :method_added
10
- alias_method :method_added, :method_added_with_state_machine
11
- end
12
- end
13
- end
14
-
15
- # Ensures that the +initialize+ hook defined in StateMachine::InstanceMethods
16
- # remains there even if the class defines its own +initialize+ method
17
- # *after* the state machine has been defined. For example,
18
- #
19
- # class Switch
20
- # state_machine do
21
- # ...
22
- # end
23
- #
24
- # def initialize(attributes = {})
25
- # ...
26
- # end
27
- # end
28
- def method_added_with_state_machine(method) #:nodoc:
29
- method_added_without_state_machine(method)
30
-
31
- # Aliasing the +initialize+ method also invokes +method_added+, so
32
- # alias processing is tracked to prevent an infinite loop
33
- if !@skip_initialize_hook && [:initialize, :initialize_with_state_machine].include?(method)
34
- @skip_initialize_hook = true
35
-
36
- # Re-defining +initialize+ instead of alias chaining is done in order to
37
- # prevent +initialize+ from showing up in #instance_methods
38
- alias_method :initialize_without_state_machine, :initialize
39
- class_eval <<-end_eval, __FILE__, __LINE__
40
- def initialize(*args, &block)
41
- initialize_with_state_machine(*args, &block)
42
- end
43
- end_eval
44
-
45
- @skip_initialize_hook = false
46
6
  end
47
7
  end
48
8
 
@@ -51,7 +11,7 @@ module StateMachine
51
11
  # is available to each subclass, each subclass having a copy of its
52
12
  # superclass's attribute.
53
13
  #
54
- # The hash of state machines maps +name+ => +machine+, e.g.
14
+ # The hash of state machines maps +attribute+ => +machine+, e.g.
55
15
  #
56
16
  # Vehicle.state_machines # => {"state" => #<StateMachine::Machine:0xb6f6e4a4 ...>
57
17
  def state_machines
@@ -60,25 +20,23 @@ module StateMachine
60
20
  end
61
21
 
62
22
  module InstanceMethods
63
- def self.included(base) #:nodoc:
64
- # Methods added from an included module don't invoke +method_added+,
65
- # triggering the initialize alias, so it's done explicitly
66
- base.method_added(:initialize_with_state_machine)
67
- end
68
-
69
23
  # Defines the initial values for state machine attributes. The values
70
24
  # will be set *after* the original initialize method is invoked. This is
71
25
  # necessary in order to ensure that the object is initialized before
72
26
  # dynamic initial attributes are evaluated.
73
- def initialize_with_state_machine(*args, &block)
74
- initialize_without_state_machine(*args, &block)
75
-
76
- self.class.state_machines.each do |attribute, machine|
77
- # Set the initial value of the machine's attribute unless it already
78
- # exists (which must mean the defaults are being skipped)
79
- value = send(attribute)
80
- send("#{attribute}=", machine.initial_state(self)) if value.nil? || value.respond_to?(:empty?) && value.empty?
81
- end
27
+ def initialize(*args, &block)
28
+ super
29
+ initialize_state_machines
82
30
  end
31
+
32
+ protected
33
+ def initialize_state_machines #:nodoc:
34
+ self.class.state_machines.each do |attribute, machine|
35
+ # Set the initial value of the machine's attribute unless it already
36
+ # exists (which must mean the defaults are being skipped)
37
+ value = send(attribute)
38
+ send("#{attribute}=", machine.initial_state(self)) if value.nil? || value.respond_to?(:empty?) && value.empty?
39
+ end
40
+ end
83
41
  end
84
42
  end