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,11 @@
1
+ {:en => {
2
+ :activemodel => {
3
+ :errors => {
4
+ :messages => {
5
+ :invalid => StateMachine::Machine.default_messages[:invalid],
6
+ :invalid_event => StateMachine::Machine.default_messages[:invalid_event] % ['%{state}'],
7
+ :invalid_transition => StateMachine::Machine.default_messages[:invalid_transition] % ['%{event}']
8
+ }
9
+ }
10
+ }
11
+ }}
@@ -0,0 +1,33 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveModel
4
+ # Adds support for invoking callbacks on ActiveModel observers with more
5
+ # than one argument (e.g. the record *and* the state transition). By
6
+ # default, ActiveModel only supports passing the record into the
7
+ # callbacks.
8
+ #
9
+ # For example:
10
+ #
11
+ # class VehicleObserver < ActiveModel::Observer
12
+ # # The default behavior: only pass in the record
13
+ # def after_save(vehicle)
14
+ # end
15
+ #
16
+ # # Custom behavior: allow the transition to be passed in as well
17
+ # def after_transition(vehicle, transition)
18
+ # Audit.log(vehicle, transition)
19
+ # end
20
+ # end
21
+ module Observer
22
+ def update_with_transition(observer_update)
23
+ method = observer_update.method
24
+ send(method, *observer_update.args) if respond_to?(method)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ActiveModel::Observer.class_eval do
32
+ include StateMachine::Integrations::ActiveModel::Observer
33
+ end if defined?(ActiveModel::Observer)
@@ -0,0 +1,42 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveModel
4
+ # Represents the encapsulation of all of the details to be included in an
5
+ # update to state machine observers. This allows multiple arguments to
6
+ # get passed to an observer method (instead of just a single +object+)
7
+ # while still respecting the way in which ActiveModel checks for the
8
+ # object's list of observers.
9
+ class ObserverUpdate
10
+ # The method to invoke on the observer
11
+ attr_reader :method
12
+
13
+ # The object being transitioned
14
+ attr_reader :object
15
+
16
+ # The transition being run
17
+ attr_reader :transition
18
+
19
+ def initialize(method, object, transition) #:nodoc:
20
+ @method, @object, @transition = method, object, transition
21
+ end
22
+
23
+ # The arguments to pass into the method
24
+ def args
25
+ [object, transition]
26
+ end
27
+
28
+ # The class of the object being transitioned. Normally the object
29
+ # getting passed into observer methods is the actual instance of the
30
+ # ActiveModel class. ActiveModel uses that instance's class to check
31
+ # for enabled / disabled observers.
32
+ #
33
+ # Since state_machine is passing an ObserverUpdate instance into observer
34
+ # methods, +class+ needs to be overridden so that ActiveModel can still
35
+ # get access to the enabled / disabled observers.
36
+ def class
37
+ object.class
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveModel
4
+ version '2.x' do
5
+ def self.active?
6
+ !defined?(::ActiveModel::VERSION) || ::ActiveModel::VERSION::MAJOR == 2
7
+ end
8
+
9
+ def define_validation_hook
10
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
11
+ def valid?(*)
12
+ self.class.state_machines.transitions(self, #{action.inspect}, :after => false).perform { super }
13
+ end
14
+ end_eval
15
+ end
16
+ end
17
+
18
+ version '3.0.x' do
19
+ def self.active?
20
+ defined?(::ActiveModel::VERSION) && ::ActiveModel::VERSION::MAJOR == 3 && ::ActiveModel::VERSION::MINOR == 0
21
+ end
22
+
23
+ def define_validation_hook
24
+ # +around+ callbacks don't have direct access to results until AS 3.1
25
+ owner_class.set_callback(:validation, :after, 'value', :prepend => true)
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,552 @@
1
+ require 'state_machine/integrations/active_model'
2
+
3
+ module StateMachine
4
+ module Integrations #:nodoc:
5
+ # Adds support for integrating state machines with ActiveRecord models.
6
+ #
7
+ # == Examples
8
+ #
9
+ # Below is an example of a simple state machine defined within an
10
+ # ActiveRecord model:
11
+ #
12
+ # class Vehicle < ActiveRecord::Base
13
+ # state_machine :initial => :parked do
14
+ # event :ignite do
15
+ # transition :parked => :idling
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # The examples in the sections below will use the above class as a
21
+ # reference.
22
+ #
23
+ # == Actions
24
+ #
25
+ # By default, the action that will be invoked when a state is transitioned
26
+ # is the +save+ action. This will cause the record to save the changes
27
+ # made to the state machine's attribute. *Note* that if any other changes
28
+ # were made to the record prior to transition, then those changes will
29
+ # be saved as well.
30
+ #
31
+ # For example,
32
+ #
33
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
34
+ # vehicle.name = 'Ford Explorer'
35
+ # vehicle.ignite # => true
36
+ # vehicle.reload # => #<Vehicle id: 1, name: "Ford Explorer", state: "idling">
37
+ #
38
+ # *Note* that if you want a transition to update additional attributes of the record,
39
+ # either the changes need to be made in a +before_transition+ callback or you need
40
+ # to save the record manually.
41
+ #
42
+ # == Events
43
+ #
44
+ # As described in StateMachine::InstanceMethods#state_machine, event
45
+ # attributes are created for every machine that allow transitions to be
46
+ # performed automatically when the object's action (in this case, :save)
47
+ # is called.
48
+ #
49
+ # In ActiveRecord, these automated events are run in the following order:
50
+ # * before validation - Run before callbacks and persist new states, then validate
51
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
52
+ # * after save - Run after callbacks
53
+ #
54
+ # For example,
55
+ #
56
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
57
+ # vehicle.state_event # => nil
58
+ # vehicle.state_event = 'invalid'
59
+ # vehicle.valid? # => false
60
+ # vehicle.errors.full_messages # => ["State event is invalid"]
61
+ #
62
+ # vehicle.state_event = 'ignite'
63
+ # vehicle.valid? # => true
64
+ # vehicle.save # => true
65
+ # vehicle.state # => "idling"
66
+ # vehicle.state_event # => nil
67
+ #
68
+ # Note that this can also be done on a mass-assignment basis:
69
+ #
70
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle id: 1, name: nil, state: "idling">
71
+ # vehicle.state # => "idling"
72
+ #
73
+ # This technique is always used for transitioning states when the +save+
74
+ # action (which is the default) is configured for the machine.
75
+ #
76
+ # === Security implications
77
+ #
78
+ # Beware that public event attributes mean that events can be fired
79
+ # whenever mass-assignment is being used. If you want to prevent malicious
80
+ # users from tampering with events through URLs / forms, the attribute
81
+ # should be protected like so:
82
+ #
83
+ # class Vehicle < ActiveRecord::Base
84
+ # attr_protected :state_event
85
+ # # attr_accessible ... # Alternative technique
86
+ #
87
+ # state_machine do
88
+ # ...
89
+ # end
90
+ # end
91
+ #
92
+ # If you want to only have *some* events be able to fire via mass-assignment,
93
+ # you can build two state machines (one public and one protected) like so:
94
+ #
95
+ # class Vehicle < ActiveRecord::Base
96
+ # attr_protected :state_event # Prevent access to events in the first machine
97
+ #
98
+ # state_machine do
99
+ # # Define private events here
100
+ # end
101
+ #
102
+ # # Public machine targets the same state as the private machine
103
+ # state_machine :public_state, :attribute => :state do
104
+ # # Define public events here
105
+ # end
106
+ # end
107
+ #
108
+ # == Transactions
109
+ #
110
+ # In order to ensure that any changes made during transition callbacks
111
+ # are rolled back during a failed attempt, every transition is wrapped
112
+ # within a transaction.
113
+ #
114
+ # For example,
115
+ #
116
+ # class Message < ActiveRecord::Base
117
+ # end
118
+ #
119
+ # Vehicle.state_machine do
120
+ # before_transition do |vehicle, transition|
121
+ # Message.create(:content => transition.inspect)
122
+ # false
123
+ # end
124
+ # end
125
+ #
126
+ # vehicle = Vehicle.create # => #<Vehicle id: 1, name: nil, state: "parked">
127
+ # vehicle.ignite # => false
128
+ # Message.count # => 0
129
+ #
130
+ # *Note* that only before callbacks that halt the callback chain and
131
+ # failed attempts to save the record will result in the transaction being
132
+ # rolled back. If an after callback halts the chain, the previous result
133
+ # still applies and the transaction is *not* rolled back.
134
+ #
135
+ # To turn off transactions:
136
+ #
137
+ # class Vehicle < ActiveRecord::Base
138
+ # state_machine :initial => :parked, :use_transactions => false do
139
+ # ...
140
+ # end
141
+ # end
142
+ #
143
+ # == Validations
144
+ #
145
+ # As mentioned in StateMachine::Machine#state, you can define behaviors,
146
+ # like validations, that only execute for certain states. One *important*
147
+ # caveat here is that, due to a constraint in ActiveRecord's validation
148
+ # framework, custom validators will not work as expected when defined to run
149
+ # in multiple states. For example:
150
+ #
151
+ # class Vehicle < ActiveRecord::Base
152
+ # state_machine do
153
+ # ...
154
+ # state :first_gear, :second_gear do
155
+ # validate :speed_is_legal
156
+ # end
157
+ # end
158
+ # end
159
+ #
160
+ # In this case, the <tt>:speed_is_legal</tt> validation will only get run
161
+ # for the <tt>:second_gear</tt> state. To avoid this, you can define your
162
+ # custom validation like so:
163
+ #
164
+ # class Vehicle < ActiveRecord::Base
165
+ # state_machine do
166
+ # ...
167
+ # state :first_gear, :second_gear do
168
+ # validate {|vehicle| vehicle.speed_is_legal}
169
+ # end
170
+ # end
171
+ # end
172
+ #
173
+ # == Validation errors
174
+ #
175
+ # If an event fails to successfully fire because there are no matching
176
+ # transitions for the current record, a validation error is added to the
177
+ # record's state attribute to help in determining why it failed and for
178
+ # reporting via the UI.
179
+ #
180
+ # For example,
181
+ #
182
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id: 1, name: nil, state: "idling">
183
+ # vehicle.ignite # => false
184
+ # vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
185
+ #
186
+ # If an event fails to fire because of a validation error on the record and
187
+ # *not* because a matching transition was not available, no error messages
188
+ # will be added to the state attribute.
189
+ #
190
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
191
+ # then the failure reason (such as the current validation errors) will be
192
+ # included in the exception that gets raised when the event fails. For
193
+ # example, assuming there's a validation on a field called +name+ on the class:
194
+ #
195
+ # vehicle = Vehicle.new
196
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
197
+ #
198
+ # == Scopes
199
+ #
200
+ # To assist in filtering models with specific states, a series of named
201
+ # scopes are defined on the model for finding records with or without a
202
+ # particular set of states.
203
+ #
204
+ # These named scopes are essentially the functional equivalent of the
205
+ # following definitions:
206
+ #
207
+ # class Vehicle < ActiveRecord::Base
208
+ # named_scope :with_states, lambda {|*states| {:conditions => {:state => states}}}
209
+ # # with_states also aliased to with_state
210
+ #
211
+ # named_scope :without_states, lambda {|*states| {:conditions => ['state NOT IN (?)', states]}}
212
+ # # without_states also aliased to without_state
213
+ # end
214
+ #
215
+ # *Note*, however, that the states are converted to their stored values
216
+ # before being passed into the query.
217
+ #
218
+ # Because of the way named scopes work in ActiveRecord, they can be
219
+ # chained like so:
220
+ #
221
+ # Vehicle.with_state(:parked).all(:order => 'id DESC')
222
+ #
223
+ # Note that states can also be referenced by the string version of their
224
+ # name:
225
+ #
226
+ # Vehicle.with_state('parked')
227
+ #
228
+ # == Callbacks
229
+ #
230
+ # All before/after transition callbacks defined for ActiveRecord models
231
+ # behave in the same way that other ActiveRecord callbacks behave. The
232
+ # object involved in the transition is passed in as an argument.
233
+ #
234
+ # For example,
235
+ #
236
+ # class Vehicle < ActiveRecord::Base
237
+ # state_machine :initial => :parked do
238
+ # before_transition any => :idling do |vehicle|
239
+ # vehicle.put_on_seatbelt
240
+ # end
241
+ #
242
+ # before_transition do |vehicle, transition|
243
+ # # log message
244
+ # end
245
+ #
246
+ # event :ignite do
247
+ # transition :parked => :idling
248
+ # end
249
+ # end
250
+ #
251
+ # def put_on_seatbelt
252
+ # ...
253
+ # end
254
+ # end
255
+ #
256
+ # Note, also, that the transition can be accessed by simply defining
257
+ # additional arguments in the callback block.
258
+ #
259
+ # === Failure callbacks
260
+ #
261
+ # +after_failure+ callbacks allow you to execute behaviors when a transition
262
+ # is allowed, but fails to save. This could be useful for something like
263
+ # auditing transition attempts. Since callbacks run within transactions in
264
+ # ActiveRecord, a save failure will cause any records that get created in
265
+ # your callback to roll back. You can work around this issue like so:
266
+ #
267
+ # class TransitionLog < ActiveRecord::Base
268
+ # establish_connection Rails.env.to_sym
269
+ # end
270
+ #
271
+ # class Vehicle < ActiveRecord::Base
272
+ # state_machine do
273
+ # after_failure do |vehicle, transition|
274
+ # TransitionLog.create(:vehicle => vehicle, :transition => transition)
275
+ # end
276
+ #
277
+ # ...
278
+ # end
279
+ # end
280
+ #
281
+ # The +TransitionLog+ model establishes a second connection to the database
282
+ # that allows new records to be saved without being affected by rollbacks
283
+ # in the +Vehicle+ model's transaction.
284
+ #
285
+ # === Callback Order
286
+ #
287
+ # Callbacks occur in the following order. Callbacks specific to state_machine
288
+ # are bolded. The remaining callbacks are part of ActiveRecord.
289
+ #
290
+ # * (-) save
291
+ # * (-) begin transaction (if enabled)
292
+ # * (1) *before_transition*
293
+ # * (-) valid
294
+ # * (2) before_validation
295
+ # * (-) validate
296
+ # * (3) after_validation
297
+ # * (4) before_save
298
+ # * (5) before_create
299
+ # * (-) create
300
+ # * (6) after_create
301
+ # * (7) after_save
302
+ # * (8) *after_transition*
303
+ # * (-) end transaction (if enabled)
304
+ # * (9) after_commit
305
+ #
306
+ # == Observers
307
+ #
308
+ # In addition to support for ActiveRecord-like hooks, there is additional
309
+ # support for ActiveRecord observers. Because of the way ActiveRecord
310
+ # observers are designed, there is less flexibility around the specific
311
+ # transitions that can be hooked in. However, a large number of hooks
312
+ # *are* supported. For example, if a transition for a record's +state+
313
+ # attribute changes the state from +parked+ to +idling+ via the +ignite+
314
+ # event, the following observer methods are supported:
315
+ # * before/after/after_failure_to-_ignite_from_parked_to_idling
316
+ # * before/after/after_failure_to-_ignite_from_parked
317
+ # * before/after/after_failure_to-_ignite_to_idling
318
+ # * before/after/after_failure_to-_ignite
319
+ # * before/after/after_failure_to-_transition_state_from_parked_to_idling
320
+ # * before/after/after_failure_to-_transition_state_from_parked
321
+ # * before/after/after_failure_to-_transition_state_to_idling
322
+ # * before/after/after_failure_to-_transition_state
323
+ # * before/after/after_failure_to-_transition
324
+ #
325
+ # The following class shows an example of some of these hooks:
326
+ #
327
+ # class VehicleObserver < ActiveRecord::Observer
328
+ # def before_save(vehicle)
329
+ # # log message
330
+ # end
331
+ #
332
+ # # Callback for :ignite event *before* the transition is performed
333
+ # def before_ignite(vehicle, transition)
334
+ # # log message
335
+ # end
336
+ #
337
+ # # Callback for :ignite event *after* the transition has been performed
338
+ # def after_ignite(vehicle, transition)
339
+ # # put on seatbelt
340
+ # end
341
+ #
342
+ # # Generic transition callback *before* the transition is performed
343
+ # def after_transition(vehicle, transition)
344
+ # Audit.log(vehicle, transition)
345
+ # end
346
+ # end
347
+ #
348
+ # More flexible transition callbacks can be defined directly within the
349
+ # model as described in StateMachine::Machine#before_transition
350
+ # and StateMachine::Machine#after_transition.
351
+ #
352
+ # To define a single observer for multiple state machines:
353
+ #
354
+ # class StateMachineObserver < ActiveRecord::Observer
355
+ # observe Vehicle, Switch, Project
356
+ #
357
+ # def after_transition(record, transition)
358
+ # Audit.log(record, transition)
359
+ # end
360
+ # end
361
+ #
362
+ # == Internationalization
363
+ #
364
+ # In Rails 2.2+, any error message that is generated from performing invalid
365
+ # transitions can be localized. The following default translations are used:
366
+ #
367
+ # en:
368
+ # activerecord:
369
+ # errors:
370
+ # messages:
371
+ # invalid: "is invalid"
372
+ # # %{value} = attribute value, %{state} = Human state name
373
+ # invalid_event: "cannot transition when %{state}"
374
+ # # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
375
+ # invalid_transition: "cannot transition via %{event}"
376
+ #
377
+ # Notice that the interpolation syntax is %{key} in Rails 3+. In Rails 2.x,
378
+ # the appropriate syntax is {{key}}.
379
+ #
380
+ # You can override these for a specific model like so:
381
+ #
382
+ # en:
383
+ # activerecord:
384
+ # errors:
385
+ # models:
386
+ # user:
387
+ # invalid: "is not valid"
388
+ #
389
+ # In addition to the above, you can also provide translations for the
390
+ # various states / events in each state machine. Using the Vehicle example,
391
+ # state translations will be looked for using the following keys, where
392
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
393
+ # * <tt>activerecord.state_machines.#{model_name}.#{machine_name}.states.#{state_name}</tt>
394
+ # * <tt>activerecord.state_machines.#{model_name}.states.#{state_name}</tt>
395
+ # * <tt>activerecord.state_machines.#{machine_name}.states.#{state_name}</tt>
396
+ # * <tt>activerecord.state_machines.states.#{state_name}</tt>
397
+ #
398
+ # Event translations will be looked for using the following keys, where
399
+ # +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
400
+ # * <tt>activerecord.state_machines.#{model_name}.#{machine_name}.events.#{event_name}</tt>
401
+ # * <tt>activerecord.state_machines.#{model_name}.events.#{event_name}</tt>
402
+ # * <tt>activerecord.state_machines.#{machine_name}.events.#{event_name}</tt>
403
+ # * <tt>activerecord.state_machines.events.#{event_name}</tt>
404
+ #
405
+ # An example translation configuration might look like so:
406
+ #
407
+ # es:
408
+ # activerecord:
409
+ # state_machines:
410
+ # states:
411
+ # parked: 'estacionado'
412
+ # events:
413
+ # park: 'estacionarse'
414
+ module ActiveRecord
415
+ include Base
416
+ include ActiveModel
417
+
418
+ require 'state_machine/integrations/active_record/versions'
419
+
420
+ # The default options to use for state machines using this integration
421
+ @defaults = {:action => :save}
422
+
423
+ # Classes that inherit from ActiveRecord::Base will automatically use
424
+ # the ActiveRecord integration.
425
+ def self.matching_ancestors
426
+ %w(ActiveRecord::Base)
427
+ end
428
+
429
+ def self.extended(base) #:nodoc:
430
+ require 'active_record/version'
431
+ super
432
+ end
433
+
434
+ protected
435
+ # Only runs validations on the action if using <tt>:save</tt>
436
+ def runs_validations_on_action?
437
+ action == :save
438
+ end
439
+
440
+ # Gets the db default for the machine's attribute
441
+ def owner_class_attribute_default
442
+ if owner_class.connected? && owner_class.table_exists? && column = owner_class.columns_hash[attribute.to_s]
443
+ column.default
444
+ end
445
+ end
446
+
447
+ # Defines an initialization hook into the owner class for setting the
448
+ # initial state of the machine *before* any attributes are set on the
449
+ # object
450
+ def define_state_initializer
451
+ define_static_state_initializer
452
+ define_dynamic_state_initializer
453
+ end
454
+
455
+ # Initializes static states
456
+ def define_static_state_initializer
457
+ # This is the only available hook where the default set of attributes
458
+ # can be overridden for a new object *prior* to the processing of the
459
+ # attributes passed into #initialize
460
+ define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
461
+ def column_defaults(*) #:nodoc:
462
+ result = super
463
+ # No need to pass in an object, since the overrides will be forced
464
+ self.state_machines.initialize_states(nil, :static => :force, :dynamic => false, :to => result)
465
+ result
466
+ end
467
+ end_eval
468
+ end
469
+
470
+ # Initializes dynamic states
471
+ def define_dynamic_state_initializer
472
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
473
+ def initialize(*)
474
+ super do |*args|
475
+ self.class.state_machines.initialize_states(self)
476
+ yield(*args) if block_given?
477
+ end
478
+ end
479
+ end_eval
480
+ end
481
+
482
+ # Uses around callbacks to run state events if using the :save hook
483
+ def define_action_hook
484
+ if action_hook == :save
485
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
486
+ def save(*)
487
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
488
+ end
489
+
490
+ def save!(*)
491
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super } || raise(ActiveRecord::RecordInvalid.new(self))
492
+ end
493
+
494
+ def changed_for_autosave?
495
+ super || self.class.state_machines.any? {|name, machine| machine.action == :save && machine.read(self, :event)}
496
+ end
497
+ end_eval
498
+ else
499
+ super
500
+ end
501
+ end
502
+
503
+ # Runs state events around the machine's :save action
504
+ def around_save(object)
505
+ transaction(object) do
506
+ object.class.state_machines.transitions(object, action).perform { yield }
507
+ end
508
+ end
509
+
510
+ # Creates a scope for finding records *with* a particular state or
511
+ # states for the attribute
512
+ def create_with_scope(name)
513
+ create_scope(name, lambda {|values| ["#{attribute_column} IN (?)", values]})
514
+ end
515
+
516
+ # Creates a scope for finding records *without* a particular state or
517
+ # states for the attribute
518
+ def create_without_scope(name)
519
+ create_scope(name, lambda {|values| ["#{attribute_column} NOT IN (?)", values]})
520
+ end
521
+
522
+ # Generates the fully-qualifed column name for this machine's attribute
523
+ def attribute_column
524
+ connection = owner_class.connection
525
+ "#{connection.quote_table_name(owner_class.table_name)}.#{connection.quote_column_name(attribute)}"
526
+ end
527
+
528
+ # Runs a new database transaction, rolling back any changes by raising
529
+ # an ActiveRecord::Rollback exception if the yielded block fails
530
+ # (i.e. returns false).
531
+ def transaction(object)
532
+ result = nil
533
+ object.class.transaction do
534
+ raise ::ActiveRecord::Rollback unless result = yield
535
+ end
536
+ result
537
+ end
538
+
539
+ # Defines a new named scope with the given name
540
+ def create_scope(name, scope)
541
+ lambda {|model, values| model.where(scope.call(values))}
542
+ end
543
+
544
+ # ActiveModel's use of method_missing / respond_to for attribute methods
545
+ # breaks both ancestor lookups and defined?(super). Need to special-case
546
+ # the existence of query attribute methods.
547
+ def owner_class_ancestor_has_method?(scope, method)
548
+ scope == :instance && method == "#{attribute}?" ? owner_class : super
549
+ end
550
+ end
551
+ end
552
+ end