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
@@ -0,0 +1,95 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module Sequel
4
+ version '2.8.x - 3.23.x' do
5
+ def self.active?
6
+ !defined?(::Sequel::MAJOR) || ::Sequel::MAJOR == 2 || ::Sequel::MAJOR == 3 && ::Sequel::MINOR <= 23
7
+ end
8
+
9
+ def define_state_initializer
10
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
11
+ def initialize(*)
12
+ super do |*args|
13
+ self.class.state_machines.initialize_states(self, :static => false)
14
+ changed_columns.clear
15
+ yield(*args) if block_given?
16
+ end
17
+ end
18
+
19
+ def set(*)
20
+ self.class.state_machines.initialize_states(self, :static => :force, :dynamic => false) if values.empty?
21
+ super
22
+ end
23
+ end_eval
24
+ end
25
+
26
+ def define_validation_hook
27
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
28
+ def valid?(*args)
29
+ opts = args.first.is_a?(Hash) ? args.first : {}
30
+ yielded = false
31
+ result = self.class.state_machines.transitions(self, :save, :after => false).perform do
32
+ yielded = true
33
+ super
34
+ end
35
+
36
+ if yielded || result
37
+ result
38
+ else
39
+ #{handle_validation_failure}
40
+ end
41
+ end
42
+ end_eval
43
+ end
44
+ end
45
+
46
+ version '2.8.x - 3.13.x' do
47
+ def self.active?
48
+ !defined?(::Sequel::MAJOR) || ::Sequel::MAJOR == 2 || ::Sequel::MAJOR == 3 && ::Sequel::MINOR <= 13
49
+ end
50
+
51
+ def handle_validation_failure
52
+ 'raise_on_save_failure ? save_failure(:validation) : result'
53
+ end
54
+
55
+ def handle_save_failure
56
+ 'save_failure(:save) if raise_on_save_failure'
57
+ end
58
+ end
59
+
60
+ version '2.8.x - 2.11.x' do
61
+ def self.active?
62
+ !defined?(::Sequel::MAJOR) || ::Sequel::MAJOR == 2 && ::Sequel::MINOR <= 11
63
+ end
64
+
65
+ def load_plugins
66
+ end
67
+
68
+ def load_inflector
69
+ end
70
+
71
+ def model_from_dataset(dataset)
72
+ dataset.model_classes[nil]
73
+ end
74
+
75
+ def define_state_accessor
76
+ name = self.name
77
+ owner_class.validates_each(attribute) do |record, attr, value|
78
+ machine = record.class.state_machine(name)
79
+ machine.invalidate(record, :state, :invalid) unless machine.states.match(record)
80
+ end
81
+ end
82
+ end
83
+
84
+ version '3.14.x - 3.23.x' do
85
+ def self.active?
86
+ defined?(::Sequel::MAJOR) && ::Sequel::MAJOR == 3 && ::Sequel::MINOR >= 14 && ::Sequel::MINOR <= 23
87
+ end
88
+
89
+ def handle_validation_failure
90
+ 'raise_on_failure?(opts) ? raise_hook_failure(:validation) : result'
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,2292 @@
1
+ require 'state_machine/extensions'
2
+ require 'state_machine/assertions'
3
+ require 'state_machine/integrations'
4
+
5
+ require 'state_machine/helper_module'
6
+ require 'state_machine/state'
7
+ require 'state_machine/event'
8
+ require 'state_machine/callback'
9
+ require 'state_machine/node_collection'
10
+ require 'state_machine/state_collection'
11
+ require 'state_machine/event_collection'
12
+ require 'state_machine/path_collection'
13
+ require 'state_machine/matcher_helpers'
14
+
15
+ module StateMachine
16
+ # Represents a state machine for a particular attribute. State machines
17
+ # consist of states, events and a set of transitions that define how the
18
+ # state changes after a particular event is fired.
19
+ #
20
+ # A state machine will not know all of the possible states for an object
21
+ # unless they are referenced *somewhere* in the state machine definition.
22
+ # As a result, any unused states should be defined with the +other_states+
23
+ # or +state+ helper.
24
+ #
25
+ # == Actions
26
+ #
27
+ # When an action is configured for a state machine, it is invoked when an
28
+ # object transitions via an event. The success of the event becomes
29
+ # dependent on the success of the action. If the action is successful, then
30
+ # the transitioned state remains persisted. However, if the action fails
31
+ # (by returning false), the transitioned state will be rolled back.
32
+ #
33
+ # For example,
34
+ #
35
+ # class Vehicle
36
+ # attr_accessor :fail, :saving_state
37
+ #
38
+ # state_machine :initial => :parked, :action => :save do
39
+ # event :ignite do
40
+ # transition :parked => :idling
41
+ # end
42
+ #
43
+ # event :park do
44
+ # transition :idling => :parked
45
+ # end
46
+ # end
47
+ #
48
+ # def save
49
+ # @saving_state = state
50
+ # fail != true
51
+ # end
52
+ # end
53
+ #
54
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
55
+ # vehicle.save # => true
56
+ # vehicle.saving_state # => "parked" # The state was "parked" was save was called
57
+ #
58
+ # # Successful event
59
+ # vehicle.ignite # => true
60
+ # vehicle.saving_state # => "idling" # The state was "idling" when save was called
61
+ # vehicle.state # => "idling"
62
+ #
63
+ # # Failed event
64
+ # vehicle.fail = true
65
+ # vehicle.park # => false
66
+ # vehicle.saving_state # => "parked"
67
+ # vehicle.state # => "idling"
68
+ #
69
+ # As shown, even though the state is set prior to calling the +save+ action
70
+ # on the object, it will be rolled back to the original state if the action
71
+ # fails. *Note* that this will also be the case if an exception is raised
72
+ # while calling the action.
73
+ #
74
+ # === Indirect transitions
75
+ #
76
+ # In addition to the action being run as the _result_ of an event, the action
77
+ # can also be used to run events itself. For example, using the above as an
78
+ # example:
79
+ #
80
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
81
+ #
82
+ # vehicle.state_event = 'ignite'
83
+ # vehicle.save # => true
84
+ # vehicle.state # => "idling"
85
+ # vehicle.state_event # => nil
86
+ #
87
+ # As can be seen, the +save+ action automatically invokes the event stored in
88
+ # the +state_event+ attribute (<tt>:ignite</tt> in this case).
89
+ #
90
+ # One important note about using this technique for running transitions is
91
+ # that if the class in which the state machine is defined *also* defines the
92
+ # action being invoked (and not a superclass), then it must manually run the
93
+ # StateMachine hook that checks for event attributes.
94
+ #
95
+ # For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel,
96
+ # the default action (+save+) is already defined in a base class. As a result,
97
+ # when a state machine is defined in a model / resource, StateMachine can
98
+ # automatically hook into the +save+ action.
99
+ #
100
+ # On the other hand, the Vehicle class from above defined its own +save+
101
+ # method (and there is no +save+ method in its superclass). As a result, it
102
+ # must be modified like so:
103
+ #
104
+ # def save
105
+ # self.class.state_machines.transitions(self, :save).perform do
106
+ # @saving_state = state
107
+ # fail != true
108
+ # end
109
+ # end
110
+ #
111
+ # This will add in the functionality for firing the event stored in the
112
+ # +state_event+ attribute.
113
+ #
114
+ # == Callbacks
115
+ #
116
+ # Callbacks are supported for hooking before and after every possible
117
+ # transition in the machine. Each callback is invoked in the order in which
118
+ # it was defined. See StateMachine::Machine#before_transition and
119
+ # StateMachine::Machine#after_transition for documentation on how to define
120
+ # new callbacks.
121
+ #
122
+ # *Note* that callbacks only get executed within the context of an event. As
123
+ # a result, if a class has an initial state when it's created, any callbacks
124
+ # that would normally get executed when the object enters that state will
125
+ # *not* get triggered.
126
+ #
127
+ # For example,
128
+ #
129
+ # class Vehicle
130
+ # state_machine :initial => :parked do
131
+ # after_transition all => :parked do
132
+ # raise ArgumentError
133
+ # end
134
+ # ...
135
+ # end
136
+ # end
137
+ #
138
+ # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
139
+ # vehicle.save # => true (no exception raised)
140
+ #
141
+ # If you need callbacks to get triggered when an object is created, this
142
+ # should be done by one of the following techniques:
143
+ # * Use a <tt>before :create</tt> or equivalent hook:
144
+ #
145
+ # class Vehicle
146
+ # before :create, :track_initial_transition
147
+ #
148
+ # state_machine do
149
+ # ...
150
+ # end
151
+ # end
152
+ #
153
+ # * Set an initial state and use the correct event to create the
154
+ # object with the proper state, resulting in callbacks being triggered and
155
+ # the object getting persisted (note that the <tt>:pending</tt> state is
156
+ # actually stored as nil):
157
+ #
158
+ # class Vehicle
159
+ # state_machine :initial => :pending
160
+ # after_transition :pending => :parked, :do => :track_initial_transition
161
+ #
162
+ # event :park do
163
+ # transition :pending => :parked
164
+ # end
165
+ #
166
+ # state :pending, :value => nil
167
+ # end
168
+ # end
169
+ #
170
+ # vehicle = Vehicle.new
171
+ # vehicle.park
172
+ #
173
+ # * Use a default event attribute that will automatically trigger when the
174
+ # configured action gets run (note that the <tt>:pending</tt> state is
175
+ # actually stored as nil):
176
+ #
177
+ # class Vehicle < ActiveRecord::Base
178
+ # state_machine :initial => :pending
179
+ # after_transition :pending => :parked, :do => :track_initial_transition
180
+ #
181
+ # event :park do
182
+ # transition :pending => :parked
183
+ # end
184
+ #
185
+ # state :pending, :value => nil
186
+ # end
187
+ #
188
+ # def initialize(*)
189
+ # super
190
+ # self.state_event = 'park'
191
+ # end
192
+ # end
193
+ #
194
+ # vehicle = Vehicle.new
195
+ # vehicle.save
196
+ #
197
+ # === Canceling callbacks
198
+ #
199
+ # Callbacks can be canceled by throwing :halt at any point during the
200
+ # callback. For example,
201
+ #
202
+ # ...
203
+ # throw :halt
204
+ # ...
205
+ #
206
+ # If a +before+ callback halts the chain, the associated transition and all
207
+ # later callbacks are canceled. If an +after+ callback halts the chain,
208
+ # the later callbacks are canceled, but the transition is still successful.
209
+ #
210
+ # These same rules apply to +around+ callbacks with the exception that any
211
+ # +around+ callback that doesn't yield will essentially result in :halt being
212
+ # thrown. Any code executed after the yield will behave in the same way as
213
+ # +after+ callbacks.
214
+ #
215
+ # *Note* that if a +before+ callback fails and the bang version of an event
216
+ # was invoked, an exception will be raised instead of returning false. For
217
+ # example,
218
+ #
219
+ # class Vehicle
220
+ # state_machine :initial => :parked do
221
+ # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
222
+ # ...
223
+ # end
224
+ # end
225
+ #
226
+ # vehicle = Vehicle.new
227
+ # vehicle.park # => false
228
+ # vehicle.park! # => StateMachine::InvalidTransition: Cannot transition state via :park from "idling"
229
+ #
230
+ # == Observers
231
+ #
232
+ # Observers, in the sense of external classes and *not* Ruby's Observable
233
+ # mechanism, can hook into state machines as well. Such observers use the
234
+ # same callback api that's used internally.
235
+ #
236
+ # Below are examples of defining observers for the following state machine:
237
+ #
238
+ # class Vehicle
239
+ # state_machine do
240
+ # event :park do
241
+ # transition :idling => :parked
242
+ # end
243
+ # ...
244
+ # end
245
+ # ...
246
+ # end
247
+ #
248
+ # Event/Transition behaviors:
249
+ #
250
+ # class VehicleObserver
251
+ # def self.before_park(vehicle, transition)
252
+ # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
253
+ # end
254
+ #
255
+ # def self.after_park(vehicle, transition, result)
256
+ # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
257
+ # end
258
+ #
259
+ # def self.before_transition(vehicle, transition)
260
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
261
+ # end
262
+ #
263
+ # def self.after_transition(vehicle, transition)
264
+ # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
265
+ # end
266
+ #
267
+ # def self.around_transition(vehicle, transition)
268
+ # logger.info Benchmark.measure { yield }
269
+ # end
270
+ # end
271
+ #
272
+ # Vehicle.state_machine do
273
+ # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
274
+ # before_transition VehicleObserver.method(:before_transition)
275
+ #
276
+ # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
277
+ # after_transition VehicleObserver.method(:after_transition)
278
+ #
279
+ # around_transition VehicleObserver.method(:around_transition)
280
+ # end
281
+ #
282
+ # One common callback is to record transitions for all models in the system
283
+ # for auditing/debugging purposes. Below is an example of an observer that
284
+ # can easily automate this process for all models:
285
+ #
286
+ # class StateMachineObserver
287
+ # def self.before_transition(object, transition)
288
+ # Audit.log_transition(object.attributes)
289
+ # end
290
+ # end
291
+ #
292
+ # [Vehicle, Switch, Project].each do |klass|
293
+ # klass.state_machines.each do |attribute, machine|
294
+ # machine.before_transition StateMachineObserver.method(:before_transition)
295
+ # end
296
+ # end
297
+ #
298
+ # Additional observer-like behavior may be exposed by the various integrations
299
+ # available. See below for more information on integrations.
300
+ #
301
+ # == Overriding instance / class methods
302
+ #
303
+ # Hooking in behavior to the generated instance / class methods from the
304
+ # state machine, events, and states is very simple because of the way these
305
+ # methods are generated on the class. Using the class's ancestors, the
306
+ # original generated method can be referred to via +super+. For example,
307
+ #
308
+ # class Vehicle
309
+ # state_machine do
310
+ # event :park do
311
+ # ...
312
+ # end
313
+ # end
314
+ #
315
+ # def park(*args)
316
+ # logger.info "..."
317
+ # super
318
+ # end
319
+ # end
320
+ #
321
+ # In the above example, the +park+ instance method that's generated on the
322
+ # Vehicle class (by the associated event) is overridden with custom behavior.
323
+ # Once this behavior is complete, the original method from the state machine
324
+ # is invoked by simply calling +super+.
325
+ #
326
+ # The same technique can be used for +state+, +state_name+, and all other
327
+ # instance *and* class methods on the Vehicle class.
328
+ #
329
+ # == Method conflicts
330
+ #
331
+ # By default state_machine does not redefine methods that exist on
332
+ # superclasses (*including* Object) or any modules (*including* Kernel) that
333
+ # were included before it was defined. This is in order to ensure that
334
+ # existing behavior on the class is not broken by the inclusion of
335
+ # state_machine.
336
+ #
337
+ # If a conflicting method is detected, state_machine will generate a warning.
338
+ # For example, consider the following class:
339
+ #
340
+ # class Vehicle
341
+ # state_machine do
342
+ # event :open do
343
+ # ...
344
+ # end
345
+ # end
346
+ # end
347
+ #
348
+ # In the above class, an event named "open" is defined for its state machine.
349
+ # However, "open" is already defined as an instance method in Ruby's Kernel
350
+ # module that gets included in every Object. As a result, state_machine will
351
+ # generate the following warning:
352
+ #
353
+ # Instance method "open" is already defined in Object, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true.
354
+ #
355
+ # Even though you may not be using Kernel's implementation of the "open"
356
+ # instance method, state_machine isn't aware of this and, as a result, stays
357
+ # safe and just skips redefining the method.
358
+ #
359
+ # As with almost all helpers methods defined by state_machine in your class,
360
+ # there are generic methods available for working around this method conflict.
361
+ # In the example above, you can invoke the "open" event like so:
362
+ #
363
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
364
+ # vehicle.fire_events(:open) # => true
365
+ #
366
+ # # This will not work
367
+ # vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
368
+ #
369
+ # If you want to take on the risk of overriding existing methods and just
370
+ # ignore method conflicts altogether, you can do so by setting the following
371
+ # configuration:
372
+ #
373
+ # StateMachine::Machine.ignore_method_conflicts = true
374
+ #
375
+ # This will allow you to define events like "open" as described above and
376
+ # still generate the "open" instance helper method. For example:
377
+ #
378
+ # StateMachine::Machine.ignore_method_conflicts = true
379
+ #
380
+ # class Vehicle
381
+ # state_machine do
382
+ # event :open do
383
+ # ...
384
+ # end
385
+ # end
386
+ #
387
+ # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
388
+ # vehicle.open # => true
389
+ #
390
+ # By default, state_machine helps prevent you from making mistakes and
391
+ # accidentally overriding methods that you didn't intend to. Once you
392
+ # understand this and what the consequences are, setting the
393
+ # +ignore_method_conflicts+ option is a perfectly reasonable workaround.
394
+ #
395
+ # == Integrations
396
+ #
397
+ # By default, state machines are library-agnostic, meaning that they work
398
+ # on any Ruby class and have no external dependencies. However, there are
399
+ # certain libraries which expose additional behavior that can be taken
400
+ # advantage of by state machines.
401
+ #
402
+ # This library is built to work out of the box with a few popular Ruby
403
+ # libraries that allow for additional behavior to provide a cleaner and
404
+ # smoother experience. This is especially the case for objects backed by a
405
+ # database that may allow for transactions, persistent storage,
406
+ # search/filters, callbacks, etc.
407
+ #
408
+ # When a state machine is defined for classes using any of the above libraries,
409
+ # it will try to automatically determine the integration to use (Agnostic,
410
+ # ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel)
411
+ # based on the class definition. To see how each integration affects the
412
+ # machine's behavior, refer to all constants defined under the
413
+ # StateMachine::Integrations namespace.
414
+ class Machine
415
+ include Assertions
416
+ include EvalHelpers
417
+ include MatcherHelpers
418
+
419
+ class << self
420
+ # Attempts to find or create a state machine for the given class. For
421
+ # example,
422
+ #
423
+ # StateMachine::Machine.find_or_create(Vehicle)
424
+ # StateMachine::Machine.find_or_create(Vehicle, :initial => :parked)
425
+ # StateMachine::Machine.find_or_create(Vehicle, :status)
426
+ # StateMachine::Machine.find_or_create(Vehicle, :status, :initial => :parked)
427
+ #
428
+ # If a machine of the given name already exists in one of the class's
429
+ # superclasses, then a copy of that machine will be created and stored
430
+ # in the new owner class (the original will remain unchanged).
431
+ def find_or_create(owner_class, *args, &block)
432
+ options = args.last.is_a?(Hash) ? args.pop : {}
433
+ name = args.first || :state
434
+
435
+ # Find an existing machine
436
+ if owner_class.respond_to?(:state_machines) && machine = owner_class.state_machines[name]
437
+ # Only create a new copy if changes are being made to the machine in
438
+ # a subclass
439
+ if machine.owner_class != owner_class && (options.any? || block_given?)
440
+ machine = machine.clone
441
+ machine.initial_state = options[:initial] if options.include?(:initial)
442
+ machine.owner_class = owner_class
443
+ end
444
+
445
+ # Evaluate DSL
446
+ machine.instance_eval(&block) if block_given?
447
+ else
448
+ # No existing machine: create a new one
449
+ machine = new(owner_class, name, options, &block)
450
+ end
451
+
452
+ machine
453
+ end
454
+
455
+ # Draws the state machines defined in the given classes using GraphViz.
456
+ # The given classes must be a comma-delimited string of class names.
457
+ #
458
+ # Configuration options:
459
+ # * <tt>:file</tt> - A comma-delimited string of files to load that
460
+ # contain the state machine definitions to draw
461
+ # * <tt>:path</tt> - The path to write the graph file to
462
+ # * <tt>:format</tt> - The image format to generate the graph in
463
+ # * <tt>:font</tt> - The name of the font to draw state names in
464
+ def draw(class_names, options = {})
465
+ raise ArgumentError, 'At least one class must be specified' unless class_names && class_names.split(',').any?
466
+
467
+ # Load any files
468
+ if files = options.delete(:file)
469
+ files.split(',').each {|file| require file}
470
+ end
471
+
472
+ class_names.split(',').each do |class_name|
473
+ # Navigate through the namespace structure to get to the class
474
+ klass = Object
475
+ class_name.split('::').each do |name|
476
+ klass = klass.const_defined?(name) ? klass.const_get(name) : klass.const_missing(name)
477
+ end
478
+
479
+ # Draw each of the class's state machines
480
+ klass.state_machines.each_value do |machine|
481
+ machine.draw(options)
482
+ end
483
+ end
484
+ end
485
+ end
486
+
487
+ # Default messages to use for validation errors in ORM integrations
488
+ class << self; attr_accessor :default_messages; end
489
+ @default_messages = {
490
+ :invalid => 'is invalid',
491
+ :invalid_event => 'cannot transition when %s',
492
+ :invalid_transition => 'cannot transition via "%1$s"'
493
+ }
494
+
495
+ # Whether to ignore any conflicts that are detected for helper methods that
496
+ # get generated for a machine's owner class. Default is false.
497
+ class << self; attr_accessor :ignore_method_conflicts; end
498
+ @ignore_method_conflicts = false
499
+
500
+ # The class that the machine is defined in
501
+ attr_reader :owner_class
502
+
503
+ # The name of the machine, used for scoping methods generated for the
504
+ # machine as a whole (not states or events)
505
+ attr_reader :name
506
+
507
+ # The events that trigger transitions. These are sorted, by default, in
508
+ # the order in which they were defined.
509
+ attr_reader :events
510
+
511
+ # A list of all of the states known to this state machine. This will pull
512
+ # states from the following sources:
513
+ # * Initial state
514
+ # * State behaviors
515
+ # * Event transitions (:to, :from, and :except_from options)
516
+ # * Transition callbacks (:to, :from, :except_to, and :except_from options)
517
+ # * Unreferenced states (using +other_states+ helper)
518
+ #
519
+ # These are sorted, by default, in the order in which they were referenced.
520
+ attr_reader :states
521
+
522
+ # The callbacks to invoke before/after a transition is performed
523
+ #
524
+ # Maps :before => callbacks and :after => callbacks
525
+ attr_reader :callbacks
526
+
527
+ # The action to invoke when an object transitions
528
+ attr_reader :action
529
+
530
+ # An identifier that forces all methods (including state predicates and
531
+ # event methods) to be generated with the value prefixed or suffixed,
532
+ # depending on the context.
533
+ attr_reader :namespace
534
+
535
+ # Whether the machine will use transactions when firing events
536
+ attr_reader :use_transactions
537
+
538
+ # Creates a new state machine for the given attribute
539
+ def initialize(owner_class, *args, &block)
540
+ options = args.last.is_a?(Hash) ? args.pop : {}
541
+ assert_valid_keys(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
542
+
543
+ # Find an integration that matches this machine's owner class
544
+ if options.include?(:integration)
545
+ @integration = options[:integration] && StateMachine::Integrations.find_by_name(options[:integration])
546
+ else
547
+ @integration = StateMachine::Integrations.match(owner_class)
548
+ end
549
+
550
+ if @integration
551
+ extend @integration
552
+ options = (@integration.defaults || {}).merge(options)
553
+ end
554
+
555
+ # Add machine-wide defaults
556
+ options = {:use_transactions => true, :initialize => true}.merge(options)
557
+
558
+ # Set machine configuration
559
+ @name = args.first || :state
560
+ @attribute = options[:attribute] || @name
561
+ @events = EventCollection.new(self)
562
+ @states = StateCollection.new(self)
563
+ @callbacks = {:before => [], :after => [], :failure => []}
564
+ @namespace = options[:namespace]
565
+ @messages = options[:messages] || {}
566
+ @action = options[:action]
567
+ @use_transactions = options[:use_transactions]
568
+ @initialize_state = options[:initialize]
569
+ @action_hook_defined = false
570
+ self.owner_class = owner_class
571
+
572
+ # Merge with sibling machine configurations
573
+ add_sibling_machine_configs
574
+
575
+ # Define class integration
576
+ define_helpers
577
+ define_scopes(options[:plural])
578
+ after_initialize
579
+
580
+ # Evaluate DSL
581
+ instance_eval(&block) if block_given?
582
+ self.initial_state = options[:initial] unless sibling_machines.any?
583
+ end
584
+
585
+ # Creates a copy of this machine in addition to copies of each associated
586
+ # event/states/callback, so that the modifications to those collections do
587
+ # not affect the original machine.
588
+ def initialize_copy(orig) #:nodoc:
589
+ super
590
+
591
+ @events = @events.dup
592
+ @events.machine = self
593
+ @states = @states.dup
594
+ @states.machine = self
595
+ @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
596
+ end
597
+
598
+ # Sets the class which is the owner of this state machine. Any methods
599
+ # generated by states, events, or other parts of the machine will be defined
600
+ # on the given owner class.
601
+ def owner_class=(klass)
602
+ @owner_class = klass
603
+
604
+ # Create modules for extending the class with state/event-specific methods
605
+ @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
606
+ owner_class.class_eval do
607
+ extend helper_modules[:class]
608
+ include helper_modules[:instance]
609
+ end
610
+
611
+ # Add class-/instance-level methods to the owner class for state initialization
612
+ unless owner_class < StateMachine::InstanceMethods
613
+ owner_class.class_eval do
614
+ extend StateMachine::ClassMethods
615
+ include StateMachine::InstanceMethods
616
+ end
617
+
618
+ define_state_initializer if @initialize_state
619
+ end
620
+
621
+ # Record this machine as matched to the name in the current owner class.
622
+ # This will override any machines mapped to the same name in any superclasses.
623
+ owner_class.state_machines[name] = self
624
+ end
625
+
626
+ # Sets the initial state of the machine. This can be either the static name
627
+ # of a state or a lambda block which determines the initial state at
628
+ # creation time.
629
+ def initial_state=(new_initial_state)
630
+ @initial_state = new_initial_state
631
+ add_states([@initial_state]) unless dynamic_initial_state?
632
+
633
+ # Update all states to reflect the new initial state
634
+ states.each {|state| state.initial = (state.name == @initial_state)}
635
+
636
+ # Output a warning if there are conflicting initial states for the machine's
637
+ # attribute
638
+ initial_state = states.detect {|state| state.initial}
639
+ if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
640
+ warn(
641
+ "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
642
+ "a different default for \"#{attribute}\". Use only one or the other for "\
643
+ "defining defaults to avoid unexpected behaviors."
644
+ )
645
+ end
646
+ end
647
+
648
+ # Gets the initial state of the machine for the given object. If a dynamic
649
+ # initial state was configured for this machine, then the object will be
650
+ # passed into the lambda block to help determine the actual state.
651
+ #
652
+ # == Examples
653
+ #
654
+ # With a static initial state:
655
+ #
656
+ # class Vehicle
657
+ # state_machine :initial => :parked do
658
+ # ...
659
+ # end
660
+ # end
661
+ #
662
+ # vehicle = Vehicle.new
663
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
664
+ #
665
+ # With a dynamic initial state:
666
+ #
667
+ # class Vehicle
668
+ # attr_accessor :force_idle
669
+ #
670
+ # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
671
+ # ...
672
+ # end
673
+ # end
674
+ #
675
+ # vehicle = Vehicle.new
676
+ #
677
+ # vehicle.force_idle = true
678
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
679
+ #
680
+ # vehicle.force_idle = false
681
+ # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
682
+ def initial_state(object)
683
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
684
+ end
685
+
686
+ # Whether a dynamic initial state is being used in the machine
687
+ def dynamic_initial_state?
688
+ instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
689
+ end
690
+
691
+ # Initializes the state on the given object. Initial values are only set if
692
+ # the machine's attribute hasn't been previously initialized.
693
+ #
694
+ # Configuration options:
695
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
696
+ # current value
697
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
698
+ # directly to the object
699
+ def initialize_state(object, options = {})
700
+ state = initial_state(object)
701
+ if state && (options[:force] || initialize_state?(object))
702
+ value = state.value
703
+
704
+ if hash = options[:to]
705
+ hash[attribute.to_s] = value
706
+ else
707
+ write(object, :state, value)
708
+ end
709
+ end
710
+ end
711
+
712
+ # Gets the actual name of the attribute on the machine's owner class that
713
+ # stores data with the given name.
714
+ def attribute(name = :state)
715
+ name == :state ? @attribute : :"#{self.name}_#{name}"
716
+ end
717
+
718
+ # Defines a new helper method in an instance or class scope with the given
719
+ # name. If the method is already defined in the scope, then this will not
720
+ # override it.
721
+ #
722
+ # If passing in a block, there are two side effects to be aware of
723
+ # 1. The method cannot be chained, meaning that the block cannot call +super+
724
+ # 2. If the method is already defined in an ancestor, then it will not get
725
+ # overridden and a warning will be output.
726
+ #
727
+ # Example:
728
+ #
729
+ # # Instance helper
730
+ # machine.define_helper(:instance, :state_name) do |machine, object|
731
+ # machine.states.match(object).name
732
+ # end
733
+ #
734
+ # # Class helper
735
+ # machine.define_helper(:class, :state_machine_name) do |machine, klass|
736
+ # "State"
737
+ # end
738
+ #
739
+ # You can also define helpers using string evaluation like so:
740
+ #
741
+ # # Instance helper
742
+ # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
743
+ # def state_name
744
+ # self.class.state_machine(:state).states.match(self).name
745
+ # end
746
+ # end_eval
747
+ #
748
+ # # Class helper
749
+ # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
750
+ # def state_machine_name
751
+ # "State"
752
+ # end
753
+ # end_eval
754
+ def define_helper(scope, method, *args, &block)
755
+ helper_module = @helper_modules.fetch(scope)
756
+
757
+ if block_given?
758
+ if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
759
+ ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
760
+ warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachine::Machine.ignore_method_conflicts = true."
761
+ else
762
+ name = self.name
763
+ helper_module.class_eval do
764
+ define_method(method) do |*block_args|
765
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args)
766
+ end
767
+ end
768
+ end
769
+ else
770
+ helper_module.class_eval(method, *args)
771
+ end
772
+ end
773
+
774
+ # Customizes the definition of one or more states in the machine.
775
+ #
776
+ # Configuration options:
777
+ # * <tt>:value</tt> - The actual value to store when an object transitions
778
+ # to the state. Default is the name (stringified).
779
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
780
+ # then setting this to true will cache the evaluated result
781
+ # * <tt>:if</tt> - Determines whether an object's value matches the state
782
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
783
+ # By default, the configured value is matched.
784
+ # * <tt>:human_name</tt> - The human-readable version of this state's name.
785
+ # By default, this is either defined by the integration or stringifies the
786
+ # name and converts underscores to spaces.
787
+ #
788
+ # == Customizing the stored value
789
+ #
790
+ # Whenever a state is automatically discovered in the state machine, its
791
+ # default value is assumed to be the stringified version of the name. For
792
+ # example,
793
+ #
794
+ # class Vehicle
795
+ # state_machine :initial => :parked do
796
+ # event :ignite do
797
+ # transition :parked => :idling
798
+ # end
799
+ # end
800
+ # end
801
+ #
802
+ # In the above state machine, there are two states automatically discovered:
803
+ # :parked and :idling. These states, by default, will store their stringified
804
+ # equivalents when an object moves into that state (e.g. "parked" / "idling").
805
+ #
806
+ # For legacy systems or when tying state machines into existing frameworks,
807
+ # it's oftentimes necessary to need to store a different value for a state
808
+ # than the default. In order to continue taking advantage of an expressive
809
+ # state machine and helper methods, every defined state can be re-configured
810
+ # with a custom stored value. For example,
811
+ #
812
+ # class Vehicle
813
+ # state_machine :initial => :parked do
814
+ # event :ignite do
815
+ # transition :parked => :idling
816
+ # end
817
+ #
818
+ # state :idling, :value => 'IDLING'
819
+ # state :parked, :value => 'PARKED
820
+ # end
821
+ # end
822
+ #
823
+ # This is also useful if being used in association with a database and,
824
+ # instead of storing the state name in a column, you want to store the
825
+ # state's foreign key:
826
+ #
827
+ # class VehicleState < ActiveRecord::Base
828
+ # end
829
+ #
830
+ # class Vehicle < ActiveRecord::Base
831
+ # state_machine :attribute => :state_id, :initial => :parked do
832
+ # event :ignite do
833
+ # transition :parked => :idling
834
+ # end
835
+ #
836
+ # states.each do |state|
837
+ # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
838
+ # end
839
+ # end
840
+ # end
841
+ #
842
+ # In the above example, each known state is configured to store it's
843
+ # associated database id in the +state_id+ attribute. Also, notice that a
844
+ # lambda block is used to define the state's value. This is required in
845
+ # situations (like testing) where the model is loaded without any existing
846
+ # data (i.e. no VehicleState records available).
847
+ #
848
+ # One caveat to the above example is to keep performance in mind. To avoid
849
+ # constant db hits for looking up the VehicleState ids, the value is cached
850
+ # by specifying the <tt>:cache</tt> option. Alternatively, a custom
851
+ # caching strategy can be used like so:
852
+ #
853
+ # class VehicleState < ActiveRecord::Base
854
+ # cattr_accessor :cache_store
855
+ # self.cache_store = ActiveSupport::Cache::MemoryStore.new
856
+ #
857
+ # def self.find_by_name(name)
858
+ # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
859
+ # end
860
+ # end
861
+ #
862
+ # === Dynamic values
863
+ #
864
+ # In addition to customizing states with other value types, lambda blocks
865
+ # can also be specified to allow for a state's value to be determined
866
+ # dynamically at runtime. For example,
867
+ #
868
+ # class Vehicle
869
+ # state_machine :purchased_at, :initial => :available do
870
+ # event :purchase do
871
+ # transition all => :purchased
872
+ # end
873
+ #
874
+ # event :restock do
875
+ # transition all => :available
876
+ # end
877
+ #
878
+ # state :available, :value => nil
879
+ # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
880
+ # end
881
+ # end
882
+ #
883
+ # In the above definition, the <tt>:purchased</tt> state is customized with
884
+ # both a dynamic value *and* a value matcher.
885
+ #
886
+ # When an object transitions to the purchased state, the value's lambda
887
+ # block will be called. This will get the current time and store it in the
888
+ # object's +purchased_at+ attribute.
889
+ #
890
+ # *Note* that the custom matcher is very important here. Since there's no
891
+ # way for the state machine to figure out an object's state when it's set to
892
+ # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
893
+ # were not configured for the state, then an ArgumentError exception would
894
+ # be raised at runtime, indicating that the state machine could not figure
895
+ # out what the current state of the object was.
896
+ #
897
+ # == Behaviors
898
+ #
899
+ # Behaviors define a series of methods to mixin with objects when the current
900
+ # state matches the given one(s). This allows instance methods to behave
901
+ # a specific way depending on what the value of the object's state is.
902
+ #
903
+ # For example,
904
+ #
905
+ # class Vehicle
906
+ # attr_accessor :driver
907
+ # attr_accessor :passenger
908
+ #
909
+ # state_machine :initial => :parked do
910
+ # event :ignite do
911
+ # transition :parked => :idling
912
+ # end
913
+ #
914
+ # state :parked do
915
+ # def speed
916
+ # 0
917
+ # end
918
+ #
919
+ # def rotate_driver
920
+ # driver = self.driver
921
+ # self.driver = passenger
922
+ # self.passenger = driver
923
+ # true
924
+ # end
925
+ # end
926
+ #
927
+ # state :idling, :first_gear do
928
+ # def speed
929
+ # 20
930
+ # end
931
+ #
932
+ # def rotate_driver
933
+ # self.state = 'parked'
934
+ # rotate_driver
935
+ # end
936
+ # end
937
+ #
938
+ # other_states :backing_up
939
+ # end
940
+ # end
941
+ #
942
+ # In the above example, there are two dynamic behaviors defined for the
943
+ # class:
944
+ # * +speed+
945
+ # * +rotate_driver+
946
+ #
947
+ # Each of these behaviors are instance methods on the Vehicle class. However,
948
+ # which method actually gets invoked is based on the current state of the
949
+ # object. Using the above class as the example:
950
+ #
951
+ # vehicle = Vehicle.new
952
+ # vehicle.driver = 'John'
953
+ # vehicle.passenger = 'Jane'
954
+ #
955
+ # # Behaviors in the "parked" state
956
+ # vehicle.state # => "parked"
957
+ # vehicle.speed # => 0
958
+ # vehicle.rotate_driver # => true
959
+ # vehicle.driver # => "Jane"
960
+ # vehicle.passenger # => "John"
961
+ #
962
+ # vehicle.ignite # => true
963
+ #
964
+ # # Behaviors in the "idling" state
965
+ # vehicle.state # => "idling"
966
+ # vehicle.speed # => 20
967
+ # vehicle.rotate_driver # => true
968
+ # vehicle.driver # => "John"
969
+ # vehicle.passenger # => "Jane"
970
+ #
971
+ # As can be seen, both the +speed+ and +rotate_driver+ instance method
972
+ # implementations changed how they behave based on what the current state
973
+ # of the vehicle was.
974
+ #
975
+ # === Invalid behaviors
976
+ #
977
+ # If a specific behavior has not been defined for a state, then a
978
+ # NoMethodError exception will be raised, indicating that that method would
979
+ # not normally exist for an object with that state.
980
+ #
981
+ # Using the example from before:
982
+ #
983
+ # vehicle = Vehicle.new
984
+ # vehicle.state = 'backing_up'
985
+ # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
986
+ #
987
+ # === Using matchers
988
+ #
989
+ # The +all+ / +any+ matchers can be used to easily define behaviors for a
990
+ # group of states. Note, however, that you cannot use these matchers to
991
+ # set configurations for states. Behaviors using these matchers can be
992
+ # defined at any point in the state machine and will always get applied to
993
+ # the proper states.
994
+ #
995
+ # For example:
996
+ #
997
+ # state_machine :initial => :parked do
998
+ # ...
999
+ #
1000
+ # state all - [:parked, :idling, :stalled] do
1001
+ # validates_presence_of :speed
1002
+ #
1003
+ # def speed
1004
+ # gear * 10
1005
+ # end
1006
+ # end
1007
+ # end
1008
+ #
1009
+ # == State-aware class methods
1010
+ #
1011
+ # In addition to defining scopes for instance methods that are state-aware,
1012
+ # the same can be done for certain types of class methods.
1013
+ #
1014
+ # Some libraries have support for class-level methods that only run certain
1015
+ # behaviors based on a conditions hash passed in. For example:
1016
+ #
1017
+ # class Vehicle < ActiveRecord::Base
1018
+ # state_machine do
1019
+ # ...
1020
+ # state :first_gear, :second_gear, :third_gear do
1021
+ # validates_presence_of :speed
1022
+ # validates_inclusion_of :speed, :in => 0..25, :if => :in_school_zone?
1023
+ # end
1024
+ # end
1025
+ # end
1026
+ #
1027
+ # In the above ActiveRecord model, two validations have been defined which
1028
+ # will *only* run when the Vehicle object is in one of the three states:
1029
+ # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
1030
+ # conditions can continue to be used.
1031
+ #
1032
+ # This functionality is not library-specific and can work for any class-level
1033
+ # method that is defined like so:
1034
+ #
1035
+ # def validates_presence_of(attribute, options = {})
1036
+ # ...
1037
+ # end
1038
+ #
1039
+ # The minimum requirement is that the last argument in the method be an
1040
+ # options hash which contains at least <tt>:if</tt> condition support.
1041
+ def state(*names, &block)
1042
+ options = names.last.is_a?(Hash) ? names.pop : {}
1043
+ assert_valid_keys(options, :value, :cache, :if, :human_name)
1044
+
1045
+ # Store the context so that it can be used for / matched against any state
1046
+ # that gets added
1047
+ @states.context(names, &block) if block_given?
1048
+
1049
+ if names.first.is_a?(Matcher)
1050
+ # Add any states referenced in the matcher. When matchers are used,
1051
+ # states are not allowed to be configured.
1052
+ raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1053
+ states = add_states(names.first.values)
1054
+ else
1055
+ states = add_states(names)
1056
+
1057
+ # Update the configuration for the state(s)
1058
+ states.each do |state|
1059
+ if options.include?(:value)
1060
+ state.value = options[:value]
1061
+ self.states.update(state)
1062
+ end
1063
+
1064
+ state.human_name = options[:human_name] if options.include?(:human_name)
1065
+ state.cache = options[:cache] if options.include?(:cache)
1066
+ state.matcher = options[:if] if options.include?(:if)
1067
+ end
1068
+ end
1069
+
1070
+ states.length == 1 ? states.first : states
1071
+ end
1072
+ alias_method :other_states, :state
1073
+
1074
+ # Gets the current value stored in the given object's attribute.
1075
+ #
1076
+ # For example,
1077
+ #
1078
+ # class Vehicle
1079
+ # state_machine :initial => :parked do
1080
+ # ...
1081
+ # end
1082
+ # end
1083
+ #
1084
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1085
+ # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
1086
+ # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1087
+ def read(object, attribute, ivar = false)
1088
+ attribute = self.attribute(attribute)
1089
+ if ivar
1090
+ object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1091
+ else
1092
+ object.send(attribute)
1093
+ end
1094
+ end
1095
+
1096
+ # Sets a new value in the given object's attribute.
1097
+ #
1098
+ # For example,
1099
+ #
1100
+ # class Vehicle
1101
+ # state_machine :initial => :parked do
1102
+ # ...
1103
+ # end
1104
+ # end
1105
+ #
1106
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1107
+ # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
1108
+ # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
1109
+ # vehicle.state # => "idling"
1110
+ # vehicle.event # => "park"
1111
+ def write(object, attribute, value, ivar = false)
1112
+ attribute = self.attribute(attribute)
1113
+ ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1114
+ end
1115
+
1116
+ # Defines one or more events for the machine and the transitions that can
1117
+ # be performed when those events are run.
1118
+ #
1119
+ # This method is also aliased as +on+ for improved compatibility with
1120
+ # using a domain-specific language.
1121
+ #
1122
+ # Configuration options:
1123
+ # * <tt>:human_name</tt> - The human-readable version of this event's name.
1124
+ # By default, this is either defined by the integration or stringifies the
1125
+ # name and converts underscores to spaces.
1126
+ #
1127
+ # == Instance methods
1128
+ #
1129
+ # The following instance methods are generated when a new event is defined
1130
+ # (the "park" event is used as an example):
1131
+ # * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
1132
+ # transitioning from the current state to the next valid state. If the
1133
+ # last argument is a boolean, it will control whether the machine's action
1134
+ # gets run.
1135
+ # * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
1136
+ # transitioning from the current state to the next valid state. If the
1137
+ # transition fails, then a StateMachine::InvalidTransition error will be
1138
+ # raised. If the last argument is a boolean, it will control whether the
1139
+ # machine's action gets run.
1140
+ # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event
1141
+ # can be fired given the current state of the object. This will *not* run
1142
+ # validations or callbacks in ORM integrations. It will only determine if
1143
+ # the state machine defines a valid transition for the event. To check
1144
+ # whether an event can fire *and* passes validations, use event attributes
1145
+ # (e.g. state_event) as described in the "Events" documentation of each
1146
+ # ORM integration.
1147
+ # * <tt>park_transition(requirements = {})</tt> - Gets the next transition
1148
+ # that would be performed if the "park" event were to be fired now on the
1149
+ # object or nil if no transitions can be performed. Like <tt>can_park?</tt>
1150
+ # this will also *not* run validations or callbacks. It will only
1151
+ # determine if the state machine defines a valid transition for the event.
1152
+ #
1153
+ # With a namespace of "car", the above names map to the following methods:
1154
+ # * <tt>can_park_car?</tt>
1155
+ # * <tt>park_car_transition</tt>
1156
+ # * <tt>park_car</tt>
1157
+ # * <tt>park_car!</tt>
1158
+ #
1159
+ # The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
1160
+ # optional set of requirements for determining what transitions are available
1161
+ # for the current object. These requirements include:
1162
+ # * <tt>:from</tt> - One or more states to transition from. If none are
1163
+ # specified, then this will be the object's current state.
1164
+ # * <tt>:to</tt> - One or more states to transition to. If none are
1165
+ # specified, then this will match any to state.
1166
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
1167
+ # conditionals defined for each one. Default is true.
1168
+ #
1169
+ # == Defining transitions
1170
+ #
1171
+ # +event+ requires a block which allows you to define the possible
1172
+ # transitions that can happen as a result of that event. For example,
1173
+ #
1174
+ # event :park, :stop do
1175
+ # transition :idling => :parked
1176
+ # end
1177
+ #
1178
+ # event :first_gear do
1179
+ # transition :parked => :first_gear, :if => :seatbelt_on?
1180
+ # transition :parked => same # Allow to loopback if seatbelt is off
1181
+ # end
1182
+ #
1183
+ # See StateMachine::Event#transition for more information on
1184
+ # the possible options that can be passed in.
1185
+ #
1186
+ # *Note* that this block is executed within the context of the actual event
1187
+ # object. As a result, you will not be able to reference any class methods
1188
+ # on the model without referencing the class itself. For example,
1189
+ #
1190
+ # class Vehicle
1191
+ # def self.safe_states
1192
+ # [:parked, :idling, :stalled]
1193
+ # end
1194
+ #
1195
+ # state_machine do
1196
+ # event :park do
1197
+ # transition Vehicle.safe_states => :parked
1198
+ # end
1199
+ # end
1200
+ # end
1201
+ #
1202
+ # == Overriding the event method
1203
+ #
1204
+ # By default, this will define an instance method (with the same name as the
1205
+ # event) that will fire the next possible transition for that. Although the
1206
+ # +before_transition+, +after_transition+, and +around_transition+ hooks
1207
+ # allow you to define behavior that gets executed as a result of the event's
1208
+ # transition, you can also override the event method in order to have a
1209
+ # little more fine-grained control.
1210
+ #
1211
+ # For example:
1212
+ #
1213
+ # class Vehicle
1214
+ # state_machine do
1215
+ # event :park do
1216
+ # ...
1217
+ # end
1218
+ # end
1219
+ #
1220
+ # def park(*)
1221
+ # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
1222
+ # if result = super # Runs the transition and all before/after/around hooks
1223
+ # applaud # Executes after the transition (and after_transition hooks)
1224
+ # end
1225
+ # result
1226
+ # end
1227
+ # end
1228
+ #
1229
+ # There are a few important things to note here. First, the method
1230
+ # signature is defined with an unlimited argument list in order to allow
1231
+ # callers to continue passing arguments that are expected by state_machine.
1232
+ # For example, it will still allow calls to +park+ with a single parameter
1233
+ # for skipping the configured action.
1234
+ #
1235
+ # Second, the overridden event method must call +super+ in order to run the
1236
+ # logic for running the next possible transition. In order to remain
1237
+ # consistent with other events, the result of +super+ is returned.
1238
+ #
1239
+ # Third, any behavior defined in this method will *not* get executed if
1240
+ # you're taking advantage of attribute-based event transitions. For example:
1241
+ #
1242
+ # vehicle = Vehicle.new
1243
+ # vehicle.state_event = 'park'
1244
+ # vehicle.save
1245
+ #
1246
+ # In this case, the +park+ event will run the before/after/around transition
1247
+ # hooks and transition the state, but the behavior defined in the overriden
1248
+ # +park+ method will *not* be executed.
1249
+ #
1250
+ # == Defining additional arguments
1251
+ #
1252
+ # Additional arguments can be passed into events and accessed by transition
1253
+ # hooks like so:
1254
+ #
1255
+ # class Vehicle
1256
+ # state_machine do
1257
+ # after_transition :on => :park do |vehicle, transition|
1258
+ # kind = *transition.args # :parallel
1259
+ # ...
1260
+ # end
1261
+ # after_transition :on => :park, :do => :take_deep_breath
1262
+ #
1263
+ # event :park do
1264
+ # ...
1265
+ # end
1266
+ #
1267
+ # def take_deep_breath(transition)
1268
+ # kind = *transition.args # :parallel
1269
+ # ...
1270
+ # end
1271
+ # end
1272
+ # end
1273
+ #
1274
+ # vehicle = Vehicle.new
1275
+ # vehicle.park(:parallel)
1276
+ #
1277
+ # *Remember* that if the last argument is a boolean, it will be used as the
1278
+ # +run_action+ parameter to the event action. Using the +park+ action
1279
+ # example from above, you can might call it like so:
1280
+ #
1281
+ # vehicle.park # => Uses default args and runs machine action
1282
+ # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
1283
+ # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
1284
+ #
1285
+ # If you decide to override the +park+ event method *and* define additional
1286
+ # arguments, you can do so as shown below:
1287
+ #
1288
+ # class Vehicle
1289
+ # state_machine do
1290
+ # event :park do
1291
+ # ...
1292
+ # end
1293
+ # end
1294
+ #
1295
+ # def park(kind = :parallel, *args)
1296
+ # take_deep_breath if kind == :parallel
1297
+ # super
1298
+ # end
1299
+ # end
1300
+ #
1301
+ # Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
1302
+ # the entire arguments list to be accessed by transition callbacks through
1303
+ # StateMachine::Transition#args.
1304
+ #
1305
+ # === Using matchers
1306
+ #
1307
+ # The +all+ / +any+ matchers can be used to easily execute blocks for a
1308
+ # group of events. Note, however, that you cannot use these matchers to
1309
+ # set configurations for events. Blocks using these matchers can be
1310
+ # defined at any point in the state machine and will always get applied to
1311
+ # the proper events.
1312
+ #
1313
+ # For example:
1314
+ #
1315
+ # state_machine :initial => :parked do
1316
+ # ...
1317
+ #
1318
+ # event all - [:crash] do
1319
+ # transition :stalled => :parked
1320
+ # end
1321
+ # end
1322
+ #
1323
+ # == Example
1324
+ #
1325
+ # class Vehicle
1326
+ # state_machine do
1327
+ # # The park, stop, and halt events will all share the given transitions
1328
+ # event :park, :stop, :halt do
1329
+ # transition [:idling, :backing_up] => :parked
1330
+ # end
1331
+ #
1332
+ # event :stop do
1333
+ # transition :first_gear => :idling
1334
+ # end
1335
+ #
1336
+ # event :ignite do
1337
+ # transition :parked => :idling
1338
+ # transition :idling => same # Allow ignite while still idling
1339
+ # end
1340
+ # end
1341
+ # end
1342
+ def event(*names, &block)
1343
+ options = names.last.is_a?(Hash) ? names.pop : {}
1344
+ assert_valid_keys(options, :human_name)
1345
+
1346
+ # Store the context so that it can be used for / matched against any event
1347
+ # that gets added
1348
+ @events.context(names, &block) if block_given?
1349
+
1350
+ if names.first.is_a?(Matcher)
1351
+ # Add any events referenced in the matcher. When matchers are used,
1352
+ # events are not allowed to be configured.
1353
+ raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1354
+ events = add_events(names.first.values)
1355
+ else
1356
+ events = add_events(names)
1357
+
1358
+ # Update the configuration for the event(s)
1359
+ events.each do |event|
1360
+ event.human_name = options[:human_name] if options.include?(:human_name)
1361
+
1362
+ # Add any states that may have been referenced within the event
1363
+ add_states(event.known_states)
1364
+ end
1365
+ end
1366
+
1367
+ events.length == 1 ? events.first : events
1368
+ end
1369
+ alias_method :on, :event
1370
+
1371
+ # Creates a new transition that determines what to change the current state
1372
+ # to when an event fires.
1373
+ #
1374
+ # == Defining transitions
1375
+ #
1376
+ # The options for a new transition uses the Hash syntax to map beginning
1377
+ # states to ending states. For example,
1378
+ #
1379
+ # transition :parked => :idling, :idling => :first_gear, :on => :ignite
1380
+ #
1381
+ # In this case, when the +ignite+ event is fired, this transition will cause
1382
+ # the state to be +idling+ if it's current state is +parked+ or +first_gear+
1383
+ # if it's current state is +idling+.
1384
+ #
1385
+ # To help define these implicit transitions, a set of helpers are available
1386
+ # for slightly more complex matching:
1387
+ # * <tt>all</tt> - Matches every state in the machine
1388
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
1389
+ # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
1390
+ # * <tt>same</tt> - Matches the same state being transitioned from
1391
+ #
1392
+ # See StateMachine::MatcherHelpers for more information.
1393
+ #
1394
+ # Examples:
1395
+ #
1396
+ # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
1397
+ # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
1398
+ # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
1399
+ # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
1400
+ # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
1401
+ # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
1402
+ #
1403
+ # transition :parked => same, :on => :park # Loops :parked back to :parked
1404
+ # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
1405
+ # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
1406
+ #
1407
+ # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
1408
+ # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
1409
+ #
1410
+ # == Verbose transitions
1411
+ #
1412
+ # Transitions can also be defined use an explicit set of configuration
1413
+ # options:
1414
+ # * <tt>:from</tt> - A state or array of states that can be transitioned from.
1415
+ # If not specified, then the transition can occur for *any* state.
1416
+ # * <tt>:to</tt> - The state that's being transitioned to. If not specified,
1417
+ # then the transition will simply loop back (i.e. the state will not change).
1418
+ # * <tt>:except_from</tt> - A state or array of states that *cannot* be
1419
+ # transitioned from.
1420
+ #
1421
+ # These options must be used when defining transitions within the context
1422
+ # of a state.
1423
+ #
1424
+ # Examples:
1425
+ #
1426
+ # transition :to => nil, :on => :park
1427
+ # transition :to => :idling, :on => :ignite
1428
+ # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
1429
+ # transition :from => nil, :to => :idling, :on => :ignite
1430
+ # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
1431
+ #
1432
+ # == Conditions
1433
+ #
1434
+ # In addition to the state requirements for each transition, a condition
1435
+ # can also be defined to help determine whether that transition is
1436
+ # available. These options will work on both the normal and verbose syntax.
1437
+ #
1438
+ # Configuration options:
1439
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
1440
+ # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
1441
+ # The condition should return or evaluate to true or false.
1442
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1443
+ # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
1444
+ # The condition should return or evaluate to true or false.
1445
+ #
1446
+ # Examples:
1447
+ #
1448
+ # transition :parked => :idling, :on => :ignite, :if => :moving?
1449
+ # transition :parked => :idling, :on => :ignite, :unless => :stopped?
1450
+ # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
1451
+ #
1452
+ # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
1453
+ # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
1454
+ #
1455
+ # == Order of operations
1456
+ #
1457
+ # Transitions are evaluated in the order in which they're defined. As a
1458
+ # result, if more than one transition applies to a given object, then the
1459
+ # first transition that matches will be performed.
1460
+ def transition(options)
1461
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
1462
+
1463
+ branches = []
1464
+ options = options.dup
1465
+ event(*Array(options.delete(:on))) { branches << transition(options) }
1466
+
1467
+ branches.length == 1 ? branches.first : branches
1468
+ end
1469
+
1470
+ # Creates a callback that will be invoked *before* a transition is
1471
+ # performed so long as the given requirements match the transition.
1472
+ #
1473
+ # == The callback
1474
+ #
1475
+ # Callbacks must be defined as either an argument, in the :do option, or
1476
+ # as a block. For example,
1477
+ #
1478
+ # class Vehicle
1479
+ # state_machine do
1480
+ # before_transition :set_alarm
1481
+ # before_transition :set_alarm, all => :parked
1482
+ # before_transition all => :parked, :do => :set_alarm
1483
+ # before_transition all => :parked do |vehicle, transition|
1484
+ # vehicle.set_alarm
1485
+ # end
1486
+ # ...
1487
+ # end
1488
+ # end
1489
+ #
1490
+ # Notice that the first three callbacks are the same in terms of how the
1491
+ # methods to invoke are defined. However, using the <tt>:do</tt> can
1492
+ # provide for a more fluid DSL.
1493
+ #
1494
+ # In addition, multiple callbacks can be defined like so:
1495
+ #
1496
+ # class Vehicle
1497
+ # state_machine do
1498
+ # before_transition :set_alarm, :lock_doors, all => :parked
1499
+ # before_transition all => :parked, :do => [:set_alarm, :lock_doors]
1500
+ # before_transition :set_alarm do |vehicle, transition|
1501
+ # vehicle.lock_doors
1502
+ # end
1503
+ # end
1504
+ # end
1505
+ #
1506
+ # Notice that the different ways of configuring methods can be mixed.
1507
+ #
1508
+ # == State requirements
1509
+ #
1510
+ # Callbacks can require that the machine be transitioning from and to
1511
+ # specific states. These requirements use a Hash syntax to map beginning
1512
+ # states to ending states. For example,
1513
+ #
1514
+ # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1515
+ #
1516
+ # In this case, the +set_alarm+ callback will only be called if the machine
1517
+ # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1518
+ #
1519
+ # To help define state requirements, a set of helpers are available for
1520
+ # slightly more complex matching:
1521
+ # * <tt>all</tt> - Matches every state/event in the machine
1522
+ # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1523
+ # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1524
+ # * <tt>same</tt> - Matches the same state being transitioned from
1525
+ #
1526
+ # See StateMachine::MatcherHelpers for more information.
1527
+ #
1528
+ # Examples:
1529
+ #
1530
+ # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1531
+ # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1532
+ # before_transition all => :parked, :do => ... # Matches all states to parked
1533
+ # before_transition any => same, :do => ... # Matches every loopback
1534
+ #
1535
+ # == Event requirements
1536
+ #
1537
+ # In addition to state requirements, an event requirement can be defined so
1538
+ # that the callback is only invoked on specific events using the +on+
1539
+ # option. This can also use the same matcher helpers as the state
1540
+ # requirements.
1541
+ #
1542
+ # Examples:
1543
+ #
1544
+ # before_transition :on => :ignite, :do => ... # Matches only on ignite
1545
+ # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1546
+ # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1547
+ #
1548
+ # == Verbose Requirements
1549
+ #
1550
+ # Requirements can also be defined using verbose options rather than the
1551
+ # implicit Hash syntax and helper methods described above.
1552
+ #
1553
+ # Configuration options:
1554
+ # * <tt>:from</tt> - One or more states being transitioned from. If none
1555
+ # are specified, then all states will match.
1556
+ # * <tt>:to</tt> - One or more states being transitioned to. If none are
1557
+ # specified, then all states will match.
1558
+ # * <tt>:on</tt> - One or more events that fired the transition. If none
1559
+ # are specified, then all events will match.
1560
+ # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1561
+ # * <tt>:except_to</tt> - One more states *not* being transitioned to
1562
+ # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1563
+ #
1564
+ # Examples:
1565
+ #
1566
+ # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1567
+ # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1568
+ #
1569
+ # == Conditions
1570
+ #
1571
+ # In addition to the state/event requirements, a condition can also be
1572
+ # defined to help determine whether the callback should be invoked.
1573
+ #
1574
+ # Configuration options:
1575
+ # * <tt>:if</tt> - A method, proc or string to call to determine if the
1576
+ # callback should occur (e.g. :if => :allow_callbacks, or
1577
+ # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1578
+ # should return or evaluate to a true or false value.
1579
+ # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1580
+ # callback should not occur (e.g. :unless => :skip_callbacks, or
1581
+ # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1582
+ # string should return or evaluate to a true or false value.
1583
+ #
1584
+ # Examples:
1585
+ #
1586
+ # before_transition :parked => :idling, :if => :moving?, :do => ...
1587
+ # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1588
+ #
1589
+ # == Accessing the transition
1590
+ #
1591
+ # In addition to passing the object being transitioned, the actual
1592
+ # transition describing the context (e.g. event, from, to) can be accessed
1593
+ # as well. This additional argument is only passed if the callback allows
1594
+ # for it.
1595
+ #
1596
+ # For example,
1597
+ #
1598
+ # class Vehicle
1599
+ # # Only specifies one parameter (the object being transitioned)
1600
+ # before_transition all => :parked do |vehicle|
1601
+ # vehicle.set_alarm
1602
+ # end
1603
+ #
1604
+ # # Specifies 2 parameters (object being transitioned and actual transition)
1605
+ # before_transition all => :parked do |vehicle, transition|
1606
+ # vehicle.set_alarm(transition)
1607
+ # end
1608
+ # end
1609
+ #
1610
+ # *Note* that the object in the callback will only be passed in as an
1611
+ # argument if callbacks are configured to *not* be bound to the object
1612
+ # involved. This is the default and may change on a per-integration basis.
1613
+ #
1614
+ # See StateMachine::Transition for more information about the
1615
+ # attributes available on the transition.
1616
+ #
1617
+ # == Usage with delegates
1618
+ #
1619
+ # As noted above, state_machine uses the callback method's argument list
1620
+ # arity to determine whether to include the transition in the method call.
1621
+ # If you're using delegates, such as those defined in ActiveSupport or
1622
+ # Forwardable, the actual arity of the delegated method gets masked. This
1623
+ # means that callbacks which reference delegates will always get passed the
1624
+ # transition as an argument. For example:
1625
+ #
1626
+ # class Vehicle
1627
+ # extend Forwardable
1628
+ # delegate :refresh => :dashboard
1629
+ #
1630
+ # state_machine do
1631
+ # before_transition :refresh
1632
+ # ...
1633
+ # end
1634
+ #
1635
+ # def dashboard
1636
+ # @dashboard ||= Dashboard.new
1637
+ # end
1638
+ # end
1639
+ #
1640
+ # class Dashboard
1641
+ # def refresh(transition)
1642
+ # # ...
1643
+ # end
1644
+ # end
1645
+ #
1646
+ # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
1647
+ # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
1648
+ # raised. The only way around this is to avoid the use of delegates and
1649
+ # manually define the delegate method so that the correct arity is used.
1650
+ #
1651
+ # == Examples
1652
+ #
1653
+ # Below is an example of a class with one state machine and various types
1654
+ # of +before+ transitions defined for it:
1655
+ #
1656
+ # class Vehicle
1657
+ # state_machine do
1658
+ # # Before all transitions
1659
+ # before_transition :update_dashboard
1660
+ #
1661
+ # # Before specific transition:
1662
+ # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1663
+ #
1664
+ # # With conditional callback:
1665
+ # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1666
+ #
1667
+ # # Using helpers:
1668
+ # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1669
+ # ...
1670
+ # end
1671
+ # end
1672
+ #
1673
+ # As can be seen, any number of transitions can be created using various
1674
+ # combinations of configuration options.
1675
+ def before_transition(*args, &block)
1676
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1677
+ options[:do] = args if args.any?
1678
+ add_callback(:before, options, &block)
1679
+ end
1680
+
1681
+ # Creates a callback that will be invoked *after* a transition is
1682
+ # performed so long as the given requirements match the transition.
1683
+ #
1684
+ # See +before_transition+ for a description of the possible configurations
1685
+ # for defining callbacks.
1686
+ def after_transition(*args, &block)
1687
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1688
+ options[:do] = args if args.any?
1689
+ add_callback(:after, options, &block)
1690
+ end
1691
+
1692
+ # Creates a callback that will be invoked *around* a transition so long as
1693
+ # the given requirements match the transition.
1694
+ #
1695
+ # == The callback
1696
+ #
1697
+ # Around callbacks wrap transitions, executing code both before and after.
1698
+ # These callbacks are defined in the exact same manner as before / after
1699
+ # callbacks with the exception that the transition must be yielded to in
1700
+ # order to finish running it.
1701
+ #
1702
+ # If defining +around+ callbacks using blocks, you must yield within the
1703
+ # transition by directly calling the block (since yielding is not allowed
1704
+ # within blocks).
1705
+ #
1706
+ # For example,
1707
+ #
1708
+ # class Vehicle
1709
+ # state_machine do
1710
+ # around_transition do |block|
1711
+ # Benchmark.measure { block.call }
1712
+ # end
1713
+ #
1714
+ # around_transition do |vehicle, block|
1715
+ # logger.info "vehicle was #{state}..."
1716
+ # block.call
1717
+ # logger.info "...and is now #{state}"
1718
+ # end
1719
+ #
1720
+ # around_transition do |vehicle, transition, block|
1721
+ # logger.info "before #{transition.event}: #{vehicle.state}"
1722
+ # block.call
1723
+ # logger.info "after #{transition.event}: #{vehicle.state}"
1724
+ # end
1725
+ # end
1726
+ # end
1727
+ #
1728
+ # Notice that referencing the block is similar to doing so within an
1729
+ # actual method definition in that it is always the last argument.
1730
+ #
1731
+ # On the other hand, if you're defining +around+ callbacks using method
1732
+ # references, you can yield like normal:
1733
+ #
1734
+ # class Vehicle
1735
+ # state_machine do
1736
+ # around_transition :benchmark
1737
+ # ...
1738
+ # end
1739
+ #
1740
+ # def benchmark
1741
+ # Benchmark.measure { yield }
1742
+ # end
1743
+ # end
1744
+ #
1745
+ # See +before_transition+ for a description of the possible configurations
1746
+ # for defining callbacks.
1747
+ def around_transition(*args, &block)
1748
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1749
+ options[:do] = args if args.any?
1750
+ add_callback(:around, options, &block)
1751
+ end
1752
+
1753
+ # Creates a callback that will be invoked *after* a transition failures to
1754
+ # be performed so long as the given requirements match the transition.
1755
+ #
1756
+ # See +before_transition+ for a description of the possible configurations
1757
+ # for defining callbacks. *Note* however that you cannot define the state
1758
+ # requirements in these callbacks. You may only define event requirements.
1759
+ #
1760
+ # = The callback
1761
+ #
1762
+ # Failure callbacks get invoked whenever an event fails to execute. This
1763
+ # can happen when no transition is available, a +before+ callback halts
1764
+ # execution, or the action associated with this machine fails to succeed.
1765
+ # In any of these cases, any failure callback that matches the attempted
1766
+ # transition will be run.
1767
+ #
1768
+ # For example,
1769
+ #
1770
+ # class Vehicle
1771
+ # state_machine do
1772
+ # after_failure do |vehicle, transition|
1773
+ # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
1774
+ # end
1775
+ #
1776
+ # after_failure :on => :ignite, :do => :log_ignition_failure
1777
+ #
1778
+ # ...
1779
+ # end
1780
+ # end
1781
+ def after_failure(*args, &block)
1782
+ options = (args.last.is_a?(Hash) ? args.pop : {})
1783
+ options[:do] = args if args.any?
1784
+ assert_valid_keys(options, :on, :do, :if, :unless)
1785
+
1786
+ add_callback(:failure, options, &block)
1787
+ end
1788
+
1789
+ # Generates a list of the possible transition sequences that can be run on
1790
+ # the given object. These paths can reveal all of the possible states and
1791
+ # events that can be encountered in the object's state machine based on the
1792
+ # object's current state.
1793
+ #
1794
+ # Configuration options:
1795
+ # * +from+ - The initial state to start all paths from. By default, this
1796
+ # is the object's current state.
1797
+ # * +to+ - The target state to end all paths on. By default, paths will
1798
+ # end when they loop back to the first transition on the path.
1799
+ # * +deep+ - Whether to allow the target state to be crossed more than once
1800
+ # in a path. By default, paths will immediately stop when the target
1801
+ # state (if specified) is reached. If this is enabled, then paths can
1802
+ # continue even after reaching the target state; they will stop when
1803
+ # reaching the target state a second time.
1804
+ #
1805
+ # *Note* that the object is never modified when the list of paths is
1806
+ # generated.
1807
+ #
1808
+ # == Examples
1809
+ #
1810
+ # class Vehicle
1811
+ # state_machine :initial => :parked do
1812
+ # event :ignite do
1813
+ # transition :parked => :idling
1814
+ # end
1815
+ #
1816
+ # event :shift_up do
1817
+ # transition :idling => :first_gear, :first_gear => :second_gear
1818
+ # end
1819
+ #
1820
+ # event :shift_down do
1821
+ # transition :second_gear => :first_gear, :first_gear => :idling
1822
+ # end
1823
+ # end
1824
+ # end
1825
+ #
1826
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
1827
+ # vehicle.state # => "parked"
1828
+ #
1829
+ # vehicle.state_paths
1830
+ # # => [
1831
+ # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1832
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1833
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
1834
+ # # #<StateMachine::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
1835
+ # # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
1836
+ # #
1837
+ # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1838
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1839
+ # # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
1840
+ # # ]
1841
+ #
1842
+ # vehicle.state_paths(:from => :parked, :to => :second_gear)
1843
+ # # => [
1844
+ # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1845
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1846
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
1847
+ # # ]
1848
+ #
1849
+ # In addition to getting the possible paths that can be accessed, you can
1850
+ # also get summary information about the states / events that can be
1851
+ # accessed at some point along one of the paths. For example:
1852
+ #
1853
+ # # Get the list of states that can be accessed from the current state
1854
+ # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
1855
+ #
1856
+ # # Get the list of events that can be accessed from the current state
1857
+ # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
1858
+ def paths_for(object, requirements = {})
1859
+ PathCollection.new(object, self, requirements)
1860
+ end
1861
+
1862
+ # Marks the given object as invalid with the given message.
1863
+ #
1864
+ # By default, this is a no-op.
1865
+ def invalidate(object, attribute, message, values = [])
1866
+ end
1867
+
1868
+ # Gets a description of the errors for the given object. This is used to
1869
+ # provide more detailed information when an InvalidTransition exception is
1870
+ # raised.
1871
+ def errors_for(object)
1872
+ ''
1873
+ end
1874
+
1875
+ # Resets any errors previously added when invalidating the given object.
1876
+ #
1877
+ # By default, this is a no-op.
1878
+ def reset(object)
1879
+ end
1880
+
1881
+ # Generates the message to use when invalidating the given object after
1882
+ # failing to transition on a specific event
1883
+ def generate_message(name, values = [])
1884
+ message = (@messages[name] || self.class.default_messages[name])
1885
+
1886
+ # Check whether there are actually any values to interpolate to avoid
1887
+ # any warnings
1888
+ if message.scan(/%./).any? {|match| match != '%%'}
1889
+ message % values.map {|value| value.last}
1890
+ else
1891
+ message
1892
+ end
1893
+ end
1894
+
1895
+ # Runs a transaction, rolling back any changes if the yielded block fails.
1896
+ #
1897
+ # This is only applicable to integrations that involve databases. By
1898
+ # default, this will not run any transactions since the changes aren't
1899
+ # taking place within the context of a database.
1900
+ def within_transaction(object)
1901
+ if use_transactions
1902
+ transaction(object) { yield }
1903
+ else
1904
+ yield
1905
+ end
1906
+ end
1907
+
1908
+ # Draws a directed graph of the machine for visualizing the various events,
1909
+ # states, and their transitions.
1910
+ #
1911
+ # This requires both the Ruby graphviz gem and the graphviz library be
1912
+ # installed on the system.
1913
+ #
1914
+ # Configuration options:
1915
+ # * <tt>:name</tt> - The name of the file to write to (without the file extension).
1916
+ # Default is "#{owner_class.name}_#{name}"
1917
+ # * <tt>:path</tt> - The path to write the graph file to. Default is the
1918
+ # current directory (".").
1919
+ # * <tt>:format</tt> - The image format to generate the graph in.
1920
+ # Default is "png'.
1921
+ # * <tt>:font</tt> - The name of the font to draw state names in.
1922
+ # Default is "Arial".
1923
+ # * <tt>:orientation</tt> - The direction of the graph ("portrait" or
1924
+ # "landscape"). Default is "portrait".
1925
+ # * <tt>:human_names</tt> - Whether to use human state / event names for
1926
+ # node labels on the graph instead of the internal name. Default is false.
1927
+ def draw(graph_options = {})
1928
+ name = graph_options.delete(:name) || "#{owner_class.name}_#{self.name}"
1929
+ draw_options = {:human_name => false}
1930
+ draw_options[:human_name] = graph_options.delete(:human_names) if graph_options.include?(:human_names)
1931
+
1932
+ graph = Graph.new(name, graph_options)
1933
+
1934
+ # Add nodes / edges
1935
+ states.by_priority.each {|state| state.draw(graph, draw_options)}
1936
+ events.each {|event| event.draw(graph, draw_options)}
1937
+
1938
+ # Output result
1939
+ graph.output
1940
+ graph
1941
+ end
1942
+
1943
+ # Determines whether an action hook was defined for firing attribute-based
1944
+ # event transitions when the configured action gets called.
1945
+ def action_hook?(self_only = false)
1946
+ @action_hook_defined || !self_only && owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self && machine.action_hook?(true)}
1947
+ end
1948
+
1949
+ protected
1950
+ # Runs additional initialization hooks. By default, this is a no-op.
1951
+ def after_initialize
1952
+ end
1953
+
1954
+ # Looks up other machines that have been defined in the owner class and
1955
+ # are targeting the same attribute as this machine. When accessing
1956
+ # sibling machines, they will be automatically copied for the current
1957
+ # class if they haven't been already. This ensures that any configuration
1958
+ # changes made to the sibling machines only affect this class and not any
1959
+ # base class that may have originally defined the machine.
1960
+ def sibling_machines
1961
+ owner_class.state_machines.inject([]) do |machines, (name, machine)|
1962
+ if machine.attribute == attribute && machine != self
1963
+ machines << (owner_class.state_machine(name) {})
1964
+ end
1965
+ machines
1966
+ end
1967
+ end
1968
+
1969
+ # Determines if the machine's attribute needs to be initialized. This
1970
+ # will only be true if the machine's attribute is blank.
1971
+ def initialize_state?(object)
1972
+ value = read(object, :state)
1973
+ (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
1974
+ end
1975
+
1976
+ # Adds helper methods for interacting with the state machine, including
1977
+ # for states, events, and transitions
1978
+ def define_helpers
1979
+ define_state_accessor
1980
+ define_state_predicate
1981
+ define_event_helpers
1982
+ define_path_helpers
1983
+ define_action_helpers if define_action_helpers?
1984
+ define_name_helpers
1985
+ end
1986
+
1987
+ # Defines the initial values for state machine attributes. Static values
1988
+ # are set prior to the original initialize method and dynamic values are
1989
+ # set *after* the initialize method in case it is dependent on it.
1990
+ def define_state_initializer
1991
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1992
+ def initialize(*)
1993
+ self.class.state_machines.initialize_states(self) { super }
1994
+ end
1995
+ end_eval
1996
+ end
1997
+
1998
+ # Adds reader/writer methods for accessing the state attribute
1999
+ def define_state_accessor
2000
+ attribute = self.attribute
2001
+
2002
+ @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
2003
+ @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
2004
+ end
2005
+
2006
+ # Adds predicate method to the owner class for determining the name of the
2007
+ # current state
2008
+ def define_state_predicate
2009
+ call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
2010
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2011
+ def #{name}?(*args)
2012
+ args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
2013
+ end
2014
+ end_eval
2015
+ end
2016
+
2017
+ # Adds helper methods for getting information about this state machine's
2018
+ # events
2019
+ def define_event_helpers
2020
+ # Gets the events that are allowed to fire on the current object
2021
+ define_helper(:instance, attribute(:events)) do |machine, object, *args|
2022
+ machine.events.valid_for(object, *args).map {|event| event.name}
2023
+ end
2024
+
2025
+ # Gets the next possible transitions that can be run on the current
2026
+ # object
2027
+ define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
2028
+ machine.events.transitions_for(object, *args)
2029
+ end
2030
+
2031
+ # Fire an arbitrary event for this machine
2032
+ define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
2033
+ machine.events.fetch(event).fire(object, *args)
2034
+ end
2035
+
2036
+ # Add helpers for tracking the event / transition to invoke when the
2037
+ # action is called
2038
+ if action
2039
+ event_attribute = attribute(:event)
2040
+ define_helper(:instance, event_attribute) do |machine, object|
2041
+ # Interpret non-blank events as present
2042
+ event = machine.read(object, :event, true)
2043
+ event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
2044
+ end
2045
+
2046
+ # A roundabout way of writing the attribute is used here so that
2047
+ # integrations can hook into this modification
2048
+ define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
2049
+ machine.write(object, :event, value, true)
2050
+ end
2051
+
2052
+ event_transition_attribute = attribute(:event_transition)
2053
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2054
+ protected; attr_accessor #{event_transition_attribute.inspect}
2055
+ end_eval
2056
+ end
2057
+ end
2058
+
2059
+ # Adds helper methods for getting information about this state machine's
2060
+ # available transition paths
2061
+ def define_path_helpers
2062
+ # Gets the paths of transitions available to the current object
2063
+ define_helper(:instance, attribute(:paths)) do |machine, object, *args|
2064
+ machine.paths_for(object, *args)
2065
+ end
2066
+ end
2067
+
2068
+ # Determines whether action helpers should be defined for this machine.
2069
+ # This is only true if there is an action configured and no other machines
2070
+ # have process this same configuration already.
2071
+ def define_action_helpers?
2072
+ action && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
2073
+ end
2074
+
2075
+ # Adds helper methods for automatically firing events when an action
2076
+ # is invoked
2077
+ def define_action_helpers
2078
+ if action_hook
2079
+ @action_hook_defined = true
2080
+ define_action_hook
2081
+ end
2082
+ end
2083
+
2084
+ # Hooks directly into actions by defining the same method in an included
2085
+ # module. As a result, when the action gets invoked, any state events
2086
+ # defined for the object will get run. Method visibility is preserved.
2087
+ def define_action_hook
2088
+ action_hook = self.action_hook
2089
+ action = self.action
2090
+ private_action_hook = owner_class.private_method_defined?(action_hook)
2091
+
2092
+ # Only define helper if it hasn't
2093
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2094
+ def #{action_hook}(*)
2095
+ self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
2096
+ end
2097
+
2098
+ private #{action_hook.inspect} if #{private_action_hook}
2099
+ end_eval
2100
+ end
2101
+
2102
+ # The method to hook into for triggering transitions when invoked. By
2103
+ # default, this is the action configured for the machine.
2104
+ #
2105
+ # Since the default hook technique relies on module inheritance, the
2106
+ # action must be defined in an ancestor of the owner classs in order for
2107
+ # it to be the action hook.
2108
+ def action_hook
2109
+ action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
2110
+ end
2111
+
2112
+ # Determines whether there's already a helper method defined within the
2113
+ # given scope. This is true only if one of the owner's ancestors defines
2114
+ # the method and is further along in the ancestor chain than this
2115
+ # machine's helper module.
2116
+ def owner_class_ancestor_has_method?(scope, method)
2117
+ superclasses = owner_class.ancestors[1..-1].select {|ancestor| ancestor.is_a?(Class)}
2118
+
2119
+ if scope == :class
2120
+ # Use singleton classes
2121
+ current = (class << owner_class; self; end)
2122
+ superclass = superclasses.first
2123
+ else
2124
+ current = owner_class
2125
+ superclass = owner_class.superclass
2126
+ end
2127
+
2128
+ # Generate the list of modules that *only* occur in the owner class, but
2129
+ # were included *prior* to the helper modules, in addition to the
2130
+ # superclasses
2131
+ ancestors = current.ancestors - superclass.ancestors + superclasses
2132
+ ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
2133
+
2134
+ # Search for for the first ancestor that defined this method
2135
+ ancestors.detect do |ancestor|
2136
+ ancestor = (class << ancestor; self; end) if scope == :class && ancestor.is_a?(Class)
2137
+ ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2138
+ end
2139
+ end
2140
+
2141
+ # Adds helper methods for accessing naming information about states and
2142
+ # events on the owner class
2143
+ def define_name_helpers
2144
+ # Gets the humanized version of a state
2145
+ define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
2146
+ machine.states.fetch(state).human_name(klass)
2147
+ end
2148
+
2149
+ # Gets the humanized version of an event
2150
+ define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
2151
+ machine.events.fetch(event).human_name(klass)
2152
+ end
2153
+
2154
+ # Gets the state name for the current value
2155
+ define_helper(:instance, attribute(:name)) do |machine, object|
2156
+ machine.states.match!(object).name
2157
+ end
2158
+
2159
+ # Gets the human state name for the current value
2160
+ define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
2161
+ machine.states.match!(object).human_name(object.class)
2162
+ end
2163
+ end
2164
+
2165
+ # Defines the with/without scope helpers for this attribute. Both the
2166
+ # singular and plural versions of the attribute are defined for each
2167
+ # scope helper. A custom plural can be specified if it cannot be
2168
+ # automatically determined by either calling +pluralize+ on the attribute
2169
+ # name or adding an "s" to the end of the name.
2170
+ def define_scopes(custom_plural = nil)
2171
+ plural = custom_plural || pluralize(name)
2172
+
2173
+ [:with, :without].each do |kind|
2174
+ [name, plural].map {|s| s.to_s}.uniq.each do |suffix|
2175
+ method = "#{kind}_#{suffix}"
2176
+
2177
+ if scope = send("create_#{kind}_scope", method)
2178
+ # Converts state names to their corresponding values so that they
2179
+ # can be looked up properly
2180
+ define_helper(:class, method) do |machine, klass, *states|
2181
+ run_scope(scope, machine, klass, states)
2182
+ end
2183
+ end
2184
+ end
2185
+ end
2186
+ end
2187
+
2188
+ # Generates the results for the given scope based on one or more states to
2189
+ # filter by
2190
+ def run_scope(scope, machine, klass, states)
2191
+ values = states.flatten.map {|state| machine.states.fetch(state).value}
2192
+ scope.call(klass, values)
2193
+ end
2194
+
2195
+ # Pluralizes the given word using #pluralize (if available) or simply
2196
+ # adding an "s" to the end of the word
2197
+ def pluralize(word)
2198
+ word = word.to_s
2199
+ if word.respond_to?(:pluralize)
2200
+ word.pluralize
2201
+ else
2202
+ "#{name}s"
2203
+ end
2204
+ end
2205
+
2206
+ # Creates a scope for finding objects *with* a particular value or values
2207
+ # for the attribute.
2208
+ #
2209
+ # By default, this is a no-op.
2210
+ def create_with_scope(name)
2211
+ end
2212
+
2213
+ # Creates a scope for finding objects *without* a particular value or
2214
+ # values for the attribute.
2215
+ #
2216
+ # By default, this is a no-op.
2217
+ def create_without_scope(name)
2218
+ end
2219
+
2220
+ # Always yields
2221
+ def transaction(object)
2222
+ yield
2223
+ end
2224
+
2225
+ # Gets the initial attribute value defined by the owner class (outside of
2226
+ # the machine's definition). By default, this is always nil.
2227
+ def owner_class_attribute_default
2228
+ nil
2229
+ end
2230
+
2231
+ # Checks whether the given state matches the attribute default specified
2232
+ # by the owner class
2233
+ def owner_class_attribute_default_matches?(state)
2234
+ state.matches?(owner_class_attribute_default)
2235
+ end
2236
+
2237
+ # Updates this machine based on the configuration of other machines in the
2238
+ # owner class that share the same target attribute.
2239
+ def add_sibling_machine_configs
2240
+ # Add existing states
2241
+ sibling_machines.each do |machine|
2242
+ machine.states.each {|state| states << state unless states[state.name]}
2243
+ end
2244
+ end
2245
+
2246
+ # Adds a new transition callback of the given type.
2247
+ def add_callback(type, options, &block)
2248
+ callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
2249
+ add_states(callback.known_states)
2250
+ callback
2251
+ end
2252
+
2253
+ # Tracks the given set of states in the list of all known states for
2254
+ # this machine
2255
+ def add_states(new_states)
2256
+ new_states.map do |new_state|
2257
+ # Check for other states that use a different class type for their name.
2258
+ # This typically prevents string / symbol misuse.
2259
+ if new_state && conflict = states.detect {|state| state.name && state.name.class != new_state.class}
2260
+ raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
2261
+ end
2262
+
2263
+ unless state = states[new_state]
2264
+ states << state = State.new(self, new_state)
2265
+
2266
+ # Copy states over to sibling machines
2267
+ sibling_machines.each {|machine| machine.states << state}
2268
+ end
2269
+
2270
+ state
2271
+ end
2272
+ end
2273
+
2274
+ # Tracks the given set of events in the list of all known events for
2275
+ # this machine
2276
+ def add_events(new_events)
2277
+ new_events.map do |new_event|
2278
+ # Check for other states that use a different class type for their name.
2279
+ # This typically prevents string / symbol misuse.
2280
+ if conflict = events.detect {|event| event.name.class != new_event.class}
2281
+ raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
2282
+ end
2283
+
2284
+ unless event = events[new_event]
2285
+ events << event = Event.new(self, new_event)
2286
+ end
2287
+
2288
+ event
2289
+ end
2290
+ end
2291
+ end
2292
+ end