state_machine 0.4.1 → 0.4.2

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,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