pluginaweek-state_machine 0.7.6
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 +273 -0
- data/LICENSE +20 -0
- data/README.rdoc +466 -0
- data/Rakefile +98 -0
- data/examples/AutoShop_state.png +0 -0
- data/examples/Car_state.png +0 -0
- data/examples/TrafficLight_state.png +0 -0
- data/examples/Vehicle_state.png +0 -0
- data/examples/auto_shop.rb +11 -0
- data/examples/car.rb +19 -0
- data/examples/merb-rest/controller.rb +51 -0
- data/examples/merb-rest/model.rb +28 -0
- data/examples/merb-rest/view_edit.html.erb +24 -0
- data/examples/merb-rest/view_index.html.erb +23 -0
- data/examples/merb-rest/view_new.html.erb +13 -0
- data/examples/merb-rest/view_show.html.erb +17 -0
- data/examples/rails-rest/controller.rb +43 -0
- data/examples/rails-rest/migration.rb +11 -0
- data/examples/rails-rest/model.rb +23 -0
- data/examples/rails-rest/view_edit.html.erb +25 -0
- data/examples/rails-rest/view_index.html.erb +23 -0
- data/examples/rails-rest/view_new.html.erb +14 -0
- data/examples/rails-rest/view_show.html.erb +17 -0
- data/examples/traffic_light.rb +7 -0
- data/examples/vehicle.rb +31 -0
- data/init.rb +1 -0
- data/lib/state_machine.rb +429 -0
- data/lib/state_machine/assertions.rb +36 -0
- data/lib/state_machine/callback.rb +189 -0
- data/lib/state_machine/condition_proxy.rb +94 -0
- data/lib/state_machine/eval_helpers.rb +67 -0
- data/lib/state_machine/event.rb +251 -0
- data/lib/state_machine/event_collection.rb +113 -0
- data/lib/state_machine/extensions.rb +158 -0
- data/lib/state_machine/guard.rb +219 -0
- data/lib/state_machine/integrations.rb +68 -0
- data/lib/state_machine/integrations/active_record.rb +444 -0
- data/lib/state_machine/integrations/active_record/locale.rb +10 -0
- data/lib/state_machine/integrations/active_record/observer.rb +41 -0
- data/lib/state_machine/integrations/data_mapper.rb +325 -0
- data/lib/state_machine/integrations/data_mapper/observer.rb +139 -0
- data/lib/state_machine/integrations/sequel.rb +292 -0
- data/lib/state_machine/machine.rb +1431 -0
- data/lib/state_machine/machine_collection.rb +146 -0
- data/lib/state_machine/matcher.rb +123 -0
- data/lib/state_machine/matcher_helpers.rb +54 -0
- data/lib/state_machine/node_collection.rb +152 -0
- data/lib/state_machine/state.rb +249 -0
- data/lib/state_machine/state_collection.rb +112 -0
- data/lib/state_machine/transition.rb +367 -0
- data/tasks/state_machine.rake +1 -0
- data/tasks/state_machine.rb +30 -0
- data/test/classes/switch.rb +11 -0
- data/test/functional/state_machine_test.rb +941 -0
- data/test/test_helper.rb +4 -0
- data/test/unit/assertions_test.rb +40 -0
- data/test/unit/callback_test.rb +455 -0
- data/test/unit/condition_proxy_test.rb +328 -0
- data/test/unit/eval_helpers_test.rb +129 -0
- data/test/unit/event_collection_test.rb +293 -0
- data/test/unit/event_test.rb +605 -0
- data/test/unit/guard_test.rb +862 -0
- data/test/unit/integrations/active_record_test.rb +1001 -0
- data/test/unit/integrations/data_mapper_test.rb +694 -0
- data/test/unit/integrations/sequel_test.rb +486 -0
- data/test/unit/integrations_test.rb +42 -0
- data/test/unit/invalid_event_test.rb +7 -0
- data/test/unit/invalid_transition_test.rb +7 -0
- data/test/unit/machine_collection_test.rb +710 -0
- data/test/unit/machine_test.rb +1910 -0
- data/test/unit/matcher_helpers_test.rb +37 -0
- data/test/unit/matcher_test.rb +155 -0
- data/test/unit/node_collection_test.rb +207 -0
- data/test/unit/state_collection_test.rb +280 -0
- data/test/unit/state_machine_test.rb +31 -0
- data/test/unit/state_test.rb +795 -0
- data/test/unit/transition_test.rb +1113 -0
- metadata +161 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
== master
|
2
|
+
|
3
|
+
* Add support for customizing generated methods like #{attribute}_name using :as instead of always prefixing with the attribute name
|
4
|
+
* Simplify reading from / writing to machine-related attributes on objects
|
5
|
+
* Fix locale for ActiveRecord getting added to the i18n load path multiple times [Reiner Dieterich]
|
6
|
+
* Fix callbacks, guards, and state-driven behaviors not always working on tainted classes [Brandon Dimcheff]
|
7
|
+
* Use Ruby 1.9's built-in Object#instance_exec for bound callbacks when it's available
|
8
|
+
* Improve performance of cached dynamic state lookups by 25%
|
9
|
+
|
10
|
+
== 0.7.5 / 2009-05-25
|
11
|
+
|
12
|
+
* Add built-in caching for dynamic state values when the value only needs to be generated once
|
13
|
+
* Fix flawed example for using record ids as state values
|
14
|
+
* Don't evaluate state values until they're actually used in an object instance
|
15
|
+
* Make it easier to use event attributes for actions defined in the same class as the state machine
|
16
|
+
* Fix #save/save! running transitions in ActiveRecord integrations even when a machine's action is not :save
|
17
|
+
|
18
|
+
== 0.7.4 / 2009-05-23
|
19
|
+
|
20
|
+
* Fix #save! not firing event attributes properly in ActiveRecord integrations
|
21
|
+
* Fix log files being included in gems
|
22
|
+
|
23
|
+
== 0.7.3 / 2009-04-25
|
24
|
+
|
25
|
+
* Require DataMapper version be >= 0.9.4
|
26
|
+
* Explicitly load Sequel's built-in inflector (>= 2.12.0) for scope names
|
27
|
+
* Don't use qualified name for event attributes
|
28
|
+
* Fix #valid? being defined for DataMapper resources when dm-validations isn't loaded
|
29
|
+
* Add auto-validation of values allowed for the state attribute in ORM integrations
|
30
|
+
|
31
|
+
== 0.7.2 / 2009-04-08
|
32
|
+
|
33
|
+
* Add support for running multiple methods in a callback without using blocks
|
34
|
+
* Add more flexibility around how callbacks are defined
|
35
|
+
* Add security documentation around mass-assignment in ORM integrations
|
36
|
+
* Fix event attribute transitions being publicly accessible
|
37
|
+
|
38
|
+
== 0.7.1 / 2009-04-05
|
39
|
+
|
40
|
+
* Fix machines failing to generate graphs when run from Merb tasks
|
41
|
+
|
42
|
+
== 0.7.0 / 2009-04-04
|
43
|
+
|
44
|
+
* Add #{attribute}_event for automatically firing events when the object's action is called
|
45
|
+
* Make it easier to override state-driven behaviors
|
46
|
+
* Rollback state changes when the action fails during transitions
|
47
|
+
* Use :messages instead of :invalid_message for customizing validation errors
|
48
|
+
* Use more human-readable validation errors
|
49
|
+
* Add support for more ActiveRecord observer hooks
|
50
|
+
* Add support for targeting multiple specific state machines in DataMapper observer hooks
|
51
|
+
* Don't pass the result of the action as an argument to callbacks (access via Transition#result)
|
52
|
+
* Fix incorrect results being used when running transitions in parallel
|
53
|
+
* Fix transition args not being set when run in parallel
|
54
|
+
* Allow callback terminators to be set on an application-wide basis
|
55
|
+
* Only catch :halt during before / after transition callbacks
|
56
|
+
* Fix ActiveRecord predicates being overwritten if they're already defined in the class
|
57
|
+
* Allow machine options to be set on an integration-wide basis
|
58
|
+
* Turn transactions off by default in DataMapper integrations
|
59
|
+
* Add support for configuring the use of transactions
|
60
|
+
* Simplify reading/writing of attributes
|
61
|
+
* Simplify access to state machines via #state_machine(:attribute) without generating dupes
|
62
|
+
* Fix assumptions that dm-validations is always available in DataMapper integration
|
63
|
+
* Automatically define DataMapper properties for machine attributes if they don't exist
|
64
|
+
* Add Transition#qualified_event, #qualified_from_name, and #qualified_to_name
|
65
|
+
* Add #fire_events / #fire_events! for running events on multiple state machines in parallel
|
66
|
+
* Rename next_#{event}_transition to #{event}_transition
|
67
|
+
* Add #{attribute}_transitions for getting the list of transitions that can be run on an object
|
68
|
+
* Add #{attribute}_events for getting the list of events that can be fired on an object
|
69
|
+
* Use generated non-bang event when running bang version so that overriding one affects the other
|
70
|
+
* Provide access to arguments passed into an event from transition callbacks via Transition#args
|
71
|
+
|
72
|
+
== 0.6.3 / 2009-03-10
|
73
|
+
|
74
|
+
* Add support for customizing the graph's orientation
|
75
|
+
* Use the standard visualizations for initial (open arrow) and final (double circle) states
|
76
|
+
* Highlight final states in GraphViz drawings
|
77
|
+
|
78
|
+
== 0.6.2 / 2009-03-08
|
79
|
+
|
80
|
+
* Make it easier to override generated instance / class methods
|
81
|
+
|
82
|
+
== 0.6.1 / 2009-03-07
|
83
|
+
|
84
|
+
* Add i18n support for ActiveRecord validation errors
|
85
|
+
* Add a validation error when failing to transition for ActiveRecord / DataMapper / Sequel integrations
|
86
|
+
|
87
|
+
== 0.6.0 / 2009-03-03
|
88
|
+
|
89
|
+
* Allow multiple conditions for callbacks / class behaviors
|
90
|
+
* Add support for state-driven class behavior with :if/:unless options
|
91
|
+
* Alias Machine#event as Machine#on
|
92
|
+
* Fix nil from/to states not being handled properly
|
93
|
+
* Simplify hooking callbacks into loopbacks
|
94
|
+
* Add simplified transition/callback requirement syntax
|
95
|
+
|
96
|
+
== 0.5.2 / 2009-02-17
|
97
|
+
|
98
|
+
* Improve pretty-print of events
|
99
|
+
* Simplify state/event matching design, improving guard performance by 30%
|
100
|
+
* Add better error notification when conflicting guard options are defined
|
101
|
+
* Fix scope name pluralization not being applied correctly
|
102
|
+
|
103
|
+
== 0.5.1 / 2009-02-11
|
104
|
+
|
105
|
+
* Allow states to be drawn as ellipses to accommodate long names
|
106
|
+
* Fix rake tasks not being registered in Rails/Merb applications
|
107
|
+
* Never automatically define machine attribute accessors when using an integration
|
108
|
+
|
109
|
+
== 0.5.0 / 2009-01-11
|
110
|
+
|
111
|
+
* Add to_name and from_name to transition objects
|
112
|
+
* Add nicely formatted #inspect for transitions
|
113
|
+
* Fix ActiveRecord integrations failing when the database doesn't exist yet
|
114
|
+
* Fix states not being drawn in GraphViz graphs in the correct order
|
115
|
+
* Add nicely formatted #inspect for states and events
|
116
|
+
* Simplify machine context-switching
|
117
|
+
* Store events/states in enumerable node collections
|
118
|
+
* No longer allow subclasses to change the integration
|
119
|
+
* Move fire! action logic into the Event class (no longer calls fire action on the object)
|
120
|
+
* Allow states in subclasses to have different values
|
121
|
+
* Recommend that all states be referenced as symbols instead of strings
|
122
|
+
* All states must now be named (and can be associated with other value types)
|
123
|
+
* Add support for customizing the actual stored value for a state
|
124
|
+
* Add compatibility with Ruby 1.9+
|
125
|
+
|
126
|
+
== 0.4.3 / 2008-12-28
|
127
|
+
|
128
|
+
* Allow dm-observer integration to be optional
|
129
|
+
* Fix non-lambda callbacks not working for DataMapper/Sequel
|
130
|
+
|
131
|
+
== 0.4.2 / 2008-12-28
|
132
|
+
|
133
|
+
* Fix graphs not being drawn the same way consistently
|
134
|
+
* Add support for sharing transitions across multiple events
|
135
|
+
* Add support for state-driven behavior
|
136
|
+
* Simplify initialize hooks, requiring super to be called instead
|
137
|
+
* Add :namespace option for generated state predicates / event methods
|
138
|
+
|
139
|
+
== 0.4.1 / 2008-12-16
|
140
|
+
|
141
|
+
* Fix nil states not being handled properly in guards, known states, or visualizations
|
142
|
+
* Fix the same node being used for different dynamic states in GraphViz output
|
143
|
+
* Always include initial state in the list of known states even if it's dynamic
|
144
|
+
* Use consistent naming scheme for dynamic states in GraphViz output
|
145
|
+
* Allow blocks to be directly passed into machine class
|
146
|
+
* Fix attribute predicates not working on attributes that represent columns in ActiveRecord
|
147
|
+
|
148
|
+
== 0.4.0 / 2008-12-14
|
149
|
+
|
150
|
+
* Remove the PluginAWeek namespace
|
151
|
+
* Add generic attribute predicate (e.g. "#{attribute}?(state_name)") and state predicates (e.g. "#{state}?")
|
152
|
+
* Add Sequel support
|
153
|
+
* Fix aliasing :initialize on ActiveRecord models causing warnings when the environment is reloaded
|
154
|
+
* Fix ActiveRecord state machines trying to query the database on unmigrated models
|
155
|
+
* Fix initial states not getting set when the current value is an empty string [Aaron Gibralter]
|
156
|
+
* Add rake tasks for generating graphviz files for state machines [Nate Murray]
|
157
|
+
* Fix initial state not being included in list of known states
|
158
|
+
* Add other_states directive for defining additional states not referenced in transitions or callbacks [Pete Forde]
|
159
|
+
* Add next_#{event}_transition for getting the next transition that would be performed if the event were invoked
|
160
|
+
* Add the ability to override the pluralized name of an attribute for creating scopes
|
161
|
+
* Add the ability to halt callback chains by: throw :halt
|
162
|
+
* Add support for dynamic to states in transitions (e.g. :to => lambda {Time.now})
|
163
|
+
* Add support for using real blocks in before_transition/after_transition calls instead of using the :do option
|
164
|
+
* Add DataMapper support
|
165
|
+
* Include states referenced in transition callbacks in the list of a machine's known states
|
166
|
+
* Only generate the known states for a machine on demand, rather than calculating beforehand
|
167
|
+
* Add the ability to skip state change actions during a transition (e.g. vehicle.ignite(false))
|
168
|
+
* Add the ability for the state change action (e.g. +save+ for ActiveRecord) to be configurable
|
169
|
+
* Allow state machines to be defined on *any* Ruby class, not just ActiveRecord (removes all external dependencies)
|
170
|
+
* Refactor transitions, guards, and callbacks for better organization/design
|
171
|
+
* Use a class containing the transition context in callbacks, rather than an ordered list of each individual attribute
|
172
|
+
* Add without_#{attribute} named scopes (opposite of the existing with_#{attribute} named scopes) [Sean O'Brien]
|
173
|
+
|
174
|
+
== 0.3.1 / 2008-10-26
|
175
|
+
|
176
|
+
* Fix the initial state not getting set when the state attribute is mass-assigned but protected
|
177
|
+
* Change how the base module is included to prevent namespacing conflicts
|
178
|
+
|
179
|
+
== 0.3.0 / 2008-09-07
|
180
|
+
|
181
|
+
* No longer allow additional arguments to be passed into event actions
|
182
|
+
* Add support for can_#{event}? for checking whether an event can be fired based on the current state of the record
|
183
|
+
* Don't use callbacks for performing transitions
|
184
|
+
* Fix state machines in subclasses not knowing what states/events/transitions were defined by superclasses
|
185
|
+
* Replace all before/after_exit/enter/loopback callback hooks and :before/:after options for events with before_transition/after_transition callbacks, e.g.
|
186
|
+
|
187
|
+
before_transition :from => 'parked', :do => :lock_doors # was before_exit :parked, :lock_doors
|
188
|
+
after_transition :on => 'ignite', :do => :turn_on_radio # was event :ignite, :after => :turn_on_radio do
|
189
|
+
|
190
|
+
* Always save when an event is fired even if it results in a loopback [Jürgen Strobel]
|
191
|
+
* Ensure initial state callbacks are invoked in the proper order when an event is fired on a new record
|
192
|
+
* Add before_loopback and after_loopback hooks [Jürgen Strobel]
|
193
|
+
|
194
|
+
== 0.2.1 / 2008-07-05
|
195
|
+
|
196
|
+
* Add more descriptive exceptions
|
197
|
+
* Assume the default state attribute is "state" if one is not provided
|
198
|
+
* Add :except_from option for transitions if you want to blacklist states
|
199
|
+
* Add PluginAWeek::StateMachine::Machine#states
|
200
|
+
* Add PluginAWeek::StateMachine::Event#transitions
|
201
|
+
* Allow creating transitions with no from state (effectively allowing the transition for *any* from state)
|
202
|
+
* Reduce the number of objects created for each transition
|
203
|
+
|
204
|
+
== 0.2.0 / 2008-06-29
|
205
|
+
|
206
|
+
* Add a non-bang version of events (e.g. park) that will return a boolean value for success
|
207
|
+
* Raise an exception if the bang version of events are used (e.g. park!) and no transition is successful
|
208
|
+
* Change callbacks to act a little more like ActiveRecord
|
209
|
+
* Avoid using string evaluation for dynamic methods
|
210
|
+
|
211
|
+
== 0.1.1 / 2008-06-22
|
212
|
+
|
213
|
+
* Remove log files from gems
|
214
|
+
|
215
|
+
== 0.1.0 / 2008-05-05
|
216
|
+
|
217
|
+
* Completely rewritten from scratch
|
218
|
+
* Renamed to state_machine
|
219
|
+
* Removed database dependencies
|
220
|
+
* Removed models in favor of an attribute-agnostic design
|
221
|
+
* Use ActiveSupport::Callbacks instead of eval_call
|
222
|
+
* Remove dry_transaction_rollbacks dependencies
|
223
|
+
* Added functional tests
|
224
|
+
* Updated documentation
|
225
|
+
|
226
|
+
== 0.0.1 / 2007-09-26
|
227
|
+
|
228
|
+
* Add dependency on custom_callbacks
|
229
|
+
* Move test fixtures out of the test application root directory
|
230
|
+
* Improve documentation
|
231
|
+
* Remove the StateExtension module in favor of adding singleton methods to the stateful class
|
232
|
+
* Convert dos newlines to unix newlines
|
233
|
+
* Fix error message when a given event can't be found in the database
|
234
|
+
* Add before_#{action} and #{action} callbacks when an event is performed
|
235
|
+
* All state and event callbacks can now explicitly return false in order to cancel the action
|
236
|
+
* Refactor ActiveState callback creation
|
237
|
+
* Refactor unit tests so that they use mock classes instead of themselves
|
238
|
+
* Allow force_reload option to be set in the state association
|
239
|
+
* Don't save the entire model when updating the state_id
|
240
|
+
* Raise exception if a class tries to define a state more than once
|
241
|
+
* Add tests for PluginAWeek::Has::States::ActiveState
|
242
|
+
* Refactor active state/active event creation
|
243
|
+
* Fix owner_type not being set correctly in active states/events of subclasses
|
244
|
+
* Allow subclasses to override the initial state
|
245
|
+
* Fix problem with migrations using default null when column cannot be null
|
246
|
+
* Moved deadline support into a separate plugin (has_state_deadlines).
|
247
|
+
* Added many more unit tests.
|
248
|
+
* Simplified many of the interfaces for maintainability.
|
249
|
+
* Added support for turning off recording state changes.
|
250
|
+
* Removed the short_description and long_description columns, in favor of an optional human_name column.
|
251
|
+
* Fixed not overriding the correct equality methods in the StateTransition class.
|
252
|
+
* Added to_sym to State and Event.
|
253
|
+
* State#name and Event#name now return the string version of the name instead of the symbol version.
|
254
|
+
* Added State#human_name and Event#human_name to automatically figure out what the human name is if it isn't specified in the table.
|
255
|
+
* Updated manual rollbacks to use the new Rails edge api (ActiveRecord::Rollback exception).
|
256
|
+
* Moved StateExtension class into a separate file in order to help keep the has_state files clean.
|
257
|
+
* Renamed InvalidState and InvalidEvent exceptions to StateNotFound and EventNotFound in order to follow the ActiveRecord convention (i.e. RecordNotFound).
|
258
|
+
* Added StateNotActive and EventNotActive exceptions to help differentiate between states which don't exist and states which weren't defined in the class.
|
259
|
+
* Added support for defining callbacks like so:
|
260
|
+
|
261
|
+
def before_exit_parked
|
262
|
+
end
|
263
|
+
|
264
|
+
def after_enter_idling
|
265
|
+
end
|
266
|
+
|
267
|
+
* Added support for defining callbacks using class methods:
|
268
|
+
|
269
|
+
before_exit_parked :fasten_seatbelt
|
270
|
+
|
271
|
+
* Added event callbacks after the transition has occurred (e.g. after_park)
|
272
|
+
* State callbacks no longer receive any of the arguments that were provided in the event action
|
273
|
+
* Updated license to include our names.
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006-2009 Aaron Pfefier
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,466 @@
|
|
1
|
+
== state_machine
|
2
|
+
|
3
|
+
+state_machine+ adds support for creating state machines for attributes on any
|
4
|
+
Ruby class.
|
5
|
+
|
6
|
+
== Resources
|
7
|
+
|
8
|
+
API
|
9
|
+
|
10
|
+
* http://api.pluginaweek.org/state_machine
|
11
|
+
|
12
|
+
Bugs
|
13
|
+
|
14
|
+
* http://pluginaweek.lighthouseapp.com/projects/13288-state_machine
|
15
|
+
|
16
|
+
Development
|
17
|
+
|
18
|
+
* http://github.com/pluginaweek/state_machine
|
19
|
+
|
20
|
+
Source
|
21
|
+
|
22
|
+
* git://github.com/pluginaweek/state_machine.git
|
23
|
+
|
24
|
+
== Description
|
25
|
+
|
26
|
+
State machines make it dead-simple to manage the behavior of a class. Too often,
|
27
|
+
the state of an object is kept by creating multiple boolean attributes and
|
28
|
+
deciding how to behave based on the values. This can become cumbersome and
|
29
|
+
difficult to maintain when the complexity of your class starts to increase.
|
30
|
+
|
31
|
+
+state_machine+ simplifies this design by introducing the various parts of a real
|
32
|
+
state machine, including states, events, transitions, and callbacks. However,
|
33
|
+
the api is designed to be so simple you don't even need to know what a
|
34
|
+
state machine is :)
|
35
|
+
|
36
|
+
Some brief, high-level features include:
|
37
|
+
* Defining state machines on any Ruby class
|
38
|
+
* Multiple state machines on a single class
|
39
|
+
* Namespaced state machines
|
40
|
+
* before/after transition hooks with explicit transition requirements
|
41
|
+
* ActiveRecord integration
|
42
|
+
* DataMapper integration
|
43
|
+
* Sequel integration
|
44
|
+
* State predicates
|
45
|
+
* State-driven instance / class behavior
|
46
|
+
* State values of any data type
|
47
|
+
* Dynamically-generated state values
|
48
|
+
* Event parallelization
|
49
|
+
* Attribute-based event transitions
|
50
|
+
* Inheritance
|
51
|
+
* Internationalization
|
52
|
+
* GraphViz visualization creator
|
53
|
+
|
54
|
+
Examples of the usage patterns for some of the above features are shown below.
|
55
|
+
You can find much more detailed documentation in the actual API.
|
56
|
+
|
57
|
+
== Usage
|
58
|
+
|
59
|
+
=== Example
|
60
|
+
|
61
|
+
Below is an example of many of the features offered by this plugin, including:
|
62
|
+
* Initial states
|
63
|
+
* Namespaced states
|
64
|
+
* Transition callbacks
|
65
|
+
* Conditional transitions
|
66
|
+
* State-driven instance behavior
|
67
|
+
* Customized state values
|
68
|
+
* Parallel events
|
69
|
+
|
70
|
+
Class definition:
|
71
|
+
|
72
|
+
class Vehicle
|
73
|
+
attr_accessor :seatbelt_on
|
74
|
+
|
75
|
+
state_machine :state, :initial => :parked do
|
76
|
+
before_transition :parked => any - :parked, :do => :put_on_seatbelt
|
77
|
+
|
78
|
+
after_transition :on => :crash, :do => :tow
|
79
|
+
after_transition :on => :repair, :do => :fix
|
80
|
+
after_transition any => :parked do |vehicle, transition|
|
81
|
+
vehicle.seatbelt_on = false
|
82
|
+
end
|
83
|
+
|
84
|
+
event :park do
|
85
|
+
transition [:idling, :first_gear] => :parked
|
86
|
+
end
|
87
|
+
|
88
|
+
event :ignite do
|
89
|
+
transition :stalled => same, :parked => :idling
|
90
|
+
end
|
91
|
+
|
92
|
+
event :idle do
|
93
|
+
transition :first_gear => :idling
|
94
|
+
end
|
95
|
+
|
96
|
+
event :shift_up do
|
97
|
+
transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
|
98
|
+
end
|
99
|
+
|
100
|
+
event :shift_down do
|
101
|
+
transition :third_gear => :second_gear, :second_gear => :first_gear
|
102
|
+
end
|
103
|
+
|
104
|
+
event :crash do
|
105
|
+
transition all - [:parked, :stalled] => :stalled, :unless => :auto_shop_busy?
|
106
|
+
end
|
107
|
+
|
108
|
+
event :repair do
|
109
|
+
transition :stalled => :parked, :if => :auto_shop_busy?
|
110
|
+
end
|
111
|
+
|
112
|
+
state :parked do
|
113
|
+
def speed
|
114
|
+
0
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
state :idling, :first_gear do
|
119
|
+
def speed
|
120
|
+
10
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
state :second_gear do
|
125
|
+
def speed
|
126
|
+
20
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
|
132
|
+
event :enable do
|
133
|
+
transition all => :active
|
134
|
+
end
|
135
|
+
|
136
|
+
event :disable do
|
137
|
+
transition all => :off
|
138
|
+
end
|
139
|
+
|
140
|
+
state :active, :value => 1
|
141
|
+
state :off, :value => 0
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize
|
145
|
+
@seatbelt_on = false
|
146
|
+
super() # NOTE: This *must* be called, otherwise states won't get initialized
|
147
|
+
end
|
148
|
+
|
149
|
+
def put_on_seatbelt
|
150
|
+
@seatbelt_on = true
|
151
|
+
end
|
152
|
+
|
153
|
+
def auto_shop_busy?
|
154
|
+
false
|
155
|
+
end
|
156
|
+
|
157
|
+
def tow
|
158
|
+
# tow the vehicle
|
159
|
+
end
|
160
|
+
|
161
|
+
def fix
|
162
|
+
# get the vehicle fixed by a mechanic
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
*Note* the comment made on the +initialize+ method in the class. In order for
|
167
|
+
state machine attributes to be properly initialized, <tt>super()</tt> must be called.
|
168
|
+
See StateMachine::MacroMethods for more information about this.
|
169
|
+
|
170
|
+
Using the above class as an example, you can interact with the state machine
|
171
|
+
like so:
|
172
|
+
|
173
|
+
vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
|
174
|
+
vehicle.state # => "parked"
|
175
|
+
vehicle.state_name # => :parked
|
176
|
+
vehicle.parked? # => true
|
177
|
+
vehicle.can_ignite? # => true
|
178
|
+
vehicle.ignite_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
179
|
+
vehicle.state_events # => [:ignite]
|
180
|
+
vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
|
181
|
+
vehicle.speed # => 0
|
182
|
+
|
183
|
+
vehicle.ignite # => true
|
184
|
+
vehicle.parked? # => false
|
185
|
+
vehicle.idling? # => true
|
186
|
+
vehicle.speed # => 10
|
187
|
+
vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
|
188
|
+
|
189
|
+
vehicle.shift_up # => true
|
190
|
+
vehicle.speed # => 10
|
191
|
+
vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
|
192
|
+
|
193
|
+
vehicle.shift_up # => true
|
194
|
+
vehicle.speed # => 20
|
195
|
+
vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
|
196
|
+
|
197
|
+
# The bang (!) operator can raise exceptions if the event fails
|
198
|
+
vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from :second_gear
|
199
|
+
|
200
|
+
# Generic state predicates can raise exceptions if the value does not exist
|
201
|
+
vehicle.state?(:parked) # => false
|
202
|
+
vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
|
203
|
+
|
204
|
+
# Namespaced machines have uniquely-generated methods
|
205
|
+
vehicle.alarm_state # => 1
|
206
|
+
vehicle.alarm_state_name # => :active
|
207
|
+
|
208
|
+
vehicle.can_disable_alarm? # => true
|
209
|
+
vehicle.disable_alarm # => true
|
210
|
+
vehicle.alarm_state # => 0
|
211
|
+
vehicle.alarm_state_name # => :off
|
212
|
+
vehicle.can_enable_alarm? # => true
|
213
|
+
|
214
|
+
vehicle.alarm_off? # => true
|
215
|
+
vehicle.alarm_active? # => false
|
216
|
+
|
217
|
+
# Events can be fired in parallel
|
218
|
+
vehicle.fire_events(:shift_down, :enable_alarm) # => true
|
219
|
+
vehicle.state_name # => :first_gear
|
220
|
+
vehicle.alarm_state_name # => :active
|
221
|
+
|
222
|
+
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachine::InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
|
223
|
+
|
224
|
+
== Integrations
|
225
|
+
|
226
|
+
In addition to being able to define state machines on all Ruby classes, a set of
|
227
|
+
out-of-the-box integrations are available for some of the more popular Ruby
|
228
|
+
libraries. These integrations add library-specific behavior, allowing for state
|
229
|
+
machines to work more tightly with the conventions defined by those libraries.
|
230
|
+
|
231
|
+
The integrations currently available include:
|
232
|
+
* ActiveRecord models
|
233
|
+
* DataMapper resources
|
234
|
+
* Sequel models
|
235
|
+
|
236
|
+
A brief overview of these integrations is described below.
|
237
|
+
|
238
|
+
=== ActiveRecord
|
239
|
+
|
240
|
+
The ActiveRecord integration adds support for database transactions, automatically
|
241
|
+
saving the record, named scopes, validation errors, and observers. For example,
|
242
|
+
|
243
|
+
class Vehicle < ActiveRecord::Base
|
244
|
+
state_machine :initial => :parked do
|
245
|
+
before_transition :parked => any - :parked, :do => :put_on_seatbelt
|
246
|
+
after_transition any => :parked do |vehicle, transition|
|
247
|
+
vehicle.seatbelt = 'off'
|
248
|
+
end
|
249
|
+
|
250
|
+
event :ignite do
|
251
|
+
transition :parked => :idling
|
252
|
+
end
|
253
|
+
|
254
|
+
state :first_gear, :second_gear do
|
255
|
+
validates_presence_of :seatbelt_on
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def put_on_seatbelt
|
260
|
+
...
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
class VehicleObserver < ActiveRecord::Observer
|
265
|
+
# Callback for :ignite event *before* the transition is performed
|
266
|
+
def before_ignite(vehicle, transition)
|
267
|
+
# log message
|
268
|
+
end
|
269
|
+
|
270
|
+
# Generic transition callback *after* the transition is performed
|
271
|
+
def after_transition(vehicle, transition)
|
272
|
+
Audit.log(vehicle, transition)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
For more information about the various behaviors added for ActiveRecord state
|
277
|
+
machines, see StateMachine::Integrations::ActiveRecord.
|
278
|
+
|
279
|
+
=== DataMapper
|
280
|
+
|
281
|
+
Like the ActiveRecord integration, the DataMapper integration adds support for
|
282
|
+
database transactions, automatically saving the record, named scopes, Extlib-like
|
283
|
+
callbacks, validation errors, and observers. For example,
|
284
|
+
|
285
|
+
class Vehicle
|
286
|
+
include DataMapper::Resource
|
287
|
+
|
288
|
+
property :id, Serial
|
289
|
+
property :state, String
|
290
|
+
|
291
|
+
state_machine :initial => :parked do
|
292
|
+
before_transition :parked => any - :parked, :do => :put_on_seatbelt
|
293
|
+
after_transition any => :parked do |transition|
|
294
|
+
self.seatbelt = 'off' # self is the record
|
295
|
+
end
|
296
|
+
|
297
|
+
event :ignite do
|
298
|
+
transition :parked => :idling
|
299
|
+
end
|
300
|
+
|
301
|
+
state :first_gear, :second_gear do
|
302
|
+
validates_present :seatbelt_on
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def put_on_seatbelt
|
307
|
+
...
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
class VehicleObserver
|
312
|
+
include DataMapper::Observer
|
313
|
+
|
314
|
+
observe Vehicle
|
315
|
+
|
316
|
+
# Callback for :ignite event *before* the transition is performed
|
317
|
+
before_transition :on => :ignite do |transition|
|
318
|
+
# log message (self is the record)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Generic transition callback *after* the transition is performed
|
322
|
+
after_transition do |transition|
|
323
|
+
Audit.log(self, transition) # self is the record
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
*Note* that the DataMapper::Observer integration is optional and only available
|
328
|
+
when the dm-observer library is installed.
|
329
|
+
|
330
|
+
For more information about the various behaviors added for DataMapper state
|
331
|
+
machines, see StateMachine::Integrations::DataMapper.
|
332
|
+
|
333
|
+
=== Sequel
|
334
|
+
|
335
|
+
Like the ActiveRecord integration, the Sequel integration adds support for
|
336
|
+
database transactions, automatically saving the record, named scopes, validation
|
337
|
+
errors and callbacks. For example,
|
338
|
+
|
339
|
+
class Vehicle < Sequel::Model
|
340
|
+
state_machine :initial => :parked do
|
341
|
+
before_transition :parked => any - :parked, :do => :put_on_seatbelt
|
342
|
+
after_transition any => :parked do |transition|
|
343
|
+
self.seatbelt = 'off' # self is the record
|
344
|
+
end
|
345
|
+
|
346
|
+
event :ignite do
|
347
|
+
transition :parked => :idling
|
348
|
+
end
|
349
|
+
|
350
|
+
state :first_gear, :second_gear do
|
351
|
+
validates_presence_of :seatbelt_on
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def put_on_seatbelt
|
356
|
+
...
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
For more information about the various behaviors added for Sequel state
|
361
|
+
machines, see StateMachine::Integrations::Sequel.
|
362
|
+
|
363
|
+
== Compatibility
|
364
|
+
|
365
|
+
Although state_machine introduces a simplified syntax, it still remains
|
366
|
+
backwards compatible with previous versions and other state-related libraries.
|
367
|
+
For example, transitions and callbacks can continue to be defined like so:
|
368
|
+
|
369
|
+
class Vehicle
|
370
|
+
state_machine :initial => :parked do
|
371
|
+
before_transition :from => :parked, :except_to => :parked, :do => :put_on_seatbelt
|
372
|
+
after_transition :to => :parked do |transition|
|
373
|
+
self.seatbelt = 'off' # self is the record
|
374
|
+
end
|
375
|
+
|
376
|
+
event :ignite do
|
377
|
+
transition :from => :parked, :to => :idling
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
Although this verbose syntax will most likely always be supported, it is
|
383
|
+
recommended that any state machines eventually migrate to the syntax introduced
|
384
|
+
in version 0.6.0.
|
385
|
+
|
386
|
+
== Tools
|
387
|
+
|
388
|
+
=== Generating graphs
|
389
|
+
|
390
|
+
This library comes with built-in support for generating di-graphs based on the
|
391
|
+
events, states, and transitions defined for a state machine using GraphViz[http://www.graphviz.org].
|
392
|
+
This requires that both the <tt>ruby-graphviz</tt> gem and graphviz library be
|
393
|
+
installed on the system.
|
394
|
+
|
395
|
+
==== Examples
|
396
|
+
|
397
|
+
To generate a graph for a specific file / class:
|
398
|
+
|
399
|
+
rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle
|
400
|
+
|
401
|
+
To save files to a specific path:
|
402
|
+
|
403
|
+
rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files
|
404
|
+
|
405
|
+
To customize the image format / orientation:
|
406
|
+
|
407
|
+
rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg ORIENTATION=landscape
|
408
|
+
|
409
|
+
To generate multiple state machine graphs:
|
410
|
+
|
411
|
+
rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
|
412
|
+
|
413
|
+
*Note* that this will generate a different file for every state machine defined
|
414
|
+
in the class. The generated files will use an output filename of the format
|
415
|
+
#{class_name}_#{attribute}.#{format}.
|
416
|
+
|
417
|
+
For examples of actual images generated using this task, see those under the
|
418
|
+
examples folder.
|
419
|
+
|
420
|
+
==== Ruby on Rails Integration
|
421
|
+
|
422
|
+
There is a special integration Rake task for generating state machines for
|
423
|
+
classes used in a Ruby on Rails application. This task will load the application
|
424
|
+
environment, meaning that it's unnecessary to specify the actual file to load.
|
425
|
+
|
426
|
+
For example,
|
427
|
+
|
428
|
+
rake state_machine:draw:rails CLASS=Vehicle
|
429
|
+
|
430
|
+
==== Merb Integration
|
431
|
+
|
432
|
+
Like Ruby on Rails, there is a special integration Rake task for generating
|
433
|
+
state machines for classes used in a Merb application. This task will load the
|
434
|
+
application environment, meaning that it's unnecessary to specify the actual
|
435
|
+
files to load.
|
436
|
+
|
437
|
+
For example,
|
438
|
+
|
439
|
+
rake state_machine:draw:merb CLASS=Vehicle
|
440
|
+
|
441
|
+
=== Interactive graphs
|
442
|
+
|
443
|
+
Jean Bovet - {Visual Automata Simulator}[http://www.cs.usfca.edu/~jbovet/vas.html].
|
444
|
+
This is a great tool for "simulating, visualizing and transforming finite state
|
445
|
+
automata and Turing Machines". This tool can help in the creation of states and
|
446
|
+
events for your models. It is cross-platform, written in Java.
|
447
|
+
|
448
|
+
== Testing
|
449
|
+
|
450
|
+
To run the entire test suite (will test ActiveRecord, DataMapper, and Sequel
|
451
|
+
integrations if the proper dependencies are available):
|
452
|
+
|
453
|
+
rake test
|
454
|
+
|
455
|
+
Target specific versions of integrations like so:
|
456
|
+
|
457
|
+
rake test AR_VERSION=2.1.0 DM_VERSION=0.9.4 SEQUEL_VERSION=2.8.0
|
458
|
+
|
459
|
+
== Dependencies
|
460
|
+
|
461
|
+
By default, there are no dependencies. If using specific integrations, those
|
462
|
+
dependencies are listed below.
|
463
|
+
|
464
|
+
* ActiveRecord[http://rubyonrails.org] integration: 2.1.0 or later
|
465
|
+
* DataMapper[http://datamapper.org] integration: 0.9.4 or later
|
466
|
+
* Sequel[http://sequel.rubyforge.org] integration: 2.8.0 or later
|