glebtv_state_machine 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (310) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +2 -0
  5. data/.travis.yml +72 -0
  6. data/.yardopts +5 -0
  7. data/Appraisals +491 -0
  8. data/CHANGELOG.md +502 -0
  9. data/Gemfile +3 -0
  10. data/LICENSE +20 -0
  11. data/README.md +1244 -0
  12. data/Rakefile +42 -0
  13. data/examples/AutoShop_state.png +0 -0
  14. data/examples/Car_state.png +0 -0
  15. data/examples/Gemfile +5 -0
  16. data/examples/Gemfile.lock +14 -0
  17. data/examples/TrafficLight_state.png +0 -0
  18. data/examples/Vehicle_state.png +0 -0
  19. data/examples/auto_shop.rb +13 -0
  20. data/examples/car.rb +21 -0
  21. data/examples/doc/AutoShop.html +2856 -0
  22. data/examples/doc/AutoShop_state.png +0 -0
  23. data/examples/doc/Car.html +919 -0
  24. data/examples/doc/Car_state.png +0 -0
  25. data/examples/doc/TrafficLight.html +2230 -0
  26. data/examples/doc/TrafficLight_state.png +0 -0
  27. data/examples/doc/Vehicle.html +7921 -0
  28. data/examples/doc/Vehicle_state.png +0 -0
  29. data/examples/doc/_index.html +136 -0
  30. data/examples/doc/class_list.html +47 -0
  31. data/examples/doc/css/common.css +1 -0
  32. data/examples/doc/css/full_list.css +55 -0
  33. data/examples/doc/css/style.css +322 -0
  34. data/examples/doc/file_list.html +46 -0
  35. data/examples/doc/frames.html +13 -0
  36. data/examples/doc/index.html +136 -0
  37. data/examples/doc/js/app.js +205 -0
  38. data/examples/doc/js/full_list.js +173 -0
  39. data/examples/doc/js/jquery.js +16 -0
  40. data/examples/doc/method_list.html +734 -0
  41. data/examples/doc/top-level-namespace.html +105 -0
  42. data/examples/merb-rest/controller.rb +51 -0
  43. data/examples/merb-rest/model.rb +28 -0
  44. data/examples/merb-rest/view_edit.html.erb +24 -0
  45. data/examples/merb-rest/view_index.html.erb +23 -0
  46. data/examples/merb-rest/view_new.html.erb +13 -0
  47. data/examples/merb-rest/view_show.html.erb +17 -0
  48. data/examples/rails-rest/controller.rb +43 -0
  49. data/examples/rails-rest/migration.rb +7 -0
  50. data/examples/rails-rest/model.rb +23 -0
  51. data/examples/rails-rest/view__form.html.erb +34 -0
  52. data/examples/rails-rest/view_edit.html.erb +6 -0
  53. data/examples/rails-rest/view_index.html.erb +25 -0
  54. data/examples/rails-rest/view_new.html.erb +5 -0
  55. data/examples/rails-rest/view_show.html.erb +19 -0
  56. data/examples/traffic_light.rb +9 -0
  57. data/examples/vehicle.rb +33 -0
  58. data/gemfiles/active_model_3.0.0.gemfile +7 -0
  59. data/gemfiles/active_model_3.0.0.gemfile.lock +35 -0
  60. data/gemfiles/active_model_3.0.5.gemfile +7 -0
  61. data/gemfiles/active_model_3.0.5.gemfile.lock +35 -0
  62. data/gemfiles/active_model_3.1.1.gemfile +7 -0
  63. data/gemfiles/active_model_3.1.1.gemfile.lock +36 -0
  64. data/gemfiles/active_model_3.2.1.gemfile +7 -0
  65. data/gemfiles/active_model_3.2.12.gemfile +7 -0
  66. data/gemfiles/active_model_3.2.12.gemfile.lock +36 -0
  67. data/gemfiles/active_model_3.2.13.rc1.gemfile +7 -0
  68. data/gemfiles/active_model_3.2.13.rc1.gemfile.lock +36 -0
  69. data/gemfiles/active_model_4.0.0.gemfile +9 -0
  70. data/gemfiles/active_model_4.0.0.gemfile.lock +78 -0
  71. data/gemfiles/active_record_2.0.0.gemfile +9 -0
  72. data/gemfiles/active_record_2.0.0.gemfile.lock +39 -0
  73. data/gemfiles/active_record_2.0.5.gemfile +9 -0
  74. data/gemfiles/active_record_2.0.5.gemfile.lock +39 -0
  75. data/gemfiles/active_record_2.1.0.gemfile +9 -0
  76. data/gemfiles/active_record_2.1.0.gemfile.lock +39 -0
  77. data/gemfiles/active_record_2.1.2.gemfile +9 -0
  78. data/gemfiles/active_record_2.1.2.gemfile.lock +39 -0
  79. data/gemfiles/active_record_2.2.3.gemfile +9 -0
  80. data/gemfiles/active_record_2.2.3.gemfile.lock +39 -0
  81. data/gemfiles/active_record_2.3.12.gemfile +9 -0
  82. data/gemfiles/active_record_2.3.12.gemfile.lock +39 -0
  83. data/gemfiles/active_record_2.3.5.gemfile +9 -0
  84. data/gemfiles/active_record_2.3.5.gemfile.lock +39 -0
  85. data/gemfiles/active_record_3.0.0.gemfile +9 -0
  86. data/gemfiles/active_record_3.0.0.gemfile.lock +51 -0
  87. data/gemfiles/active_record_3.0.5.gemfile +9 -0
  88. data/gemfiles/active_record_3.0.5.gemfile.lock +50 -0
  89. data/gemfiles/active_record_3.1.1.gemfile +9 -0
  90. data/gemfiles/active_record_3.1.1.gemfile.lock +51 -0
  91. data/gemfiles/active_record_3.2.12.gemfile +9 -0
  92. data/gemfiles/active_record_3.2.12.gemfile.lock +51 -0
  93. data/gemfiles/active_record_3.2.13.rc1.gemfile +9 -0
  94. data/gemfiles/active_record_3.2.13.rc1.gemfile.lock +51 -0
  95. data/gemfiles/active_record_4.0.0.gemfile +11 -0
  96. data/gemfiles/active_record_4.0.0.gemfile.lock +83 -0
  97. data/gemfiles/data_mapper_0.10.2.gemfile +13 -0
  98. data/gemfiles/data_mapper_0.10.2.gemfile.lock +56 -0
  99. data/gemfiles/data_mapper_0.9.11.gemfile +13 -0
  100. data/gemfiles/data_mapper_0.9.11.gemfile.lock +71 -0
  101. data/gemfiles/data_mapper_0.9.4.gemfile +12 -0
  102. data/gemfiles/data_mapper_0.9.4.gemfile.lock +70 -0
  103. data/gemfiles/data_mapper_0.9.7.gemfile +13 -0
  104. data/gemfiles/data_mapper_0.9.7.gemfile.lock +67 -0
  105. data/gemfiles/data_mapper_1.0.0.gemfile +12 -0
  106. data/gemfiles/data_mapper_1.0.0.gemfile.lock +63 -0
  107. data/gemfiles/data_mapper_1.0.1.gemfile +12 -0
  108. data/gemfiles/data_mapper_1.0.1.gemfile.lock +63 -0
  109. data/gemfiles/data_mapper_1.0.2.gemfile +12 -0
  110. data/gemfiles/data_mapper_1.0.2.gemfile.lock +63 -0
  111. data/gemfiles/data_mapper_1.1.0.gemfile +12 -0
  112. data/gemfiles/data_mapper_1.1.0.gemfile.lock +61 -0
  113. data/gemfiles/data_mapper_1.2.0.gemfile +12 -0
  114. data/gemfiles/data_mapper_1.2.0.gemfile.lock +61 -0
  115. data/gemfiles/default.gemfile +7 -0
  116. data/gemfiles/default.gemfile.lock +27 -0
  117. data/gemfiles/graphviz_0.9.17.gemfile +7 -0
  118. data/gemfiles/graphviz_0.9.17.gemfile.lock +29 -0
  119. data/gemfiles/graphviz_0.9.21.gemfile +7 -0
  120. data/gemfiles/graphviz_0.9.21.gemfile.lock +29 -0
  121. data/gemfiles/graphviz_1.0.0.gemfile +7 -0
  122. data/gemfiles/graphviz_1.0.0.gemfile.lock +29 -0
  123. data/gemfiles/graphviz_1.0.3.gemfile +7 -0
  124. data/gemfiles/graphviz_1.0.3.gemfile.lock +29 -0
  125. data/gemfiles/graphviz_1.0.8.gemfile +7 -0
  126. data/gemfiles/graphviz_1.0.8.gemfile.lock +29 -0
  127. data/gemfiles/mongo_mapper_0.10.0.gemfile +8 -0
  128. data/gemfiles/mongo_mapper_0.10.0.gemfile.lock +47 -0
  129. data/gemfiles/mongo_mapper_0.11.2.gemfile +9 -0
  130. data/gemfiles/mongo_mapper_0.11.2.gemfile.lock +48 -0
  131. data/gemfiles/mongo_mapper_0.12.0.gemfile +9 -0
  132. data/gemfiles/mongo_mapper_0.12.0.gemfile.lock +48 -0
  133. data/gemfiles/mongo_mapper_0.5.5.gemfile +8 -0
  134. data/gemfiles/mongo_mapper_0.5.5.gemfile.lock +36 -0
  135. data/gemfiles/mongo_mapper_0.5.8.gemfile +8 -0
  136. data/gemfiles/mongo_mapper_0.5.8.gemfile.lock +36 -0
  137. data/gemfiles/mongo_mapper_0.6.0.gemfile +8 -0
  138. data/gemfiles/mongo_mapper_0.6.0.gemfile.lock +36 -0
  139. data/gemfiles/mongo_mapper_0.6.10.gemfile +8 -0
  140. data/gemfiles/mongo_mapper_0.6.10.gemfile.lock +36 -0
  141. data/gemfiles/mongo_mapper_0.7.0.gemfile +8 -0
  142. data/gemfiles/mongo_mapper_0.7.0.gemfile.lock +36 -0
  143. data/gemfiles/mongo_mapper_0.7.5.gemfile +8 -0
  144. data/gemfiles/mongo_mapper_0.7.5.gemfile.lock +39 -0
  145. data/gemfiles/mongo_mapper_0.8.0.gemfile +10 -0
  146. data/gemfiles/mongo_mapper_0.8.0.gemfile.lock +43 -0
  147. data/gemfiles/mongo_mapper_0.8.3.gemfile +10 -0
  148. data/gemfiles/mongo_mapper_0.8.3.gemfile.lock +43 -0
  149. data/gemfiles/mongo_mapper_0.8.4.gemfile +8 -0
  150. data/gemfiles/mongo_mapper_0.8.4.gemfile.lock +42 -0
  151. data/gemfiles/mongo_mapper_0.8.6.gemfile +8 -0
  152. data/gemfiles/mongo_mapper_0.8.6.gemfile.lock +42 -0
  153. data/gemfiles/mongo_mapper_0.9.0.gemfile +7 -0
  154. data/gemfiles/mongo_mapper_0.9.0.gemfile.lock +45 -0
  155. data/gemfiles/mongoid_2.0.0.gemfile +9 -0
  156. data/gemfiles/mongoid_2.0.0.gemfile.lock +49 -0
  157. data/gemfiles/mongoid_2.1.4.gemfile +9 -0
  158. data/gemfiles/mongoid_2.1.4.gemfile.lock +47 -0
  159. data/gemfiles/mongoid_2.2.4.gemfile +9 -0
  160. data/gemfiles/mongoid_2.2.4.gemfile.lock +47 -0
  161. data/gemfiles/mongoid_2.3.3.gemfile +9 -0
  162. data/gemfiles/mongoid_2.3.3.gemfile.lock +47 -0
  163. data/gemfiles/mongoid_2.4.0.gemfile +9 -0
  164. data/gemfiles/mongoid_2.4.0.gemfile.lock +47 -0
  165. data/gemfiles/mongoid_2.4.10.gemfile +9 -0
  166. data/gemfiles/mongoid_2.4.10.gemfile.lock +47 -0
  167. data/gemfiles/mongoid_2.5.2.gemfile +9 -0
  168. data/gemfiles/mongoid_2.5.2.gemfile.lock +47 -0
  169. data/gemfiles/mongoid_2.6.0.gemfile +9 -0
  170. data/gemfiles/mongoid_2.6.0.gemfile.lock +47 -0
  171. data/gemfiles/mongoid_3.0.0.gemfile +8 -0
  172. data/gemfiles/mongoid_3.0.0.gemfile.lock +45 -0
  173. data/gemfiles/mongoid_3.0.22.gemfile +8 -0
  174. data/gemfiles/mongoid_3.0.22.gemfile.lock +45 -0
  175. data/gemfiles/mongoid_3.1.0.gemfile +8 -0
  176. data/gemfiles/mongoid_3.1.0.gemfile.lock +45 -0
  177. data/gemfiles/sequel_2.11.0.gemfile +9 -0
  178. data/gemfiles/sequel_2.11.0.gemfile.lock +33 -0
  179. data/gemfiles/sequel_2.12.0.gemfile +9 -0
  180. data/gemfiles/sequel_2.12.0.gemfile.lock +33 -0
  181. data/gemfiles/sequel_2.8.0.gemfile +9 -0
  182. data/gemfiles/sequel_2.8.0.gemfile.lock +33 -0
  183. data/gemfiles/sequel_3.0.0.gemfile +9 -0
  184. data/gemfiles/sequel_3.0.0.gemfile.lock +33 -0
  185. data/gemfiles/sequel_3.10.0.gemfile +9 -0
  186. data/gemfiles/sequel_3.10.0.gemfile.lock +33 -0
  187. data/gemfiles/sequel_3.13.0.gemfile +9 -0
  188. data/gemfiles/sequel_3.13.0.gemfile.lock +33 -0
  189. data/gemfiles/sequel_3.14.0.gemfile +9 -0
  190. data/gemfiles/sequel_3.14.0.gemfile.lock +33 -0
  191. data/gemfiles/sequel_3.23.0.gemfile +9 -0
  192. data/gemfiles/sequel_3.23.0.gemfile.lock +33 -0
  193. data/gemfiles/sequel_3.24.0.gemfile +9 -0
  194. data/gemfiles/sequel_3.24.0.gemfile.lock +33 -0
  195. data/gemfiles/sequel_3.29.0.gemfile +9 -0
  196. data/gemfiles/sequel_3.29.0.gemfile.lock +33 -0
  197. data/gemfiles/sequel_3.34.0.gemfile +9 -0
  198. data/gemfiles/sequel_3.34.0.gemfile.lock +33 -0
  199. data/gemfiles/sequel_3.35.0.gemfile +9 -0
  200. data/gemfiles/sequel_3.35.0.gemfile.lock +33 -0
  201. data/gemfiles/sequel_3.4.0.gemfile +9 -0
  202. data/gemfiles/sequel_3.4.0.gemfile.lock +33 -0
  203. data/gemfiles/sequel_3.44.0.gemfile +9 -0
  204. data/gemfiles/sequel_3.44.0.gemfile.lock +33 -0
  205. data/glebtv_state_machine.gemspec +22 -0
  206. data/init.rb +1 -0
  207. data/lib/glebtv_state_machine.rb +1 -0
  208. data/lib/state_machine.rb +8 -0
  209. data/lib/state_machine/assertions.rb +36 -0
  210. data/lib/state_machine/branch.rb +225 -0
  211. data/lib/state_machine/callback.rb +236 -0
  212. data/lib/state_machine/core.rb +12 -0
  213. data/lib/state_machine/core_ext.rb +2 -0
  214. data/lib/state_machine/core_ext/class/state_machine.rb +5 -0
  215. data/lib/state_machine/error.rb +13 -0
  216. data/lib/state_machine/eval_helpers.rb +87 -0
  217. data/lib/state_machine/event.rb +257 -0
  218. data/lib/state_machine/event_collection.rb +141 -0
  219. data/lib/state_machine/extensions.rb +149 -0
  220. data/lib/state_machine/graph.rb +92 -0
  221. data/lib/state_machine/helper_module.rb +17 -0
  222. data/lib/state_machine/initializers.rb +4 -0
  223. data/lib/state_machine/initializers/merb.rb +1 -0
  224. data/lib/state_machine/initializers/rails.rb +25 -0
  225. data/lib/state_machine/integrations.rb +121 -0
  226. data/lib/state_machine/integrations/active_model.rb +585 -0
  227. data/lib/state_machine/integrations/active_model/locale.rb +11 -0
  228. data/lib/state_machine/integrations/active_model/observer.rb +33 -0
  229. data/lib/state_machine/integrations/active_model/observer_update.rb +42 -0
  230. data/lib/state_machine/integrations/active_model/versions.rb +31 -0
  231. data/lib/state_machine/integrations/active_record.rb +548 -0
  232. data/lib/state_machine/integrations/active_record/locale.rb +20 -0
  233. data/lib/state_machine/integrations/active_record/versions.rb +123 -0
  234. data/lib/state_machine/integrations/base.rb +100 -0
  235. data/lib/state_machine/integrations/data_mapper.rb +511 -0
  236. data/lib/state_machine/integrations/data_mapper/observer.rb +210 -0
  237. data/lib/state_machine/integrations/data_mapper/versions.rb +85 -0
  238. data/lib/state_machine/integrations/mongo_mapper.rb +389 -0
  239. data/lib/state_machine/integrations/mongo_mapper/locale.rb +4 -0
  240. data/lib/state_machine/integrations/mongo_mapper/versions.rb +89 -0
  241. data/lib/state_machine/integrations/mongoid.rb +465 -0
  242. data/lib/state_machine/integrations/mongoid/locale.rb +4 -0
  243. data/lib/state_machine/integrations/mongoid/versions.rb +81 -0
  244. data/lib/state_machine/integrations/sequel.rb +486 -0
  245. data/lib/state_machine/integrations/sequel/versions.rb +95 -0
  246. data/lib/state_machine/machine.rb +2292 -0
  247. data/lib/state_machine/machine_collection.rb +86 -0
  248. data/lib/state_machine/macro_methods.rb +522 -0
  249. data/lib/state_machine/matcher.rb +123 -0
  250. data/lib/state_machine/matcher_helpers.rb +54 -0
  251. data/lib/state_machine/node_collection.rb +222 -0
  252. data/lib/state_machine/path.rb +120 -0
  253. data/lib/state_machine/path_collection.rb +90 -0
  254. data/lib/state_machine/state.rb +297 -0
  255. data/lib/state_machine/state_collection.rb +112 -0
  256. data/lib/state_machine/state_context.rb +138 -0
  257. data/lib/state_machine/transition.rb +470 -0
  258. data/lib/state_machine/transition_collection.rb +245 -0
  259. data/lib/state_machine/version.rb +3 -0
  260. data/lib/state_machine/yard.rb +8 -0
  261. data/lib/state_machine/yard/handlers.rb +12 -0
  262. data/lib/state_machine/yard/handlers/base.rb +32 -0
  263. data/lib/state_machine/yard/handlers/event.rb +25 -0
  264. data/lib/state_machine/yard/handlers/machine.rb +344 -0
  265. data/lib/state_machine/yard/handlers/state.rb +25 -0
  266. data/lib/state_machine/yard/handlers/transition.rb +47 -0
  267. data/lib/state_machine/yard/templates.rb +3 -0
  268. data/lib/state_machine/yard/templates/default/class/html/setup.rb +30 -0
  269. data/lib/state_machine/yard/templates/default/class/html/state_machines.erb +12 -0
  270. data/lib/tasks/state_machine.rake +1 -0
  271. data/lib/tasks/state_machine.rb +30 -0
  272. data/lib/yard-state_machine.rb +2 -0
  273. data/test/files/en.yml +17 -0
  274. data/test/files/switch.rb +15 -0
  275. data/test/functional/state_machine_test.rb +1066 -0
  276. data/test/test_helper.rb +7 -0
  277. data/test/unit/assertions_test.rb +40 -0
  278. data/test/unit/branch_test.rb +969 -0
  279. data/test/unit/callback_test.rb +704 -0
  280. data/test/unit/error_test.rb +43 -0
  281. data/test/unit/eval_helpers_test.rb +270 -0
  282. data/test/unit/event_collection_test.rb +398 -0
  283. data/test/unit/event_test.rb +1196 -0
  284. data/test/unit/graph_test.rb +98 -0
  285. data/test/unit/helper_module_test.rb +17 -0
  286. data/test/unit/integrations/active_model_test.rb +1245 -0
  287. data/test/unit/integrations/active_record_test.rb +2551 -0
  288. data/test/unit/integrations/base_test.rb +104 -0
  289. data/test/unit/integrations/data_mapper_test.rb +2194 -0
  290. data/test/unit/integrations/mongo_mapper_test.rb +2026 -0
  291. data/test/unit/integrations/mongoid_test.rb +2309 -0
  292. data/test/unit/integrations/sequel_test.rb +1896 -0
  293. data/test/unit/integrations_test.rb +83 -0
  294. data/test/unit/invalid_event_test.rb +20 -0
  295. data/test/unit/invalid_parallel_transition_test.rb +18 -0
  296. data/test/unit/invalid_transition_test.rb +115 -0
  297. data/test/unit/machine_collection_test.rb +603 -0
  298. data/test/unit/machine_test.rb +3407 -0
  299. data/test/unit/matcher_helpers_test.rb +37 -0
  300. data/test/unit/matcher_test.rb +155 -0
  301. data/test/unit/node_collection_test.rb +362 -0
  302. data/test/unit/path_collection_test.rb +266 -0
  303. data/test/unit/path_test.rb +485 -0
  304. data/test/unit/state_collection_test.rb +352 -0
  305. data/test/unit/state_context_test.rb +441 -0
  306. data/test/unit/state_machine_test.rb +31 -0
  307. data/test/unit/state_test.rb +1101 -0
  308. data/test/unit/transition_collection_test.rb +2168 -0
  309. data/test/unit/transition_test.rb +1558 -0
  310. metadata +438 -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.
@@ -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