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