culturecode-state_machine 1.2.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 (307) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +72 -0
  4. data/.yardopts +5 -0
  5. data/Appraisals +491 -0
  6. data/CHANGELOG.md +502 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE +20 -0
  9. data/README.md +1244 -0
  10. data/Rakefile +41 -0
  11. data/examples/AutoShop_state.png +0 -0
  12. data/examples/Car_state.png +0 -0
  13. data/examples/Gemfile +5 -0
  14. data/examples/Gemfile.lock +14 -0
  15. data/examples/TrafficLight_state.png +0 -0
  16. data/examples/Vehicle_state.png +0 -0
  17. data/examples/auto_shop.rb +13 -0
  18. data/examples/car.rb +21 -0
  19. data/examples/doc/AutoShop.html +2856 -0
  20. data/examples/doc/AutoShop_state.png +0 -0
  21. data/examples/doc/Car.html +919 -0
  22. data/examples/doc/Car_state.png +0 -0
  23. data/examples/doc/TrafficLight.html +2230 -0
  24. data/examples/doc/TrafficLight_state.png +0 -0
  25. data/examples/doc/Vehicle.html +7921 -0
  26. data/examples/doc/Vehicle_state.png +0 -0
  27. data/examples/doc/_index.html +136 -0
  28. data/examples/doc/class_list.html +47 -0
  29. data/examples/doc/css/common.css +1 -0
  30. data/examples/doc/css/full_list.css +55 -0
  31. data/examples/doc/css/style.css +322 -0
  32. data/examples/doc/file_list.html +46 -0
  33. data/examples/doc/frames.html +13 -0
  34. data/examples/doc/index.html +136 -0
  35. data/examples/doc/js/app.js +205 -0
  36. data/examples/doc/js/full_list.js +173 -0
  37. data/examples/doc/js/jquery.js +16 -0
  38. data/examples/doc/method_list.html +734 -0
  39. data/examples/doc/top-level-namespace.html +105 -0
  40. data/examples/merb-rest/controller.rb +51 -0
  41. data/examples/merb-rest/model.rb +28 -0
  42. data/examples/merb-rest/view_edit.html.erb +24 -0
  43. data/examples/merb-rest/view_index.html.erb +23 -0
  44. data/examples/merb-rest/view_new.html.erb +13 -0
  45. data/examples/merb-rest/view_show.html.erb +17 -0
  46. data/examples/rails-rest/controller.rb +43 -0
  47. data/examples/rails-rest/migration.rb +7 -0
  48. data/examples/rails-rest/model.rb +23 -0
  49. data/examples/rails-rest/view__form.html.erb +34 -0
  50. data/examples/rails-rest/view_edit.html.erb +6 -0
  51. data/examples/rails-rest/view_index.html.erb +25 -0
  52. data/examples/rails-rest/view_new.html.erb +5 -0
  53. data/examples/rails-rest/view_show.html.erb +19 -0
  54. data/examples/traffic_light.rb +9 -0
  55. data/examples/vehicle.rb +33 -0
  56. data/gemfiles/active_model_3.0.0.gemfile +7 -0
  57. data/gemfiles/active_model_3.0.0.gemfile.lock +35 -0
  58. data/gemfiles/active_model_3.0.5.gemfile +7 -0
  59. data/gemfiles/active_model_3.0.5.gemfile.lock +35 -0
  60. data/gemfiles/active_model_3.1.1.gemfile +7 -0
  61. data/gemfiles/active_model_3.1.1.gemfile.lock +36 -0
  62. data/gemfiles/active_model_3.2.1.gemfile +7 -0
  63. data/gemfiles/active_model_3.2.12.gemfile +7 -0
  64. data/gemfiles/active_model_3.2.12.gemfile.lock +36 -0
  65. data/gemfiles/active_model_3.2.13.rc1.gemfile +7 -0
  66. data/gemfiles/active_model_3.2.13.rc1.gemfile.lock +36 -0
  67. data/gemfiles/active_model_4.0.0.gemfile +9 -0
  68. data/gemfiles/active_model_4.0.0.gemfile.lock +78 -0
  69. data/gemfiles/active_record_2.0.0.gemfile +9 -0
  70. data/gemfiles/active_record_2.0.0.gemfile.lock +39 -0
  71. data/gemfiles/active_record_2.0.5.gemfile +9 -0
  72. data/gemfiles/active_record_2.0.5.gemfile.lock +39 -0
  73. data/gemfiles/active_record_2.1.0.gemfile +9 -0
  74. data/gemfiles/active_record_2.1.0.gemfile.lock +39 -0
  75. data/gemfiles/active_record_2.1.2.gemfile +9 -0
  76. data/gemfiles/active_record_2.1.2.gemfile.lock +39 -0
  77. data/gemfiles/active_record_2.2.3.gemfile +9 -0
  78. data/gemfiles/active_record_2.2.3.gemfile.lock +39 -0
  79. data/gemfiles/active_record_2.3.12.gemfile +9 -0
  80. data/gemfiles/active_record_2.3.12.gemfile.lock +39 -0
  81. data/gemfiles/active_record_2.3.5.gemfile +9 -0
  82. data/gemfiles/active_record_2.3.5.gemfile.lock +39 -0
  83. data/gemfiles/active_record_3.0.0.gemfile +9 -0
  84. data/gemfiles/active_record_3.0.0.gemfile.lock +51 -0
  85. data/gemfiles/active_record_3.0.5.gemfile +9 -0
  86. data/gemfiles/active_record_3.0.5.gemfile.lock +50 -0
  87. data/gemfiles/active_record_3.1.1.gemfile +9 -0
  88. data/gemfiles/active_record_3.1.1.gemfile.lock +51 -0
  89. data/gemfiles/active_record_3.2.12.gemfile +9 -0
  90. data/gemfiles/active_record_3.2.12.gemfile.lock +51 -0
  91. data/gemfiles/active_record_3.2.13.rc1.gemfile +9 -0
  92. data/gemfiles/active_record_3.2.13.rc1.gemfile.lock +51 -0
  93. data/gemfiles/active_record_4.0.0.gemfile +11 -0
  94. data/gemfiles/active_record_4.0.0.gemfile.lock +83 -0
  95. data/gemfiles/data_mapper_0.10.2.gemfile +13 -0
  96. data/gemfiles/data_mapper_0.10.2.gemfile.lock +56 -0
  97. data/gemfiles/data_mapper_0.9.11.gemfile +13 -0
  98. data/gemfiles/data_mapper_0.9.11.gemfile.lock +71 -0
  99. data/gemfiles/data_mapper_0.9.4.gemfile +12 -0
  100. data/gemfiles/data_mapper_0.9.4.gemfile.lock +70 -0
  101. data/gemfiles/data_mapper_0.9.7.gemfile +13 -0
  102. data/gemfiles/data_mapper_0.9.7.gemfile.lock +67 -0
  103. data/gemfiles/data_mapper_1.0.0.gemfile +12 -0
  104. data/gemfiles/data_mapper_1.0.0.gemfile.lock +63 -0
  105. data/gemfiles/data_mapper_1.0.1.gemfile +12 -0
  106. data/gemfiles/data_mapper_1.0.1.gemfile.lock +63 -0
  107. data/gemfiles/data_mapper_1.0.2.gemfile +12 -0
  108. data/gemfiles/data_mapper_1.0.2.gemfile.lock +63 -0
  109. data/gemfiles/data_mapper_1.1.0.gemfile +12 -0
  110. data/gemfiles/data_mapper_1.1.0.gemfile.lock +61 -0
  111. data/gemfiles/data_mapper_1.2.0.gemfile +12 -0
  112. data/gemfiles/data_mapper_1.2.0.gemfile.lock +61 -0
  113. data/gemfiles/default.gemfile +7 -0
  114. data/gemfiles/default.gemfile.lock +27 -0
  115. data/gemfiles/graphviz_0.9.17.gemfile +7 -0
  116. data/gemfiles/graphviz_0.9.17.gemfile.lock +29 -0
  117. data/gemfiles/graphviz_0.9.21.gemfile +7 -0
  118. data/gemfiles/graphviz_0.9.21.gemfile.lock +29 -0
  119. data/gemfiles/graphviz_1.0.0.gemfile +7 -0
  120. data/gemfiles/graphviz_1.0.0.gemfile.lock +29 -0
  121. data/gemfiles/graphviz_1.0.3.gemfile +7 -0
  122. data/gemfiles/graphviz_1.0.3.gemfile.lock +29 -0
  123. data/gemfiles/graphviz_1.0.8.gemfile +7 -0
  124. data/gemfiles/graphviz_1.0.8.gemfile.lock +29 -0
  125. data/gemfiles/mongo_mapper_0.10.0.gemfile +8 -0
  126. data/gemfiles/mongo_mapper_0.10.0.gemfile.lock +47 -0
  127. data/gemfiles/mongo_mapper_0.11.2.gemfile +9 -0
  128. data/gemfiles/mongo_mapper_0.11.2.gemfile.lock +48 -0
  129. data/gemfiles/mongo_mapper_0.12.0.gemfile +9 -0
  130. data/gemfiles/mongo_mapper_0.12.0.gemfile.lock +48 -0
  131. data/gemfiles/mongo_mapper_0.5.5.gemfile +8 -0
  132. data/gemfiles/mongo_mapper_0.5.5.gemfile.lock +36 -0
  133. data/gemfiles/mongo_mapper_0.5.8.gemfile +8 -0
  134. data/gemfiles/mongo_mapper_0.5.8.gemfile.lock +36 -0
  135. data/gemfiles/mongo_mapper_0.6.0.gemfile +8 -0
  136. data/gemfiles/mongo_mapper_0.6.0.gemfile.lock +36 -0
  137. data/gemfiles/mongo_mapper_0.6.10.gemfile +8 -0
  138. data/gemfiles/mongo_mapper_0.6.10.gemfile.lock +36 -0
  139. data/gemfiles/mongo_mapper_0.7.0.gemfile +8 -0
  140. data/gemfiles/mongo_mapper_0.7.0.gemfile.lock +36 -0
  141. data/gemfiles/mongo_mapper_0.7.5.gemfile +8 -0
  142. data/gemfiles/mongo_mapper_0.7.5.gemfile.lock +39 -0
  143. data/gemfiles/mongo_mapper_0.8.0.gemfile +10 -0
  144. data/gemfiles/mongo_mapper_0.8.0.gemfile.lock +43 -0
  145. data/gemfiles/mongo_mapper_0.8.3.gemfile +10 -0
  146. data/gemfiles/mongo_mapper_0.8.3.gemfile.lock +43 -0
  147. data/gemfiles/mongo_mapper_0.8.4.gemfile +8 -0
  148. data/gemfiles/mongo_mapper_0.8.4.gemfile.lock +42 -0
  149. data/gemfiles/mongo_mapper_0.8.6.gemfile +8 -0
  150. data/gemfiles/mongo_mapper_0.8.6.gemfile.lock +42 -0
  151. data/gemfiles/mongo_mapper_0.9.0.gemfile +7 -0
  152. data/gemfiles/mongo_mapper_0.9.0.gemfile.lock +45 -0
  153. data/gemfiles/mongoid_2.0.0.gemfile +9 -0
  154. data/gemfiles/mongoid_2.0.0.gemfile.lock +49 -0
  155. data/gemfiles/mongoid_2.1.4.gemfile +9 -0
  156. data/gemfiles/mongoid_2.1.4.gemfile.lock +47 -0
  157. data/gemfiles/mongoid_2.2.4.gemfile +9 -0
  158. data/gemfiles/mongoid_2.2.4.gemfile.lock +47 -0
  159. data/gemfiles/mongoid_2.3.3.gemfile +9 -0
  160. data/gemfiles/mongoid_2.3.3.gemfile.lock +47 -0
  161. data/gemfiles/mongoid_2.4.0.gemfile +9 -0
  162. data/gemfiles/mongoid_2.4.0.gemfile.lock +47 -0
  163. data/gemfiles/mongoid_2.4.10.gemfile +9 -0
  164. data/gemfiles/mongoid_2.4.10.gemfile.lock +47 -0
  165. data/gemfiles/mongoid_2.5.2.gemfile +9 -0
  166. data/gemfiles/mongoid_2.5.2.gemfile.lock +47 -0
  167. data/gemfiles/mongoid_2.6.0.gemfile +9 -0
  168. data/gemfiles/mongoid_2.6.0.gemfile.lock +47 -0
  169. data/gemfiles/mongoid_3.0.0.gemfile +8 -0
  170. data/gemfiles/mongoid_3.0.0.gemfile.lock +45 -0
  171. data/gemfiles/mongoid_3.0.22.gemfile +8 -0
  172. data/gemfiles/mongoid_3.0.22.gemfile.lock +45 -0
  173. data/gemfiles/mongoid_3.1.0.gemfile +8 -0
  174. data/gemfiles/mongoid_3.1.0.gemfile.lock +45 -0
  175. data/gemfiles/sequel_2.11.0.gemfile +9 -0
  176. data/gemfiles/sequel_2.11.0.gemfile.lock +33 -0
  177. data/gemfiles/sequel_2.12.0.gemfile +9 -0
  178. data/gemfiles/sequel_2.12.0.gemfile.lock +33 -0
  179. data/gemfiles/sequel_2.8.0.gemfile +9 -0
  180. data/gemfiles/sequel_2.8.0.gemfile.lock +33 -0
  181. data/gemfiles/sequel_3.0.0.gemfile +9 -0
  182. data/gemfiles/sequel_3.0.0.gemfile.lock +33 -0
  183. data/gemfiles/sequel_3.10.0.gemfile +9 -0
  184. data/gemfiles/sequel_3.10.0.gemfile.lock +33 -0
  185. data/gemfiles/sequel_3.13.0.gemfile +9 -0
  186. data/gemfiles/sequel_3.13.0.gemfile.lock +33 -0
  187. data/gemfiles/sequel_3.14.0.gemfile +9 -0
  188. data/gemfiles/sequel_3.14.0.gemfile.lock +33 -0
  189. data/gemfiles/sequel_3.23.0.gemfile +9 -0
  190. data/gemfiles/sequel_3.23.0.gemfile.lock +33 -0
  191. data/gemfiles/sequel_3.24.0.gemfile +9 -0
  192. data/gemfiles/sequel_3.24.0.gemfile.lock +33 -0
  193. data/gemfiles/sequel_3.29.0.gemfile +9 -0
  194. data/gemfiles/sequel_3.29.0.gemfile.lock +33 -0
  195. data/gemfiles/sequel_3.34.0.gemfile +9 -0
  196. data/gemfiles/sequel_3.34.0.gemfile.lock +33 -0
  197. data/gemfiles/sequel_3.35.0.gemfile +9 -0
  198. data/gemfiles/sequel_3.35.0.gemfile.lock +33 -0
  199. data/gemfiles/sequel_3.4.0.gemfile +9 -0
  200. data/gemfiles/sequel_3.4.0.gemfile.lock +33 -0
  201. data/gemfiles/sequel_3.44.0.gemfile +9 -0
  202. data/gemfiles/sequel_3.44.0.gemfile.lock +33 -0
  203. data/init.rb +1 -0
  204. data/lib/state_machine.rb +8 -0
  205. data/lib/state_machine/assertions.rb +36 -0
  206. data/lib/state_machine/branch.rb +225 -0
  207. data/lib/state_machine/callback.rb +236 -0
  208. data/lib/state_machine/core.rb +12 -0
  209. data/lib/state_machine/core_ext.rb +2 -0
  210. data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
  211. data/lib/state_machine/error.rb +13 -0
  212. data/lib/state_machine/eval_helpers.rb +87 -0
  213. data/lib/state_machine/event.rb +257 -0
  214. data/lib/state_machine/event_collection.rb +141 -0
  215. data/lib/state_machine/extensions.rb +149 -0
  216. data/lib/state_machine/graph.rb +92 -0
  217. data/lib/state_machine/helper_module.rb +17 -0
  218. data/lib/state_machine/initializers.rb +4 -0
  219. data/lib/state_machine/initializers/merb.rb +1 -0
  220. data/lib/state_machine/initializers/rails.rb +25 -0
  221. data/lib/state_machine/integrations.rb +121 -0
  222. data/lib/state_machine/integrations/active_model.rb +585 -0
  223. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  224. data/lib/state_machine/integrations/active_model/observer.rb +33 -0
  225. data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
  226. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  227. data/lib/state_machine/integrations/active_record.rb +548 -0
  228. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  229. data/lib/state_machine/integrations/active_record/versions.rb +123 -0
  230. data/lib/state_machine/integrations/base.rb +100 -0
  231. data/lib/state_machine/integrations/data_mapper.rb +511 -0
  232. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  233. data/lib/state_machine/integrations/data_mapper/versions.rb +85 -0
  234. data/lib/state_machine/integrations/mongo_mapper.rb +389 -0
  235. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  236. data/lib/state_machine/integrations/mongo_mapper/versions.rb +89 -0
  237. data/lib/state_machine/integrations/mongoid.rb +461 -0
  238. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  239. data/lib/state_machine/integrations/mongoid/versions.rb +81 -0
  240. data/lib/state_machine/integrations/sequel.rb +486 -0
  241. data/lib/state_machine/integrations/sequel/versions.rb +95 -0
  242. data/lib/state_machine/machine.rb +2292 -0
  243. data/lib/state_machine/machine_collection.rb +86 -0
  244. data/lib/state_machine/macro_methods.rb +522 -0
  245. data/lib/state_machine/matcher.rb +123 -0
  246. data/lib/state_machine/matcher_helpers.rb +54 -0
  247. data/lib/state_machine/node_collection.rb +222 -0
  248. data/lib/state_machine/path.rb +120 -0
  249. data/lib/state_machine/path_collection.rb +90 -0
  250. data/lib/state_machine/state.rb +297 -0
  251. data/lib/state_machine/state_collection.rb +112 -0
  252. data/lib/state_machine/state_context.rb +138 -0
  253. data/lib/state_machine/transition.rb +470 -0
  254. data/lib/state_machine/transition_collection.rb +245 -0
  255. data/lib/state_machine/version.rb +3 -0
  256. data/lib/state_machine/yard.rb +8 -0
  257. data/lib/state_machine/yard/handlers.rb +12 -0
  258. data/lib/state_machine/yard/handlers/base.rb +32 -0
  259. data/lib/state_machine/yard/handlers/event.rb +25 -0
  260. data/lib/state_machine/yard/handlers/machine.rb +344 -0
  261. data/lib/state_machine/yard/handlers/state.rb +25 -0
  262. data/lib/state_machine/yard/handlers/transition.rb +47 -0
  263. data/lib/state_machine/yard/templates.rb +3 -0
  264. data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
  265. data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  266. data/lib/tasks/state_machine.rake +1 -0
  267. data/lib/tasks/state_machine.rb +30 -0
  268. data/lib/yard-state_machine.rb +2 -0
  269. data/state_machine.gemspec +22 -0
  270. data/test/files/en.yml +17 -0
  271. data/test/files/switch.rb +15 -0
  272. data/test/functional/state_machine_test.rb +1066 -0
  273. data/test/test_helper.rb +7 -0
  274. data/test/unit/assertions_test.rb +40 -0
  275. data/test/unit/branch_test.rb +969 -0
  276. data/test/unit/callback_test.rb +704 -0
  277. data/test/unit/error_test.rb +43 -0
  278. data/test/unit/eval_helpers_test.rb +270 -0
  279. data/test/unit/event_collection_test.rb +398 -0
  280. data/test/unit/event_test.rb +1196 -0
  281. data/test/unit/graph_test.rb +98 -0
  282. data/test/unit/helper_module_test.rb +17 -0
  283. data/test/unit/integrations/active_model_test.rb +1245 -0
  284. data/test/unit/integrations/active_record_test.rb +2551 -0
  285. data/test/unit/integrations/base_test.rb +104 -0
  286. data/test/unit/integrations/data_mapper_test.rb +2194 -0
  287. data/test/unit/integrations/mongo_mapper_test.rb +2026 -0
  288. data/test/unit/integrations/mongoid_test.rb +2309 -0
  289. data/test/unit/integrations/sequel_test.rb +1896 -0
  290. data/test/unit/integrations_test.rb +83 -0
  291. data/test/unit/invalid_event_test.rb +20 -0
  292. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  293. data/test/unit/invalid_transition_test.rb +115 -0
  294. data/test/unit/machine_collection_test.rb +603 -0
  295. data/test/unit/machine_test.rb +3407 -0
  296. data/test/unit/matcher_helpers_test.rb +37 -0
  297. data/test/unit/matcher_test.rb +155 -0
  298. data/test/unit/node_collection_test.rb +362 -0
  299. data/test/unit/path_collection_test.rb +266 -0
  300. data/test/unit/path_test.rb +485 -0
  301. data/test/unit/state_collection_test.rb +352 -0
  302. data/test/unit/state_context_test.rb +441 -0
  303. data/test/unit/state_machine_test.rb +31 -0
  304. data/test/unit/state_test.rb +1101 -0
  305. data/test/unit/transition_collection_test.rb +2168 -0
  306. data/test/unit/transition_test.rb +1558 -0
  307. metadata +435 -0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://www.rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2006-2012 Aaron Pfeifer
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.md ADDED
@@ -0,0 +1,1244 @@
1
+ # state_machine [![Build Status](https://secure.travis-ci.org/pluginaweek/state_machine.png "Build Status")](http://travis-ci.org/pluginaweek/state_machine) [![Dependency Status](https://gemnasium.com/pluginaweek/state_machine.png "Dependency Status")](https://gemnasium.com/pluginaweek/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://rdoc.info/github/pluginaweek/state_machine/master/frames
11
+
12
+ Bugs
13
+
14
+ * http://github.com/pluginaweek/state_machine/issues
15
+
16
+ Development
17
+
18
+ * http://github.com/pluginaweek/state_machine
19
+
20
+ Testing
21
+
22
+ * http://travis-ci.org/pluginaweek/state_machine
23
+
24
+ Source
25
+
26
+ * git://github.com/pluginaweek/state_machine.git
27
+
28
+ Mailing List
29
+
30
+ * http://groups.google.com/group/pluginaweek-talk
31
+
32
+ ## Description
33
+
34
+ State machines make it dead-simple to manage the behavior of a class. Too often,
35
+ the state of an object is kept by creating multiple boolean attributes and
36
+ deciding how to behave based on the values. This can become cumbersome and
37
+ difficult to maintain when the complexity of your class starts to increase.
38
+
39
+ *state_machine* simplifies this design by introducing the various parts of a real
40
+ state machine, including states, events, transitions, and callbacks. However,
41
+ the api is designed to be so simple you don't even need to know what a
42
+ state machine is :)
43
+
44
+ Some brief, high-level features include:
45
+
46
+ * Defining state machines on any Ruby class
47
+ * Multiple state machines on a single class
48
+ * Namespaced state machines
49
+ * before/after/around/failure transition hooks with explicit transition requirements
50
+ * Integration with ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel
51
+ * State predicates
52
+ * State-driven instance / class behavior
53
+ * State values of any data type
54
+ * Dynamically-generated state values
55
+ * Event parallelization
56
+ * Attribute-based event transitions
57
+ * Path analysis
58
+ * Inheritance
59
+ * Internationalization
60
+ * GraphViz visualization creator
61
+ * YARD integration (Ruby 1.9+ only)
62
+ * Flexible machine syntax
63
+
64
+ Examples of the usage patterns for some of the above features are shown below.
65
+ You can find much more detailed documentation in the actual API.
66
+
67
+ ## Usage
68
+
69
+ ### Example
70
+
71
+ Below is an example of many of the features offered by this plugin, including:
72
+
73
+ * Initial states
74
+ * Namespaced states
75
+ * Transition callbacks
76
+ * Conditional transitions
77
+ * State-driven instance behavior
78
+ * Customized state values
79
+ * Parallel events
80
+ * Path analysis
81
+
82
+ Class definition:
83
+
84
+ ```ruby
85
+ class Vehicle
86
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
87
+
88
+ state_machine :state, :initial => :parked do
89
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
90
+
91
+ after_transition :on => :crash, :do => :tow
92
+ after_transition :on => :repair, :do => :fix
93
+ after_transition any => :parked do |vehicle, transition|
94
+ vehicle.seatbelt_on = false
95
+ end
96
+
97
+ after_failure :on => :ignite, :do => :log_start_failure
98
+
99
+ around_transition do |vehicle, transition, block|
100
+ start = Time.now
101
+ block.call
102
+ vehicle.time_used += Time.now - start
103
+ end
104
+
105
+ event :park do
106
+ transition [:idling, :first_gear] => :parked
107
+ end
108
+
109
+ event :ignite do
110
+ transition :stalled => same, :parked => :idling
111
+ end
112
+
113
+ event :idle do
114
+ transition :first_gear => :idling
115
+ end
116
+
117
+ event :shift_up do
118
+ transition :idling => :first_gear, :first_gear => :second_gear, :second_gear => :third_gear
119
+ end
120
+
121
+ event :shift_down do
122
+ transition :third_gear => :second_gear, :second_gear => :first_gear
123
+ end
124
+
125
+ event :crash do
126
+ transition all - [:parked, :stalled] => :stalled, :if => lambda {|vehicle| !vehicle.passed_inspection?}
127
+ end
128
+
129
+ event :repair do
130
+ # The first transition that matches the state and passes its conditions
131
+ # will be used
132
+ transition :stalled => :parked, :unless => :auto_shop_busy
133
+ transition :stalled => same
134
+ end
135
+
136
+ state :parked do
137
+ def speed
138
+ 0
139
+ end
140
+ end
141
+
142
+ state :idling, :first_gear do
143
+ def speed
144
+ 10
145
+ end
146
+ end
147
+
148
+ state all - [:parked, :stalled, :idling] do
149
+ def moving?
150
+ true
151
+ end
152
+ end
153
+
154
+ state :parked, :stalled, :idling do
155
+ def moving?
156
+ false
157
+ end
158
+ end
159
+ end
160
+
161
+ state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
162
+ event :enable do
163
+ transition all => :active
164
+ end
165
+
166
+ event :disable do
167
+ transition all => :off
168
+ end
169
+
170
+ state :active, :value => 1
171
+ state :off, :value => 0
172
+ end
173
+
174
+ def initialize
175
+ @seatbelt_on = false
176
+ @time_used = 0
177
+ @auto_shop_busy = true
178
+ super() # NOTE: This *must* be called, otherwise states won't get initialized
179
+ end
180
+
181
+ def put_on_seatbelt
182
+ @seatbelt_on = true
183
+ end
184
+
185
+ def passed_inspection?
186
+ false
187
+ end
188
+
189
+ def tow
190
+ # tow the vehicle
191
+ end
192
+
193
+ def fix
194
+ # get the vehicle fixed by a mechanic
195
+ end
196
+
197
+ def log_start_failure
198
+ # log a failed attempt to start the vehicle
199
+ end
200
+ end
201
+ ```
202
+
203
+ **Note** the comment made on the `initialize` method in the class. In order for
204
+ state machine attributes to be properly initialized, `super()` must be called.
205
+ See `StateMachine::MacroMethods` for more information about this.
206
+
207
+ Using the above class as an example, you can interact with the state machine
208
+ like so:
209
+
210
+ ```ruby
211
+ vehicle = Vehicle.new # => #<Vehicle:0xb7cf4eac @state="parked", @seatbelt_on=false>
212
+ vehicle.state # => "parked"
213
+ vehicle.state_name # => :parked
214
+ vehicle.human_state_name # => "parked"
215
+ vehicle.parked? # => true
216
+ vehicle.can_ignite? # => true
217
+ vehicle.ignite_transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
218
+ vehicle.state_events # => [:ignite]
219
+ vehicle.state_transitions # => [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
220
+ vehicle.speed # => 0
221
+ vehicle.moving? # => false
222
+
223
+ vehicle.ignite # => true
224
+ vehicle.parked? # => false
225
+ vehicle.idling? # => true
226
+ vehicle.speed # => 10
227
+ vehicle # => #<Vehicle:0xb7cf4eac @state="idling", @seatbelt_on=true>
228
+
229
+ vehicle.shift_up # => true
230
+ vehicle.speed # => 10
231
+ vehicle.moving? # => true
232
+ vehicle # => #<Vehicle:0xb7cf4eac @state="first_gear", @seatbelt_on=true>
233
+
234
+ # A generic event helper is available to fire without going through the event's instance method
235
+ vehicle.fire_state_event(:shift_up) # => true
236
+
237
+ # Call state-driven behavior that's undefined for the state raises a NoMethodError
238
+ vehicle.speed # => NoMethodError: super: no superclass method `speed' for #<Vehicle:0xb7cf4eac>
239
+ vehicle # => #<Vehicle:0xb7cf4eac @state="second_gear", @seatbelt_on=true>
240
+
241
+ # The bang (!) operator can raise exceptions if the event fails
242
+ vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from :second_gear
243
+
244
+ # Generic state predicates can raise exceptions if the value does not exist
245
+ vehicle.state?(:parked) # => false
246
+ vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
247
+
248
+ # Namespaced machines have uniquely-generated methods
249
+ vehicle.alarm_state # => 1
250
+ vehicle.alarm_state_name # => :active
251
+
252
+ vehicle.can_disable_alarm? # => true
253
+ vehicle.disable_alarm # => true
254
+ vehicle.alarm_state # => 0
255
+ vehicle.alarm_state_name # => :off
256
+ vehicle.can_enable_alarm? # => true
257
+
258
+ vehicle.alarm_off? # => true
259
+ vehicle.alarm_active? # => false
260
+
261
+ # Events can be fired in parallel
262
+ vehicle.fire_events(:shift_down, :enable_alarm) # => true
263
+ vehicle.state_name # => :first_gear
264
+ vehicle.alarm_state_name # => :active
265
+
266
+ vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachine::InvalidTransition: Cannot run events in parallel: ignite, enable_alarm
267
+
268
+ # Human-friendly names can be accessed for states/events
269
+ Vehicle.human_state_name(:first_gear) # => "first gear"
270
+ Vehicle.human_alarm_state_name(:active) # => "active"
271
+
272
+ Vehicle.human_state_event_name(:shift_down) # => "shift down"
273
+ Vehicle.human_alarm_state_event_name(:enable) # => "enable"
274
+
275
+ # States / events can also be references by the string version of their name
276
+ Vehicle.human_state_name('first_gear') # => "first gear"
277
+ Vehicle.human_state_event_name('shift_down') # => "shift down"
278
+
279
+ # Available transition paths can be analyzed for an object
280
+ vehicle.state_paths # => [[#<StateMachine::Transition ...], [#<StateMachine::Transition ...], ...]
281
+ vehicle.state_paths.to_states # => [:parked, :idling, :first_gear, :stalled, :second_gear, :third_gear]
282
+ vehicle.state_paths.events # => [:park, :ignite, :shift_up, :idle, :crash, :repair, :shift_down]
283
+
284
+ # Find all paths that start and end on certain states
285
+ vehicle.state_paths(:from => :parked, :to => :first_gear) # => [[
286
+ # #<StateMachine::Transition attribute=:state event=:ignite from="parked" ...>,
287
+ # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" ...>
288
+ # ]]
289
+ # Skipping state_machine and writing to attributes directly
290
+ vehicle.state = "parked"
291
+ vehicle.state # => "parked"
292
+ vehicle.state_name # => :parked
293
+
294
+ # *Note* that the following is not supported (see StateMachine::MacroMethods#state_machine):
295
+ # vehicle.state = :parked
296
+ ```
297
+
298
+ ## Integrations
299
+
300
+ In addition to being able to define state machines on all Ruby classes, a set of
301
+ out-of-the-box integrations are available for some of the more popular Ruby
302
+ libraries. These integrations add library-specific behavior, allowing for state
303
+ machines to work more tightly with the conventions defined by those libraries.
304
+
305
+ The integrations currently available include:
306
+
307
+ * ActiveModel classes
308
+ * ActiveRecord models
309
+ * DataMapper resources
310
+ * Mongoid models
311
+ * MongoMapper models
312
+ * Sequel models
313
+
314
+ A brief overview of these integrations is described below.
315
+
316
+ ### ActiveModel
317
+
318
+ The ActiveModel integration is useful for both standalone usage and for providing
319
+ the base implementation for ORMs which implement the ActiveModel API. This
320
+ integration adds support for validation errors, dirty attribute tracking, and
321
+ observers. For example,
322
+
323
+ ```ruby
324
+ class Vehicle
325
+ include ActiveModel::Dirty
326
+ include ActiveModel::Validations
327
+ include ActiveModel::Observing
328
+
329
+ attr_accessor :state
330
+ define_attribute_methods [:state]
331
+
332
+ state_machine :initial => :parked do
333
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
334
+ after_transition any => :parked do |vehicle, transition|
335
+ vehicle.seatbelt = 'off'
336
+ end
337
+ around_transition :benchmark
338
+
339
+ event :ignite do
340
+ transition :parked => :idling
341
+ end
342
+
343
+ state :first_gear, :second_gear do
344
+ validates_presence_of :seatbelt_on
345
+ end
346
+ end
347
+
348
+ def put_on_seatbelt
349
+ ...
350
+ end
351
+
352
+ def benchmark
353
+ ...
354
+ yield
355
+ ...
356
+ end
357
+ end
358
+
359
+ class VehicleObserver < ActiveModel::Observer
360
+ # Callback for :ignite event *before* the transition is performed
361
+ def before_ignite(vehicle, transition)
362
+ # log message
363
+ end
364
+
365
+ # Generic transition callback *after* the transition is performed
366
+ def after_transition(vehicle, transition)
367
+ Audit.log(vehicle, transition)
368
+ end
369
+
370
+ # Generic callback after the transition fails to perform
371
+ def after_failure_to_transition(vehicle, transition)
372
+ Audit.error(vehicle, transition)
373
+ end
374
+ end
375
+ ```
376
+
377
+ For more information about the various behaviors added for ActiveModel state
378
+ machines and how to build new integrations that use ActiveModel, see
379
+ `StateMachine::Integrations::ActiveModel`.
380
+
381
+ ### ActiveRecord
382
+
383
+ The ActiveRecord integration adds support for database transactions, automatically
384
+ saving the record, named scopes, validation errors, and observers. For example,
385
+
386
+ ```ruby
387
+ class Vehicle < ActiveRecord::Base
388
+ state_machine :initial => :parked do
389
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
390
+ after_transition any => :parked do |vehicle, transition|
391
+ vehicle.seatbelt = 'off'
392
+ end
393
+ around_transition :benchmark
394
+
395
+ event :ignite do
396
+ transition :parked => :idling
397
+ end
398
+
399
+ state :first_gear, :second_gear do
400
+ validates_presence_of :seatbelt_on
401
+ end
402
+ end
403
+
404
+ def put_on_seatbelt
405
+ ...
406
+ end
407
+
408
+ def benchmark
409
+ ...
410
+ yield
411
+ ...
412
+ end
413
+ end
414
+
415
+ class VehicleObserver < ActiveRecord::Observer
416
+ # Callback for :ignite event *before* the transition is performed
417
+ def before_ignite(vehicle, transition)
418
+ # log message
419
+ end
420
+
421
+ # Generic transition callback *after* the transition is performed
422
+ def after_transition(vehicle, transition)
423
+ Audit.log(vehicle, transition)
424
+ end
425
+ end
426
+ ```
427
+
428
+ For more information about the various behaviors added for ActiveRecord state
429
+ machines, see `StateMachine::Integrations::ActiveRecord`.
430
+
431
+ ### DataMapper
432
+
433
+ Like the ActiveRecord integration, the DataMapper integration adds support for
434
+ database transactions, automatically saving the record, named scopes, Extlib-like
435
+ callbacks, validation errors, and observers. For example,
436
+
437
+ ```ruby
438
+ class Vehicle
439
+ include DataMapper::Resource
440
+
441
+ property :id, Serial
442
+ property :state, String
443
+
444
+ state_machine :initial => :parked do
445
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
446
+ after_transition any => :parked do |transition|
447
+ self.seatbelt = 'off' # self is the record
448
+ end
449
+ around_transition :benchmark
450
+
451
+ event :ignite do
452
+ transition :parked => :idling
453
+ end
454
+
455
+ state :first_gear, :second_gear do
456
+ validates_presence_of :seatbelt_on
457
+ end
458
+ end
459
+
460
+ def put_on_seatbelt
461
+ ...
462
+ end
463
+
464
+ def benchmark
465
+ ...
466
+ yield
467
+ ...
468
+ end
469
+ end
470
+
471
+ class VehicleObserver
472
+ include DataMapper::Observer
473
+
474
+ observe Vehicle
475
+
476
+ # Callback for :ignite event *before* the transition is performed
477
+ before_transition :on => :ignite do |transition|
478
+ # log message (self is the record)
479
+ end
480
+
481
+ # Generic transition callback *after* the transition is performed
482
+ after_transition do |transition|
483
+ Audit.log(self, transition) # self is the record
484
+ end
485
+
486
+ around_transition do |transition, block|
487
+ # mark start time
488
+ block.call
489
+ # mark stop time
490
+ end
491
+
492
+ # Generic callback after the transition fails to perform
493
+ after_transition_failure do |transition|
494
+ Audit.log(self, transition) # self is the record
495
+ end
496
+ end
497
+ ```
498
+
499
+ **Note** that the DataMapper::Observer integration is optional and only available
500
+ when the dm-observer library is installed.
501
+
502
+ For more information about the various behaviors added for DataMapper state
503
+ machines, see `StateMachine::Integrations::DataMapper`.
504
+
505
+ ### Mongoid
506
+
507
+ The Mongoid integration adds support for automatically saving the record,
508
+ basic scopes, validation errors, and observers. For example,
509
+
510
+ ```ruby
511
+ class Vehicle
512
+ include Mongoid::Document
513
+
514
+ state_machine :initial => :parked do
515
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
516
+ after_transition any => :parked do |vehicle, transition|
517
+ vehicle.seatbelt = 'off' # self is the record
518
+ end
519
+ around_transition :benchmark
520
+
521
+ event :ignite do
522
+ transition :parked => :idling
523
+ end
524
+
525
+ state :first_gear, :second_gear do
526
+ validates_presence_of :seatbelt_on
527
+ end
528
+ end
529
+
530
+ def put_on_seatbelt
531
+ ...
532
+ end
533
+
534
+ def benchmark
535
+ ...
536
+ yield
537
+ ...
538
+ end
539
+ end
540
+
541
+ class VehicleObserver < Mongoid::Observer
542
+ # Callback for :ignite event *before* the transition is performed
543
+ def before_ignite(vehicle, transition)
544
+ # log message
545
+ end
546
+
547
+ # Generic transition callback *after* the transition is performed
548
+ def after_transition(vehicle, transition)
549
+ Audit.log(vehicle, transition)
550
+ end
551
+ end
552
+ ```
553
+
554
+ For more information about the various behaviors added for Mongoid state
555
+ machines, see `StateMachine::Integrations::Mongoid`.
556
+
557
+ ### MongoMapper
558
+
559
+ The MongoMapper integration adds support for automatically saving the record,
560
+ basic scopes, validation errors and callbacks. For example,
561
+
562
+ ```ruby
563
+ class Vehicle
564
+ include MongoMapper::Document
565
+
566
+ state_machine :initial => :parked do
567
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
568
+ after_transition any => :parked do |vehicle, transition|
569
+ vehicle.seatbelt = 'off' # self is the record
570
+ end
571
+ around_transition :benchmark
572
+
573
+ event :ignite do
574
+ transition :parked => :idling
575
+ end
576
+
577
+ state :first_gear, :second_gear do
578
+ validates_presence_of :seatbelt_on
579
+ end
580
+ end
581
+
582
+ def put_on_seatbelt
583
+ ...
584
+ end
585
+
586
+ def benchmark
587
+ ...
588
+ yield
589
+ ...
590
+ end
591
+ end
592
+ ```
593
+
594
+ For more information about the various behaviors added for MongoMapper state
595
+ machines, see `StateMachine::Integrations::MongoMapper`.
596
+
597
+ ### Sequel
598
+
599
+ Like the ActiveRecord integration, the Sequel integration adds support for
600
+ database transactions, automatically saving the record, named scopes, validation
601
+ errors and callbacks. For example,
602
+
603
+ ```ruby
604
+ class Vehicle < Sequel::Model
605
+ plugin :validation_class_methods
606
+
607
+ state_machine :initial => :parked do
608
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
609
+ after_transition any => :parked do |transition|
610
+ self.seatbelt = 'off' # self is the record
611
+ end
612
+ around_transition :benchmark
613
+
614
+ event :ignite do
615
+ transition :parked => :idling
616
+ end
617
+
618
+ state :first_gear, :second_gear do
619
+ validates_presence_of :seatbelt_on
620
+ end
621
+ end
622
+
623
+ def put_on_seatbelt
624
+ ...
625
+ end
626
+
627
+ def benchmark
628
+ ...
629
+ yield
630
+ ...
631
+ end
632
+ end
633
+ ```
634
+
635
+ For more information about the various behaviors added for Sequel state
636
+ machines, see `StateMachine::Integrations::Sequel`.
637
+
638
+ ## Additional Topics
639
+
640
+ ### Explicit vs. Implicit Event Transitions
641
+
642
+ Every event defined for a state machine generates an instance method on the
643
+ class that allows the event to be explicitly triggered. Most of the examples in
644
+ the state_machine documentation use this technique. However, with some types of
645
+ integrations, like ActiveRecord, you can also *implicitly* fire events by
646
+ setting a special attribute on the instance.
647
+
648
+ Suppose you're using the ActiveRecord integration and the following model is
649
+ defined:
650
+
651
+ ```ruby
652
+ class Vehicle < ActiveRecord::Base
653
+ state_machine :initial => :parked do
654
+ event :ignite do
655
+ transition :parked => :idling
656
+ end
657
+ end
658
+ end
659
+ ```
660
+
661
+ To trigger the `ignite` event, you would typically call the `Vehicle#ignite`
662
+ method like so:
663
+
664
+ ```ruby
665
+ vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
666
+ vehicle.ignite # => true
667
+ vehicle.state # => "idling"
668
+ ```
669
+
670
+ This is referred to as an *explicit* event transition. The same behavior can
671
+ also be achieved *implicitly* by setting the state event attribute and invoking
672
+ the action associated with the state machine. For example:
673
+
674
+ ```ruby
675
+ vehicle = Vehicle.create # => #<Vehicle id=1 state="parked">
676
+ vehicle.state_event = "ignite" # => "ignite"
677
+ vehicle.save # => true
678
+ vehicle.state # => "idling"
679
+ vehicle.state_event # => nil
680
+ ```
681
+
682
+ As you can see, the `ignite` event was automatically triggered when the `save`
683
+ action was called. This is particularly useful if you want to allow users to
684
+ drive the state transitions from a web API.
685
+
686
+ See each integration's API documentation for more information on the implicit
687
+ approach.
688
+
689
+ ### Symbols vs. Strings
690
+
691
+ In all of the examples used throughout the documentation, you'll notice that
692
+ states and events are almost always referenced as symbols. This isn't a
693
+ requirement, but rather a suggested best practice.
694
+
695
+ You can very well define your state machine with Strings like so:
696
+
697
+ ```ruby
698
+ class Vehicle
699
+ state_machine :initial => 'parked' do
700
+ event 'ignite' do
701
+ transition 'parked' => 'idling'
702
+ end
703
+
704
+ # ...
705
+ end
706
+ end
707
+ ```
708
+
709
+ You could even use numbers as your state / event names. The **important** thing
710
+ to keep in mind is that the type being used for referencing states / events in
711
+ your machine definition must be **consistent**. If you're using Symbols, then
712
+ all states / events must use Symbols. Otherwise you'll encounter the following
713
+ error:
714
+
715
+ ```ruby
716
+ class Vehicle
717
+ state_machine do
718
+ event :ignite do
719
+ transition :parked => 'idling'
720
+ end
721
+ end
722
+ end
723
+
724
+ # => ArgumentError: "idling" state defined as String, :parked defined as Symbol; all states must be consistent
725
+ ```
726
+
727
+ There **is** an exception to this rule. The consistency is only required within
728
+ the definition itself. However, when the machine's helper methods are called
729
+ with input from external sources, such as a web form, state_machine will map
730
+ that input to a String / Symbol. For example:
731
+
732
+ ```ruby
733
+ class Vehicle
734
+ state_machine :initial => :parked do
735
+ event :ignite do
736
+ transition :parked => :idling
737
+ end
738
+ end
739
+ end
740
+
741
+ v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state="parked">
742
+ v.state?('parked') # => true
743
+ v.state?(:parked) # => true
744
+ ```
745
+
746
+ **Note** that none of this actually has to do with the type of the value that
747
+ gets stored. By default, all state values are assumed to be string -- regardless
748
+ of whether the state names are symbols or strings. If you want to store states
749
+ as symbols instead you'll have to be explicit about it:
750
+
751
+ ```ruby
752
+ class Vehicle
753
+ state_machine :initial => :parked do
754
+ event :ignite do
755
+ transition :parked => :idling
756
+ end
757
+
758
+ states.each do |state|
759
+ self.state(state.name, :value => state.name.to_sym)
760
+ end
761
+ end
762
+ end
763
+
764
+ v = Vehicle.new # => #<Vehicle:0xb71da5f8 @state=:parked>
765
+ v.state?('parked') # => true
766
+ v.state?(:parked) # => true
767
+ ```
768
+
769
+ ### Syntax flexibility
770
+
771
+ Although state_machine introduces a simplified syntax, it still remains
772
+ backwards compatible with previous versions and other state-related libraries by
773
+ providing some flexibility around how transitions are defined. See below for an
774
+ overview of these syntaxes.
775
+
776
+ #### Verbose syntax
777
+
778
+ In general, it's recommended that state machines use the implicit syntax for
779
+ transitions. However, you can be a little more explicit and verbose about
780
+ transitions by using the `:from`, `:except_from`, `:to`,
781
+ and `:except_to` options.
782
+
783
+ For example, transitions and callbacks can be defined like so:
784
+
785
+ ```ruby
786
+ class Vehicle
787
+ state_machine :initial => :parked do
788
+ before_transition :from => :parked, :except_to => :parked, :do => :put_on_seatbelt
789
+ after_transition :to => :parked do |transition|
790
+ self.seatbelt = 'off' # self is the record
791
+ end
792
+
793
+ event :ignite do
794
+ transition :from => :parked, :to => :idling
795
+ end
796
+ end
797
+ end
798
+ ```
799
+
800
+ #### Transition context
801
+
802
+ Some flexibility is provided around the context in which transitions can be
803
+ defined. In almost all examples throughout the documentation, transitions are
804
+ defined within the context of an event. If you prefer to have state machines
805
+ defined in the context of a **state** either out of preference or in order to
806
+ easily migrate from a different library, you can do so as shown below:
807
+
808
+ ```ruby
809
+ class Vehicle
810
+ state_machine :initial => :parked do
811
+ ...
812
+
813
+ state :parked do
814
+ transition :to => :idling, :on => [:ignite, :shift_up], :if => :seatbelt_on?
815
+
816
+ def speed
817
+ 0
818
+ end
819
+ end
820
+
821
+ state :first_gear do
822
+ transition :to => :second_gear, :on => :shift_up
823
+
824
+ def speed
825
+ 10
826
+ end
827
+ end
828
+
829
+ state :idling, :first_gear do
830
+ transition :to => :parked, :on => :park
831
+ end
832
+ end
833
+ end
834
+ ```
835
+
836
+ In the above example, there's no need to specify the `from` state for each
837
+ transition since it's inferred from the context.
838
+
839
+ You can also define transitions completely outside the context of a particular
840
+ state / event. This may be useful in cases where you're building a state
841
+ machine from a data store instead of part of the class definition. See the
842
+ example below:
843
+
844
+ ```ruby
845
+ class Vehicle
846
+ state_machine :initial => :parked do
847
+ ...
848
+
849
+ transition :parked => :idling, :on => [:ignite, :shift_up]
850
+ transition :first_gear => :second_gear, :second_gear => :third_gear, :on => :shift_up
851
+ transition [:idling, :first_gear] => :parked, :on => :park
852
+ transition [:idling, :first_gear] => :parked, :on => :park
853
+ transition all - [:parked, :stalled] => :stalled, :unless => :auto_shop_busy?
854
+ end
855
+ end
856
+ ```
857
+
858
+ Notice that in these alternative syntaxes:
859
+
860
+ * You can continue to configure `:if` and `:unless` conditions
861
+ * You can continue to define `from` states (when in the machine context) using
862
+ the `all`, `any`, and `same` helper methods
863
+
864
+ ### Static / Dynamic definitions
865
+
866
+ In most cases, the definition of a state machine is **static**. That is to say,
867
+ the states, events and possible transitions are known ahead of time even though
868
+ they may depend on data that's only known at runtime. For example, certain
869
+ transitions may only be available depending on an attribute on that object it's
870
+ being run on. All of the documentation in this library define static machines
871
+ like so:
872
+
873
+ ```ruby
874
+ class Vehicle
875
+ state_machine :state, :initial => :parked do
876
+ event :park do
877
+ transition [:idling, :first_gear] => :parked
878
+ end
879
+
880
+ ...
881
+ end
882
+ end
883
+ ```
884
+
885
+ However, there may be cases where the definition of a state machine is **dynamic**.
886
+ This means that you don't know the possible states or events for a machine until
887
+ runtime. For example, you may allow users in your application to manage the
888
+ state machine of a project or task in your system. This means that the list of
889
+ transitions (and their associated states / events) could be stored externally,
890
+ such as in a database. In a case like this, you can define dynamically-generated
891
+ state machines like so:
892
+
893
+ ```ruby
894
+ class Vehicle
895
+ attr_accessor :state
896
+
897
+ # Make sure the machine gets initialized so the initial state gets set properly
898
+ def initialize(*)
899
+ super
900
+ machine
901
+ end
902
+
903
+ # Replace this with an external source (like a db)
904
+ def transitions
905
+ [
906
+ {:parked => :idling, :on => :ignite},
907
+ {:idling => :first_gear, :first_gear => :second_gear, :on => :shift_up}
908
+ # ...
909
+ ]
910
+ end
911
+
912
+ # Create a state machine for this vehicle instance dynamically based on the
913
+ # transitions defined from the source above
914
+ def machine
915
+ vehicle = self
916
+ @machine ||= Machine.new(vehicle, :initial => :parked, :action => :save) do
917
+ vehicle.transitions.each {|attrs| transition(attrs)}
918
+ end
919
+ end
920
+
921
+ def save
922
+ # Save the state change...
923
+ true
924
+ end
925
+ end
926
+
927
+ # Generic class for building machines
928
+ class Machine
929
+ def self.new(object, *args, &block)
930
+ machine_class = Class.new
931
+ machine = machine_class.state_machine(*args, &block)
932
+ attribute = machine.attribute
933
+ action = machine.action
934
+
935
+ # Delegate attributes
936
+ machine_class.class_eval do
937
+ define_method(:definition) { machine }
938
+ define_method(attribute) { object.send(attribute) }
939
+ define_method("#{attribute}=") {|value| object.send("#{attribute}=", value) }
940
+ define_method(action) { object.send(action) } if action
941
+ end
942
+
943
+ machine_class.new
944
+ end
945
+ end
946
+
947
+ vehicle = Vehicle.new # => #<Vehicle:0xb708412c @state="parked" ...>
948
+ vehicle.state # => "parked"
949
+ vehicle.machine.ignite # => true
950
+ vehicle.machine.state # => "idling
951
+ vehicle.state # => "idling"
952
+ vehicle.machine.state_transitions # => [#<StateMachine::Transition ...>]
953
+ vehicle.machine.definition.states.keys # => :first_gear, :second_gear, :parked, :idling
954
+ ```
955
+
956
+ As you can see, state_machine provides enough flexibility for you to be able
957
+ to create new machine definitions on the fly based on an external source of
958
+ transitions.
959
+
960
+ ### Core Extensions
961
+
962
+ By default, state_machine extends the Ruby core with a `state_machine` method on
963
+ `Class`. All other parts of the library are confined within the `StateMachine`
964
+ namespace. While this isn't wholly necessary, it also doesn't have any performance
965
+ impact and makes it truly feel like an extension to the language. This is very
966
+ similar to the way that you'll find `yaml`, `json`, or other libraries adding a
967
+ simple method to all objects just by loading the library.
968
+
969
+ However, if you'd like to avoid having state_machine add this extension to the
970
+ Ruby core, you can do so like so:
971
+
972
+ ```ruby
973
+ require 'state_machine/core'
974
+
975
+ class Vehicle
976
+ extend StateMachine::MacroMethods
977
+
978
+ state_machine do
979
+ # ...
980
+ end
981
+ end
982
+ ```
983
+
984
+ If you're using a gem loader like Bundler, you can explicitly indicate which
985
+ file to load:
986
+
987
+ ```ruby
988
+ # In Gemfile
989
+ ...
990
+ gem 'state_machine', :require => 'state_machine/core'
991
+ ```
992
+
993
+ ## Tools
994
+
995
+ ### Generating graphs
996
+
997
+ This library comes with built-in support for generating di-graphs based on the
998
+ events, states, and transitions defined for a state machine using [GraphViz](http://www.graphviz.org).
999
+ This requires that both the `ruby-graphviz` gem and graphviz library be
1000
+ installed on the system.
1001
+
1002
+ #### Examples
1003
+
1004
+ To generate a graph for a specific file / class:
1005
+
1006
+ ```bash
1007
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle
1008
+ ```
1009
+
1010
+ To save files to a specific path:
1011
+
1012
+ ```bash
1013
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle TARGET=files
1014
+ ```
1015
+
1016
+ To customize the image format / orientation:
1017
+
1018
+ ```bash
1019
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle FORMAT=jpg ORIENTATION=landscape
1020
+ ```
1021
+
1022
+ See http://rdoc.info/github/glejeune/Ruby-Graphviz/Constants for the list of
1023
+ supported image formats. If resolution is an issue, the svg format may offer
1024
+ better results.
1025
+
1026
+ To generate multiple state machine graphs:
1027
+
1028
+ ```bash
1029
+ rake state_machine:draw FILE=vehicle.rb,car.rb CLASS=Vehicle,Car
1030
+ ```
1031
+
1032
+ To use human state / event names:
1033
+
1034
+ ```bash
1035
+ rake state_machine:draw FILE=vehicle.rb CLASS=Vehicle HUMAN_NAMES=true
1036
+ ```
1037
+
1038
+ **Note** that this will generate a different file for every state machine defined
1039
+ in the class. The generated files will use an output filename of the format
1040
+ `#{class_name}_#{machine_name}.#{format}`.
1041
+
1042
+ For examples of actual images generated using this task, see those under the
1043
+ examples folder.
1044
+
1045
+ ### Interactive graphs
1046
+
1047
+ Jean Bovet's [Visual Automata Simulator](http://www.cs.usfca.edu/~jbovet/vas.html)
1048
+ is a great tool for "simulating, visualizing and transforming finite state
1049
+ automata and Turing Machines". It can help in the creation of states and events
1050
+ for your models. It is cross-platform, written in Java.
1051
+
1052
+ ### Generating documentation
1053
+
1054
+ If you use YARD to generate documentation for your projects, state_machine can
1055
+ be enabled to generate API docs for auto-generated methods from each state machine
1056
+ definition as well as providing embedded visualizations.
1057
+
1058
+ See the generated API documentation under the examples folder to see what the
1059
+ output looks like.
1060
+
1061
+ To enable the YARD integration, you'll need to add state_machine to the list of
1062
+ YARD's plugins by editing the global YARD config:
1063
+
1064
+ ~/.yard/config:
1065
+
1066
+ ```yaml
1067
+ load_plugins: true
1068
+ autoload_plugins:
1069
+ - state_machine
1070
+ ```
1071
+
1072
+ Once enabled, simply generate your documentation like you normally do.
1073
+
1074
+ *Note* that this only works for Ruby 1.9+.
1075
+
1076
+ ## Web Frameworks
1077
+
1078
+ ### Ruby on Rails
1079
+
1080
+ Integrating state_machine into your Ruby on Rails application is straightforward
1081
+ and provides a few additional features specific to the framework. To get
1082
+ started, following the steps below.
1083
+
1084
+ #### 1. Install the gem
1085
+
1086
+ If using Rails 2.x:
1087
+
1088
+ ```ruby
1089
+ # In config/environment.rb
1090
+ ...
1091
+ Rails::Initializer.run do |config|
1092
+ ...
1093
+ config.gem 'state_machine', :version => '~> 1.0'
1094
+ ...
1095
+ end
1096
+ ```
1097
+
1098
+ If using Rails 3.x or up:
1099
+
1100
+ ```ruby
1101
+ # In Gemfile
1102
+ ...
1103
+ gem 'state_machine'
1104
+ gem 'ruby-graphviz', :require => 'graphviz' # Optional: only required for graphing
1105
+ ```
1106
+
1107
+ As usual, run `bundle install` to load the gems.
1108
+
1109
+ #### 2. Create a model
1110
+
1111
+ Create a model with a field to store the state, along with other any other
1112
+ fields your application requires:
1113
+
1114
+ ```bash
1115
+ $ rails generate model Vehicle state:string
1116
+ $ rake db:migrate
1117
+ ```
1118
+
1119
+ #### 3. Configure the state machine
1120
+
1121
+ Add the state machine to your model. Following the examples above,
1122
+ *app/models/vehicle.rb* might become:
1123
+
1124
+ ```ruby
1125
+ class Vehicle < ActiveRecord::Base
1126
+ state_machine :initial => :parked do
1127
+ before_transition :parked => any - :parked, :do => :put_on_seatbelt
1128
+ ...
1129
+ end
1130
+ end
1131
+ ```
1132
+
1133
+ #### Rake tasks
1134
+
1135
+ There is a special integration Rake task for generating state machines for
1136
+ classes used in a Ruby on Rails application. This task will load the application
1137
+ environment, meaning that it's unnecessary to specify the actual file to load.
1138
+
1139
+ For example,
1140
+
1141
+ ```bash
1142
+ rake state_machine:draw CLASS=Vehicle
1143
+ ```
1144
+
1145
+ If you are using this library as a gem in Rails 2.x, the following must be added
1146
+ to the end of your application's Rakefile in order for the above task to work:
1147
+
1148
+ ```ruby
1149
+ require 'tasks/state_machine'
1150
+ ```
1151
+
1152
+ ### Merb
1153
+
1154
+ #### Rake tasks
1155
+
1156
+ Like Ruby on Rails, there is a special integration Rake task for generating
1157
+ state machines for classes used in a Merb application. This task will load the
1158
+ application environment, meaning that it's unnecessary to specify the actual
1159
+ files to load.
1160
+
1161
+ For example,
1162
+
1163
+ ```bash
1164
+ rake state_machine:draw CLASS=Vehicle
1165
+ ```
1166
+
1167
+ ## Testing
1168
+
1169
+ To run the core test suite (does **not** test any of the integrations):
1170
+
1171
+ ```bash
1172
+ bundle install
1173
+ bundle exec rake test
1174
+ ```
1175
+
1176
+ To run integration tests:
1177
+
1178
+ ```bash
1179
+ bundle install
1180
+ rake appraisal:install
1181
+ rake appraisal:test
1182
+ ```
1183
+
1184
+ You can also test a specific version:
1185
+
1186
+ ```bash
1187
+ rake appraisal:active_model-3.0.0 test
1188
+ rake appraisal:active_record-2.0.0 test
1189
+ rake appraisal:data_mapper-0.9.4 test
1190
+ rake appraisal:mongoid-2.0.0 test
1191
+ rake appraisal:mongo_mapper-0.5.5 test
1192
+ rake appraisal:sequel-2.8.0 test
1193
+ ```
1194
+
1195
+ ## Caveats
1196
+
1197
+ The following caveats should be noted when using state_machine:
1198
+
1199
+ * Overridden event methods won't get invoked when using attribute-based event transitions
1200
+ * **DataMapper**: Attribute-based event transitions are disabled when using dm-validations 0.9.4 - 0.9.6
1201
+ * **DataMapper**: Transitions cannot persist states when run from after :create / :save callbacks
1202
+ * **JRuby / Rubinius**: around_transition callbacks in ORM integrations won't work on JRuby since it doesn't support continuations
1203
+ * **Factory Girl**: Dynamic initial states don't work because of the way factory_girl
1204
+ builds objects. You can work around this in a few ways:
1205
+ 1. Use a default state that is common across all objects and rely on events to
1206
+ determine the actual initial state for your object.
1207
+ 2. Assuming you're not using state-driven behavior on initialization, you can
1208
+ re-initialize states after the fact:
1209
+
1210
+ ```ruby
1211
+ # Re-initialize in FactoryGirl
1212
+ FactoryGirl.define do
1213
+ factory :vehicle do
1214
+ after_build {|user| user.send(:initialize_state_machines, :dynamic => :force)}
1215
+ end
1216
+ end
1217
+
1218
+ # Alternatively re-initialize in your model
1219
+ class Vehicle < ActiveRecord::Base
1220
+ ...
1221
+ before_validation :on => :create {|user| user.send(:initialize_state_machines, :dynamic => :force)}
1222
+ end
1223
+ ```
1224
+
1225
+ ## Dependencies
1226
+
1227
+ Ruby versions officially supported and tested:
1228
+
1229
+ * Ruby (MRI) 1.8.6+
1230
+ * JRuby (1.8, 1.9)
1231
+ * Rubinius (1.8, 1.9)
1232
+
1233
+ ORM versions officially supported and tested:
1234
+
1235
+ * [ActiveModel](http://rubyonrails.org) integration: 3.0.0 or later
1236
+ * [ActiveRecord](http://rubyonrails.org) integration: 2.0.0 or later
1237
+ * [DataMapper](http://datamapper.org) integration: 0.9.4 or later
1238
+ * [Mongoid](http://mongoid.org) integration: 2.0.0 or later
1239
+ * [MongoMapper](http://mongomapper.com) integration: 0.5.5 or later
1240
+ * [Sequel](http://sequel.rubyforge.org) integration: 2.8.0 or later
1241
+
1242
+ If graphing state machine:
1243
+
1244
+ * [ruby-graphviz](http://github.com/glejeune/Ruby-Graphviz): 0.9.17 or later