joelind-state_machine 0.8.1

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.
Files changed (78) hide show
  1. data/CHANGELOG.rdoc +297 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +466 -0
  4. data/Rakefile +98 -0
  5. data/examples/AutoShop_state.png +0 -0
  6. data/examples/Car_state.png +0 -0
  7. data/examples/TrafficLight_state.png +0 -0
  8. data/examples/Vehicle_state.png +0 -0
  9. data/examples/auto_shop.rb +11 -0
  10. data/examples/car.rb +19 -0
  11. data/examples/merb-rest/controller.rb +51 -0
  12. data/examples/merb-rest/model.rb +28 -0
  13. data/examples/merb-rest/view_edit.html.erb +24 -0
  14. data/examples/merb-rest/view_index.html.erb +23 -0
  15. data/examples/merb-rest/view_new.html.erb +13 -0
  16. data/examples/merb-rest/view_show.html.erb +17 -0
  17. data/examples/rails-rest/controller.rb +43 -0
  18. data/examples/rails-rest/migration.rb +11 -0
  19. data/examples/rails-rest/model.rb +23 -0
  20. data/examples/rails-rest/view_edit.html.erb +25 -0
  21. data/examples/rails-rest/view_index.html.erb +23 -0
  22. data/examples/rails-rest/view_new.html.erb +14 -0
  23. data/examples/rails-rest/view_show.html.erb +17 -0
  24. data/examples/traffic_light.rb +7 -0
  25. data/examples/vehicle.rb +31 -0
  26. data/init.rb +1 -0
  27. data/lib/state_machine.rb +388 -0
  28. data/lib/state_machine/assertions.rb +36 -0
  29. data/lib/state_machine/callback.rb +189 -0
  30. data/lib/state_machine/condition_proxy.rb +94 -0
  31. data/lib/state_machine/eval_helpers.rb +67 -0
  32. data/lib/state_machine/event.rb +252 -0
  33. data/lib/state_machine/event_collection.rb +122 -0
  34. data/lib/state_machine/extensions.rb +149 -0
  35. data/lib/state_machine/guard.rb +230 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +492 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +11 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +351 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +322 -0
  43. data/lib/state_machine/machine.rb +1467 -0
  44. data/lib/state_machine/machine_collection.rb +155 -0
  45. data/lib/state_machine/matcher.rb +123 -0
  46. data/lib/state_machine/matcher_helpers.rb +54 -0
  47. data/lib/state_machine/node_collection.rb +152 -0
  48. data/lib/state_machine/state.rb +249 -0
  49. data/lib/state_machine/state_collection.rb +112 -0
  50. data/lib/state_machine/transition.rb +394 -0
  51. data/tasks/state_machine.rake +1 -0
  52. data/tasks/state_machine.rb +30 -0
  53. data/test/classes/switch.rb +11 -0
  54. data/test/functional/state_machine_test.rb +941 -0
  55. data/test/test_helper.rb +4 -0
  56. data/test/unit/assertions_test.rb +40 -0
  57. data/test/unit/callback_test.rb +455 -0
  58. data/test/unit/condition_proxy_test.rb +328 -0
  59. data/test/unit/eval_helpers_test.rb +120 -0
  60. data/test/unit/event_collection_test.rb +326 -0
  61. data/test/unit/event_test.rb +743 -0
  62. data/test/unit/guard_test.rb +908 -0
  63. data/test/unit/integrations/active_record_test.rb +1374 -0
  64. data/test/unit/integrations/data_mapper_test.rb +962 -0
  65. data/test/unit/integrations/sequel_test.rb +859 -0
  66. data/test/unit/integrations_test.rb +42 -0
  67. data/test/unit/invalid_event_test.rb +7 -0
  68. data/test/unit/invalid_transition_test.rb +7 -0
  69. data/test/unit/machine_collection_test.rb +938 -0
  70. data/test/unit/machine_test.rb +2004 -0
  71. data/test/unit/matcher_helpers_test.rb +37 -0
  72. data/test/unit/matcher_test.rb +155 -0
  73. data/test/unit/node_collection_test.rb +207 -0
  74. data/test/unit/state_collection_test.rb +280 -0
  75. data/test/unit/state_machine_test.rb +31 -0
  76. data/test/unit/state_test.rb +795 -0
  77. data/test/unit/transition_test.rb +1212 -0
  78. metadata +163 -0
@@ -0,0 +1,17 @@
1
+ <p>
2
+ <b>Name:</b>
3
+ <%=h @user.name %>
4
+ </p>
5
+
6
+ <p>
7
+ <b>State:</b>
8
+ <%=h @user.state %>
9
+ </p>
10
+
11
+ <p>
12
+ <b>Access State:</b>
13
+ <%=h @user.access_state %>
14
+ </p>
15
+
16
+ <%= link_to 'Edit', edit_user_path(@user) %> |
17
+ <%= link_to 'Back', users_path %>
@@ -0,0 +1,7 @@
1
+ class TrafficLight
2
+ state_machine :initial => :stop do
3
+ event :cycle do
4
+ transition :stop => :proceed, :proceed => :caution, :caution => :stop
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ class Vehicle
2
+ state_machine :initial => :parked do
3
+ event :park do
4
+ transition [:idling, :first_gear] => :parked
5
+ end
6
+
7
+ event :ignite do
8
+ transition :stalled => same, :parked => :idling
9
+ end
10
+
11
+ event :idle do
12
+ transition :first_gear => :idling
13
+ end
14
+
15
+ event :shift_up do
16
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
17
+ end
18
+
19
+ event :shift_down do
20
+ transition :third_gear => :second_gear, :second_gear => :first_gear
21
+ end
22
+
23
+ event :crash do
24
+ transition [:first_gear, :second_gear, :third_gear] => :stalled
25
+ end
26
+
27
+ event :repair do
28
+ transition :stalled => :parked
29
+ end
30
+ end
31
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'state_machine'
@@ -0,0 +1,388 @@
1
+ require 'state_machine/machine'
2
+
3
+ # A state machine is a model of behavior composed of states, events, and
4
+ # transitions. This helper adds support for defining this type of
5
+ # functionality on any Ruby class.
6
+ module StateMachine
7
+ module MacroMethods
8
+ # Creates a new state machine with the given name. The default name, if not
9
+ # specified, is <tt>:state</tt>.
10
+ #
11
+ # Configuration options:
12
+ # * <tt>:attribute</tt> - The name of the attribute to store the state value
13
+ # in. By default, this is the same as the name of the machine.
14
+ # * <tt>:initial</tt> - The initial state of the attribute. This can be a
15
+ # static state or a lambda block which will be evaluated at runtime
16
+ # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
17
+ # Default is nil.
18
+ # * <tt>:action</tt> - The instance method to invoke when an object
19
+ # transitions. Default is nil unless otherwise specified by the
20
+ # configured integration.
21
+ # * <tt>:namespace</tt> - The name to use for namespacing all generated
22
+ # state / event instance methods (e.g. "heater" would generate
23
+ # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
24
+ # Default is nil.
25
+ # * <tt>:integration</tt> - The name of the integration to use for adding
26
+ # library-specific behavior to the machine. Built-in integrations
27
+ # include :data_mapper, :active_record, and :sequel. By default, this
28
+ # is determined automatically.
29
+ #
30
+ # Configuration options relevant to ORM integrations:
31
+ # * <tt>:plural</tt> - The pluralized name of the attribute. By default,
32
+ # this will attempt to call +pluralize+ on the attribute. If this
33
+ # method is not available, an "s" is appended. This is used for
34
+ # generating scopes.
35
+ # * <tt>:messages</tt> - The error messages to use when invalidating
36
+ # objects due to failed transitions. Messages include:
37
+ # * <tt>:invalid</tt>
38
+ # * <tt>:invalid_event</tt>
39
+ # * <tt>:invalid_transition</tt>
40
+ # * <tt>:use_transactions</tt> - Whether transactions should be used when
41
+ # firing events. Default is true unless otherwise specified by the
42
+ # configured integration.
43
+ #
44
+ # This also expects a block which will be used to actually configure the
45
+ # states, events and transitions for the state machine. *Note* that this
46
+ # block will be executed within the context of the state machine. As a
47
+ # result, you will not be able to access any class methods unless you refer
48
+ # to them directly (i.e. specifying the class name).
49
+ #
50
+ # For examples on the types of state machine configurations and blocks, see
51
+ # the section below.
52
+ #
53
+ # == Examples
54
+ #
55
+ # With the default name/attribute and no configuration:
56
+ #
57
+ # class Vehicle
58
+ # state_machine do
59
+ # event :park do
60
+ # ...
61
+ # end
62
+ # end
63
+ # end
64
+ #
65
+ # The above example will define a state machine named "state" that will
66
+ # store the value in the +state+ attribute. Every vehicle will start
67
+ # without an initial state.
68
+ #
69
+ # With a custom name / attribute:
70
+ #
71
+ # class Vehicle
72
+ # state_machine :status, :attribute => :status_value do
73
+ # ...
74
+ # end
75
+ # end
76
+ #
77
+ # With a static initial state:
78
+ #
79
+ # class Vehicle
80
+ # state_machine :status, :initial => :parked do
81
+ # ...
82
+ # end
83
+ # end
84
+ #
85
+ # With a dynamic initial state:
86
+ #
87
+ # class Vehicle
88
+ # state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
89
+ # ...
90
+ # end
91
+ # end
92
+ #
93
+ # == Instance Methods
94
+ #
95
+ # The following instance methods will be automatically generated by the
96
+ # state machine based on the *name* of the machine. Any existing methods
97
+ # will not be overwritten.
98
+ # * <tt>state</tt> - Gets the current value for the attribute
99
+ # * <tt>state=(value)</tt> - Sets the current value for the attribute
100
+ # * <tt>state?(name)</tt> - Checks the given state name against the current
101
+ # state. If the name is not a known state, then an ArgumentError is raised.
102
+ # * <tt>state_name</tt> - Gets the name of the state for the current value
103
+ # * <tt>state_events</tt> - Gets the list of events that can be fired on
104
+ # the current object's state (uses the *unqualified* event names)
105
+ # * <tt>state_transitions(requirements = {})</tt> - Gets the list of possible
106
+ # transitions that can be made on the current object's state. Additional
107
+ # requirements, such as the :from / :to state and :on event can be specified
108
+ # to restrict the transitions to select. By default, the current state
109
+ # will be used for the :from state.
110
+ #
111
+ # For example,
112
+ #
113
+ # class Vehicle
114
+ # state_machine :state, :initial => :parked do
115
+ # event :ignite do
116
+ # transition :parked => :idling
117
+ # end
118
+ #
119
+ # event :park do
120
+ # transition :idling => :parked
121
+ # end
122
+ # end
123
+ # end
124
+ #
125
+ # vehicle = Vehicle.new
126
+ # vehicle.state # => "parked"
127
+ # vehicle.state_name # => :parked
128
+ # vehicle.state?(:parked) # => true
129
+ #
130
+ # # Changing state
131
+ # vehicle.state = 'idling'
132
+ # vehicle.state # => "idling"
133
+ # vehicle.state_name # => :idling
134
+ # vehicle.state?(:parked) # => false
135
+ #
136
+ # # Getting current event / transition availability
137
+ # vehicle.state_events # => [:park]
138
+ # vehicle.park # => true
139
+ # vehicle.state_events # => [:ignite]
140
+ #
141
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
142
+ # vehicle.ignite
143
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
144
+ #
145
+ # == Attribute initialization
146
+ #
147
+ # For most classes, the initial values for state machine attributes are
148
+ # automatically assigned when a new object is created. However, this
149
+ # behavior will *not* work if the class defines an +initialize+ method
150
+ # without properly calling +super+.
151
+ #
152
+ # For example,
153
+ #
154
+ # class Vehicle
155
+ # state_machine :state, :initial => :parked do
156
+ # ...
157
+ # end
158
+ # end
159
+ #
160
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
161
+ # vehicle.state # => "parked"
162
+ #
163
+ # In the above example, no +initialize+ method is defined. As a result,
164
+ # the default behavior of initializing the state machine attributes is used.
165
+ #
166
+ # In the following example, a custom +initialize+ method is defined:
167
+ #
168
+ # class Vehicle
169
+ # state_machine :state, :initial => :parked do
170
+ # ...
171
+ # end
172
+ #
173
+ # def initialize
174
+ # end
175
+ # end
176
+ #
177
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
178
+ # vehicle.state # => nil
179
+ #
180
+ # Since the +initialize+ method is defined, the state machine attributes
181
+ # never get initialized. In order to ensure that all initialization hooks
182
+ # are called, the custom method *must* call +super+ without any arguments
183
+ # like so:
184
+ #
185
+ # class Vehicle
186
+ # state_machine :state, :initial => :parked do
187
+ # ...
188
+ # end
189
+ #
190
+ # def initialize(attributes = {})
191
+ # ...
192
+ # super()
193
+ # end
194
+ # end
195
+ #
196
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
197
+ # vehicle.state # => "parked"
198
+ #
199
+ # Because of the way the inclusion of modules works in Ruby, calling
200
+ # <tt>super()</tt> will not only call the superclass's +initialize+, but
201
+ # also +initialize+ on all included modules. This allows the original state
202
+ # machine hook to get called properly.
203
+ #
204
+ # If you want to avoid calling the superclass's constructor, but still want
205
+ # to initialize the state machine attributes:
206
+ #
207
+ # class Vehicle
208
+ # state_machine :state, :initial => :parked do
209
+ # ...
210
+ # end
211
+ #
212
+ # def initialize(attributes = {})
213
+ # ...
214
+ # initialize_state_machines
215
+ # end
216
+ # end
217
+ #
218
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
219
+ # vehicle.state # => "parked"
220
+ #
221
+ # == States
222
+ #
223
+ # All of the valid states for the machine are automatically tracked based
224
+ # on the events, transitions, and callbacks defined for the machine. If
225
+ # there are additional states that are never referenced, these should be
226
+ # explicitly added using the StateMachine::Machine#state or
227
+ # StateMachine::Machine#other_states helpers.
228
+ #
229
+ # When a new state is defined, a predicate method for that state is
230
+ # generated on the class. For example,
231
+ #
232
+ # class Vehicle
233
+ # state_machine :initial => :parked do
234
+ # event :ignite do
235
+ # transition all => :idling
236
+ # end
237
+ # end
238
+ # end
239
+ #
240
+ # ...will generate the following instance methods (assuming they're not
241
+ # already defined in the class):
242
+ # * <tt>parked?</tt>
243
+ # * <tt>idling?</tt>
244
+ #
245
+ # Each predicate method will return true if it matches the object's
246
+ # current state. Otherwise, it will return false.
247
+ #
248
+ # == Events and Transitions
249
+ #
250
+ # Events defined on the machine are the interface to transitioning states
251
+ # for an object. Events can be fired either directly (through the method
252
+ # generated for the event) or indirectly (through attributes defined on
253
+ # the machine).
254
+ #
255
+ # For example,
256
+ #
257
+ # class Vehicle
258
+ # include DataMapper::Resource
259
+ # property :id, Serial
260
+ #
261
+ # state_machine :initial => :parked do
262
+ # event :ignite do
263
+ # transition :parked => :idling
264
+ # end
265
+ # end
266
+ #
267
+ # state_machine :alarm_state, :initial => :active do
268
+ # event :disable do
269
+ # transition all => :off
270
+ # end
271
+ # end
272
+ # end
273
+ #
274
+ # # Fire +ignite+ event directly
275
+ # vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
276
+ # vehicle.ignite # => true
277
+ # vehicle.state # => "idling"
278
+ # vehicle.alarm_state # => "active"
279
+ #
280
+ # # Fire +disable+ event automatically
281
+ # vehicle.alarm_state_event = 'disable'
282
+ # vehicle.save # => true
283
+ # vehicle.alarm_state # => "off"
284
+ #
285
+ # In the above example, the +state+ attribute is transitioned using the
286
+ # +ignite+ action that's generated from the state machine. On the other
287
+ # hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
288
+ # attribute that automatically gets fired when the machine's action (+save+)
289
+ # is invoked.
290
+ #
291
+ # For more information about how to configure an event and its associated
292
+ # transitions, see StateMachine::Machine#event.
293
+ #
294
+ # == Defining callbacks
295
+ #
296
+ # Within the +state_machine+ block, you can also define callbacks for
297
+ # transitions. For more information about defining these callbacks,
298
+ # see StateMachine::Machine#before_transition and
299
+ # StateMachine::Machine#after_transition.
300
+ #
301
+ # == Namespaces
302
+ #
303
+ # When a namespace is configured for a state machine, the name provided
304
+ # will be used in generating the instance methods for interacting with
305
+ # states/events in the machine. This is particularly useful when a class
306
+ # has multiple state machines and it would be difficult to differentiate
307
+ # between the various states / events.
308
+ #
309
+ # For example,
310
+ #
311
+ # class Vehicle
312
+ # state_machine :heater_state, :initial => :off, :namespace => 'heater' do
313
+ # event :turn_on do
314
+ # transition all => :on
315
+ # end
316
+ #
317
+ # event :turn_off do
318
+ # transition all => :off
319
+ # end
320
+ # end
321
+ #
322
+ # state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
323
+ # event :turn_on do
324
+ # transition all => :active
325
+ # end
326
+ #
327
+ # event :turn_off do
328
+ # transition all => :off
329
+ # end
330
+ # end
331
+ # end
332
+ #
333
+ # The above class defines two state machines: +heater_state+ and +alarm_state+.
334
+ # For the +heater_state+ machine, the following methods are generated since
335
+ # it's namespaced by "heater":
336
+ # * <tt>can_turn_on_heater?</tt>
337
+ # * <tt>turn_on_heater</tt>
338
+ # * ...
339
+ # * <tt>can_turn_off_heater?</tt>
340
+ # * <tt>turn_off_heater</tt>
341
+ # * ..
342
+ # * <tt>heater_off?</tt>
343
+ # * <tt>heater_on?</tt>
344
+ #
345
+ # As shown, each method is unique to the state machine so that the states
346
+ # and events don't conflict. The same goes for the +alarm_state+ machine:
347
+ # * <tt>can_turn_on_alarm?</tt>
348
+ # * <tt>turn_on_alarm</tt>
349
+ # * ...
350
+ # * <tt>can_turn_off_alarm?</tt>
351
+ # * <tt>turn_off_alarm</tt>
352
+ # * ..
353
+ # * <tt>alarm_active?</tt>
354
+ # * <tt>alarm_off?</tt>
355
+ #
356
+ # == Scopes
357
+ #
358
+ # For integrations that support it, a group of default scope filters will
359
+ # be automatically created for assisting in finding objects that have the
360
+ # attribute set to one of a given set of states.
361
+ #
362
+ # For example,
363
+ #
364
+ # Vehicle.with_state(:parked) # => All vehicles where the state is parked
365
+ # Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
366
+ #
367
+ # Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
368
+ # Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
369
+ #
370
+ # *Note* that if class methods already exist with those names (i.e.
371
+ # :with_state, :with_states, :without_state, or :without_states), then a
372
+ # scope will not be defined for that name.
373
+ #
374
+ # See StateMachine::Machine for more information about using integrations
375
+ # and the individual integration docs for information about the actual
376
+ # scopes that are generated.
377
+ def state_machine(*args, &block)
378
+ StateMachine::Machine.find_or_create(self, *args, &block)
379
+ end
380
+ end
381
+ end
382
+
383
+ Class.class_eval do
384
+ include StateMachine::MacroMethods
385
+ end
386
+
387
+ # Register rake tasks for supported libraries
388
+ Merb::Plugins.add_rakefiles("#{File.dirname(__FILE__)}/../tasks/state_machine") if defined?(Merb::Plugins)