pluginaweek-state_machine 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data/CHANGELOG.rdoc +273 -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 +429 -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 +251 -0
  33. data/lib/state_machine/event_collection.rb +113 -0
  34. data/lib/state_machine/extensions.rb +158 -0
  35. data/lib/state_machine/guard.rb +219 -0
  36. data/lib/state_machine/integrations.rb +68 -0
  37. data/lib/state_machine/integrations/active_record.rb +444 -0
  38. data/lib/state_machine/integrations/active_record/locale.rb +10 -0
  39. data/lib/state_machine/integrations/active_record/observer.rb +41 -0
  40. data/lib/state_machine/integrations/data_mapper.rb +325 -0
  41. data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
  42. data/lib/state_machine/integrations/sequel.rb +292 -0
  43. data/lib/state_machine/machine.rb +1431 -0
  44. data/lib/state_machine/machine_collection.rb +146 -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 +367 -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 +129 -0
  60. data/test/unit/event_collection_test.rb +293 -0
  61. data/test/unit/event_test.rb +605 -0
  62. data/test/unit/guard_test.rb +862 -0
  63. data/test/unit/integrations/active_record_test.rb +1001 -0
  64. data/test/unit/integrations/data_mapper_test.rb +694 -0
  65. data/test/unit/integrations/sequel_test.rb +486 -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 +710 -0
  70. data/test/unit/machine_test.rb +1910 -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 +1113 -0
  78. metadata +161 -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,429 @@
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 for the given attribute. The default
9
+ # attribute, if not specified, is <tt>:state</tt>.
10
+ #
11
+ # Configuration options:
12
+ # * <tt>:initial</tt> - The initial state of the attribute. This can be a
13
+ # static state or a lambda block which will be evaluated at runtime
14
+ # (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}).
15
+ # Default is nil.
16
+ # * <tt>:action</tt> - The instance method to invoke when an object
17
+ # transitions. Default is nil unless otherwise specified by the
18
+ # configured integration.
19
+ # * <tt>:as</tt> - The name to use for prefixing all generated machine
20
+ # instance / class methods (e.g. if the attribute is +state_id+, then
21
+ # "state" would generate :state_name, :state_transitions, etc. instead of
22
+ # :state_id_name and :state_id_transitions)
23
+ # * <tt>:namespace</tt> - The name to use for namespacing all generated
24
+ # state / event instance methods (e.g. "heater" would generate
25
+ # :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events).
26
+ # Default is nil.
27
+ # * <tt>:integration</tt> - The name of the integration to use for adding
28
+ # library-specific behavior to the machine. Built-in integrations
29
+ # include :data_mapper, :active_record, and :sequel. By default, this
30
+ # is determined automatically.
31
+ #
32
+ # Configuration options relevant to ORM integrations:
33
+ # * <tt>:plural</tt> - The pluralized name of the attribute. By default,
34
+ # this will attempt to call +pluralize+ on the attribute. If this
35
+ # method is not available, an "s" is appended. This is used for
36
+ # generating scopes.
37
+ # * <tt>:messages</tt> - The error messages to use when invalidating
38
+ # objects due to failed transitions. Messages include:
39
+ # * <tt>:invalid</tt>
40
+ # * <tt>:invalid_event</tt>
41
+ # * <tt>:invalid_transition</tt>
42
+ # * <tt>:use_transactions</tt> - Whether transactions should be used when
43
+ # firing events. Default is true unless otherwise specified by the
44
+ # configured integration.
45
+ #
46
+ # This also expects a block which will be used to actually configure the
47
+ # states, events and transitions for the state machine. *Note* that this
48
+ # block will be executed within the context of the state machine. As a
49
+ # result, you will not be able to access any class methods unless you refer
50
+ # to them directly (i.e. specifying the class name).
51
+ #
52
+ # For examples on the types of configured state machines and blocks, see
53
+ # the section below.
54
+ #
55
+ # == Examples
56
+ #
57
+ # With the default attribute and no configuration:
58
+ #
59
+ # class Vehicle
60
+ # state_machine do
61
+ # event :park do
62
+ # ...
63
+ # end
64
+ # end
65
+ # end
66
+ #
67
+ # The above example will define a state machine for the +state+ attribute
68
+ # on the class. Every vehicle will start without an initial state.
69
+ #
70
+ # With a custom attribute:
71
+ #
72
+ # class Vehicle
73
+ # state_machine :status do
74
+ # ...
75
+ # end
76
+ # end
77
+ #
78
+ # With a static initial state:
79
+ #
80
+ # class Vehicle
81
+ # state_machine :status, :initial => :parked do
82
+ # ...
83
+ # end
84
+ # end
85
+ #
86
+ # With a dynamic initial state:
87
+ #
88
+ # class Vehicle
89
+ # state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
90
+ # ...
91
+ # end
92
+ # end
93
+ #
94
+ # == Instance Methods
95
+ #
96
+ # The following instance methods will be automatically generated by the
97
+ # state machine. Any existing methods 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</tt> - Gets the list of possible transitions
106
+ # that can be made on the current object's state
107
+ #
108
+ # For example,
109
+ #
110
+ # class Vehicle
111
+ # state_machine :state, :initial => :parked do
112
+ # event :ignite do
113
+ # transition :parked => :idling
114
+ # end
115
+ #
116
+ # event :park do
117
+ # transition :idling => :parked
118
+ # end
119
+ # end
120
+ # end
121
+ #
122
+ # vehicle = Vehicle.new
123
+ # vehicle.state # => "parked"
124
+ # vehicle.state_name # => :parked
125
+ # vehicle.state?(:parked) # => true
126
+ #
127
+ # # Changing state
128
+ # vehicle.state = 'idling'
129
+ # vehicle.state # => "idling"
130
+ # vehicle.state_name # => :idling
131
+ # vehicle.state?(:parked) # => false
132
+ #
133
+ # # Getting current event / transition availability
134
+ # vehicle.state_events # => [:park]
135
+ # vehicle.park # => true
136
+ # vehicle.state_events # => [:ignite]
137
+ #
138
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
139
+ # vehicle.ignite
140
+ # vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]
141
+ #
142
+ # == Attribute initialization
143
+ #
144
+ # For most classes, the initial values for state machine attributes are
145
+ # automatically assigned when a new object is created. However, this
146
+ # behavior will *not* work if the class defines an +initialize+ method
147
+ # without properly calling +super+.
148
+ #
149
+ # For example,
150
+ #
151
+ # class Vehicle
152
+ # state_machine :state, :initial => :parked do
153
+ # ...
154
+ # end
155
+ # end
156
+ #
157
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
158
+ # vehicle.state # => "parked"
159
+ #
160
+ # In the above example, no +initialize+ method is defined. As a result,
161
+ # the default behavior of initializing the state machine attributes is used.
162
+ #
163
+ # In the following example, a custom +initialize+ method is defined:
164
+ #
165
+ # class Vehicle
166
+ # state_machine :state, :initial => :parked do
167
+ # ...
168
+ # end
169
+ #
170
+ # def initialize
171
+ # end
172
+ # end
173
+ #
174
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c77678>
175
+ # vehicle.state # => nil
176
+ #
177
+ # Since the +initialize+ method is defined, the state machine attributes
178
+ # never get initialized. In order to ensure that all initialization hooks
179
+ # are called, the custom method *must* call +super+ without any arguments
180
+ # like so:
181
+ #
182
+ # class Vehicle
183
+ # state_machine :state, :initial => :parked do
184
+ # ...
185
+ # end
186
+ #
187
+ # def initialize(attributes = {})
188
+ # ...
189
+ # super()
190
+ # end
191
+ # end
192
+ #
193
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
194
+ # vehicle.state # => "parked"
195
+ #
196
+ # Because of the way the inclusion of modules works in Ruby, calling
197
+ # <tt>super()</tt> will not only call the superclass's +initialize+, but
198
+ # also +initialize+ on all included modules. This allows the original state
199
+ # machine hook to get called properly.
200
+ #
201
+ # If you want to avoid calling the superclass's constructor, but still want
202
+ # to initialize the state machine attributes:
203
+ #
204
+ # class Vehicle
205
+ # state_machine :state, :initial => :parked do
206
+ # ...
207
+ # end
208
+ #
209
+ # def initialize(attributes = {})
210
+ # ...
211
+ # initialize_state_machines
212
+ # end
213
+ # end
214
+ #
215
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c8dbf8 @state="parked">
216
+ # vehicle.state # => "parked"
217
+ #
218
+ # == States
219
+ #
220
+ # All of the valid states for the machine are automatically tracked based
221
+ # on the events, transitions, and callbacks defined for the machine. If
222
+ # there are additional states that are never referenced, these should be
223
+ # explicitly added using the StateMachine::Machine#state or
224
+ # StateMachine::Machine#other_states helpers.
225
+ #
226
+ # When a new state is defined, a predicate method for that state is
227
+ # generated on the class. For example,
228
+ #
229
+ # class Vehicle
230
+ # state_machine :initial => :parked do
231
+ # event :ignite do
232
+ # transition all => :idling
233
+ # end
234
+ # end
235
+ # end
236
+ #
237
+ # ...will generate the following instance methods (assuming they're not
238
+ # already defined in the class):
239
+ # * <tt>parked?</tt>
240
+ # * <tt>idling?</tt>
241
+ #
242
+ # Each predicate method will return true if it matches the object's
243
+ # current state. Otherwise, it will return false.
244
+ #
245
+ # == Events and Transitions
246
+ #
247
+ # Events defined on the machine are the interface to transitioning states
248
+ # for an object. Events can be fired either directly (through the method
249
+ # generated for the event) or indirectly (through attributes defined on
250
+ # the machine).
251
+ #
252
+ # For example,
253
+ #
254
+ # class Vehicle
255
+ # include DataMapper::Resource
256
+ # property :id, Integer, :serial => true
257
+ #
258
+ # state_machine :initial => :parked do
259
+ # event :ignite do
260
+ # transition :parked => :idling
261
+ # end
262
+ # end
263
+ #
264
+ # state_machine :alarm_state, :initial => :active do
265
+ # event :disable do
266
+ # transition all => :off
267
+ # end
268
+ # end
269
+ # end
270
+ #
271
+ # # Fire +ignite+ event directly
272
+ # vehicle = Vehicle.create # => #<Vehicle id=1 state="parked" alarm_state="active">
273
+ # vehicle.ignite # => true
274
+ # vehicle.state # => "idling"
275
+ # vehicle.alarm_state # => "active"
276
+ #
277
+ # # Fire +disable+ event automatically
278
+ # vehicle.alarm_state_event = 'disable'
279
+ # vehicle.save # => true
280
+ # vehicle.alarm_state # => "off"
281
+ #
282
+ # In the above example, the +state+ attribute is transitioned using the
283
+ # +ignite+ action that's generated from the state machine. On the other
284
+ # hand, the +alarm_state+ attribute is transitioned using the +alarm_state_event+
285
+ # attribute that automatically gets fired when the machine's action (+save+)
286
+ # is invoked.
287
+ #
288
+ # For more information about how to configure an event and its associated
289
+ # transitions, see StateMachine::Machine#event.
290
+ #
291
+ # == Defining callbacks
292
+ #
293
+ # Within the +state_machine+ block, you can also define callbacks for
294
+ # transitions. For more information about defining these callbacks,
295
+ # see StateMachine::Machine#before_transition and
296
+ # StateMachine::Machine#after_transition.
297
+ #
298
+ # == Attribute aliases
299
+ #
300
+ # When a state machine is defined, several methods are generated scoped by
301
+ # the name of the attribute, such as (if the attribute were "state"):
302
+ # * <tt>state_name</tt>
303
+ # * <tt>state_event</tt>
304
+ # * <tt>state_transitions</tt>
305
+ # * etc.
306
+ #
307
+ # If the attribute for the machine were something less common, such as
308
+ # "state_id" or "state_value", this makes for more awkward scoped methods.
309
+ #
310
+ # Rather than scope based on the attribute, these methods can be customized
311
+ # using the <tt>:as</tt> option as essentially an alias.
312
+ #
313
+ # For example,
314
+ #
315
+ # class Vehicle
316
+ # state_machine :state_id, :as => :state do
317
+ # event :turn_on do
318
+ # transition all => :on
319
+ # end
320
+ #
321
+ # event :turn_off do
322
+ # transition all => :off
323
+ # end
324
+ #
325
+ # state :on, :value => 1
326
+ # state :off, :value => 2
327
+ # end
328
+ # end
329
+ #
330
+ # ...will generate the following methods:
331
+ # * <tt>state_name</tt>
332
+ # * <tt>state_event</tt>
333
+ # * <tt>state_transitions</tt>
334
+ #
335
+ # ...instead of:
336
+ # * <tt>state_id_name</tt>
337
+ # * <tt>state_id_event</tt>
338
+ # * <tt>state_id_transitions</tt>
339
+ #
340
+ # However, it will continue to read and write to the +state_id+ attribute.
341
+ #
342
+ # == Namespaces
343
+ #
344
+ # When a namespace is configured for a state machine, the name provided
345
+ # will be used in generating the instance methods for interacting with
346
+ # events/states in the machine. This is particularly useful when a class
347
+ # has multiple state machines and it would be difficult to differentiate
348
+ # between the various states / events.
349
+ #
350
+ # For example,
351
+ #
352
+ # class Vehicle
353
+ # state_machine :heater_state, :initial => :off, :namespace => 'heater' do
354
+ # event :turn_on do
355
+ # transition all => :on
356
+ # end
357
+ #
358
+ # event :turn_off do
359
+ # transition all => :off
360
+ # end
361
+ # end
362
+ #
363
+ # state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
364
+ # event :turn_on do
365
+ # transition all => :active
366
+ # end
367
+ #
368
+ # event :turn_off do
369
+ # transition all => :off
370
+ # end
371
+ # end
372
+ # end
373
+ #
374
+ # The above class defines two state machines: +heater_state+ and +alarm_state+.
375
+ # For the +heater_state+ machine, the following methods are generated since
376
+ # it's namespaced by "heater":
377
+ # * <tt>can_turn_on_heater?</tt>
378
+ # * <tt>turn_on_heater</tt>
379
+ # * ...
380
+ # * <tt>can_turn_off_heater?</tt>
381
+ # * <tt>turn_off_heater</tt>
382
+ # * ..
383
+ # * <tt>heater_off?</tt>
384
+ # * <tt>heater_on?</tt>
385
+ #
386
+ # As shown, each method is unique to the state machine so that the states
387
+ # and events don't conflict. The same goes for the +alarm_state+ machine:
388
+ # * <tt>can_turn_on_alarm?</tt>
389
+ # * <tt>turn_on_alarm</tt>
390
+ # * ...
391
+ # * <tt>can_turn_off_alarm?</tt>
392
+ # * <tt>turn_off_alarm</tt>
393
+ # * ..
394
+ # * <tt>alarm_active?</tt>
395
+ # * <tt>alarm_off?</tt>
396
+ #
397
+ # == Scopes
398
+ #
399
+ # For integrations that support it, a group of default scope filters will
400
+ # be automatically created for assisting in finding objects that have the
401
+ # attribute set to the value for a given set of states.
402
+ #
403
+ # For example,
404
+ #
405
+ # Vehicle.with_state(:parked) # => All vehicles where the state is parked
406
+ # Vehicle.with_states(:parked, :idling) # => All vehicles where the state is either parked or idling
407
+ #
408
+ # Vehicle.without_state(:parked) # => All vehicles where the state is *not* parked
409
+ # Vehicle.without_states(:parked, :idling) # => All vehicles where the state is *not* parked or idling
410
+ #
411
+ # *Note* that if class methods already exist with those names (i.e.
412
+ # :with_state, :with_states, :without_state, or :without_states), then a
413
+ # scope will not be defined for that name.
414
+ #
415
+ # See StateMachine::Machine for more information about using
416
+ # integrations and the individual integration docs for information about
417
+ # the actual scopes that are generated.
418
+ def state_machine(*args, &block)
419
+ StateMachine::Machine.find_or_create(self, *args, &block)
420
+ end
421
+ end
422
+ end
423
+
424
+ Class.class_eval do
425
+ include StateMachine::MacroMethods
426
+ end
427
+
428
+ # Register rake tasks for supported libraries
429
+ Merb::Plugins.add_rakefiles("#{File.dirname(__FILE__)}/../tasks/state_machine") if defined?(Merb::Plugins)