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,4 @@
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(File.expand_path(filename)), binding, filename)
3
+ translations[:en][:mongoid] = translations[:en].delete(:activemodel)
4
+ translations
@@ -0,0 +1,81 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module Mongoid
4
+ version '2.x' do
5
+ def self.active?
6
+ ::Mongoid::VERSION =~ /^2\./
7
+ end
8
+
9
+ def define_state_initializer
10
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
11
+ def initialize(*)
12
+ @attributes ||= {}
13
+ self.class.state_machines.initialize_states(self, :static => :force, :dynamic => false)
14
+
15
+ super do |*args|
16
+ self.class.state_machines.initialize_states(self, :static => false)
17
+ yield(*args) if block_given?
18
+ end
19
+ end
20
+ end_eval
21
+ end
22
+
23
+ def owner_class_attribute_default
24
+ attribute_field && attribute_field.default
25
+ end
26
+
27
+ def define_action_hook
28
+ if action_hook == :save
29
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
30
+ def insert(*)
31
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super.persisted? }
32
+ self
33
+ end
34
+
35
+ def update(*)
36
+ self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
37
+ end
38
+ end_eval
39
+ else
40
+ super
41
+ end
42
+ end
43
+ end
44
+
45
+ version '2.0.x - 2.3.x' do
46
+ def self.active?
47
+ ::Mongoid::VERSION =~ /^2\.[0-3]\./
48
+ end
49
+
50
+ def attribute_field
51
+ owner_class.fields[attribute.to_s]
52
+ end
53
+ end
54
+
55
+ version '2.0.x - 2.2.x' do
56
+ def self.active?
57
+ ::Mongoid::VERSION =~ /^2\.[0-2]\./
58
+ end
59
+
60
+ def define_state_initializer
61
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
62
+ # Initializes dynamic states
63
+ def initialize(*)
64
+ super do |*args|
65
+ self.class.state_machines.initialize_states(self, :static => false)
66
+ yield(*args) if block_given?
67
+ end
68
+ end
69
+
70
+ # Initializes static states
71
+ def apply_default_attributes(*)
72
+ result = super
73
+ self.class.state_machines.initialize_states(self, :static => :force, :dynamic => false, :to => result) if new_record?
74
+ result
75
+ end
76
+ end_eval
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,486 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with Sequel models.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # Sequel model:
9
+ #
10
+ # class Vehicle < Sequel::Model
11
+ # state_machine :initial => :parked do
12
+ # event :ignite do
13
+ # transition :parked => :idling
14
+ # end
15
+ # end
16
+ # end
17
+ #
18
+ # The examples in the sections below will use the above class as a
19
+ # reference.
20
+ #
21
+ # == Actions
22
+ #
23
+ # By default, the action that will be invoked when a state is transitioned
24
+ # is the +save+ action. This will cause the resource to save the changes
25
+ # made to the state machine's attribute. *Note* that if any other changes
26
+ # were made to the resource prior to transition, then those changes will
27
+ # be made as well.
28
+ #
29
+ # For example,
30
+ #
31
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
32
+ # vehicle.name = 'Ford Explorer'
33
+ # vehicle.ignite # => true
34
+ # vehicle.refresh # => #<Vehicle @values={:state=>"idling", :name=>"Ford Explorer", :id=>1}>
35
+ #
36
+ # == Events
37
+ #
38
+ # As described in StateMachine::InstanceMethods#state_machine, event
39
+ # attributes are created for every machine that allow transitions to be
40
+ # performed automatically when the object's action (in this case, :save)
41
+ # is called.
42
+ #
43
+ # In Sequel, these automated events are run in the following order:
44
+ # * before validation - Run before callbacks and persist new states, then validate
45
+ # * before save - If validation was skipped, run before callbacks and persist new states, then save
46
+ # * after save - Run after callbacks
47
+ #
48
+ # For example,
49
+ #
50
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
51
+ # vehicle.state_event # => nil
52
+ # vehicle.state_event = 'invalid'
53
+ # vehicle.valid? # => false
54
+ # vehicle.errors.full_messages # => ["state_event is invalid"]
55
+ #
56
+ # vehicle.state_event = 'ignite'
57
+ # vehicle.valid? # => true
58
+ # vehicle.save # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
59
+ # vehicle.state # => "idling"
60
+ # vehicle.state_event # => nil
61
+ #
62
+ # Note that this can also be done on a mass-assignment basis:
63
+ #
64
+ # vehicle = Vehicle.create(:state_event => 'ignite') # => #<Vehicle @values={:state=>"idling", :name=>nil, :id=>1}>
65
+ # vehicle.state # => "idling"
66
+ #
67
+ # This technique is always used for transitioning states when the +save+
68
+ # action (which is the default) is configured for the machine.
69
+ #
70
+ # === Security implications
71
+ #
72
+ # Beware that public event attributes mean that events can be fired
73
+ # whenever mass-assignment is being used. If you want to prevent malicious
74
+ # users from tampering with events through URLs / forms, the attribute
75
+ # should be protected like so:
76
+ #
77
+ # class Vehicle < Sequel::Model
78
+ # set_restricted_columns :state_event
79
+ # # set_allowed_columns ... # Alternative technique
80
+ #
81
+ # state_machine do
82
+ # ...
83
+ # end
84
+ # end
85
+ #
86
+ # If you want to only have *some* events be able to fire via mass-assignment,
87
+ # you can build two state machines (one public and one protected) like so:
88
+ #
89
+ # class Vehicle < Sequel::Model
90
+ # set_restricted_columns :state_event # Prevent access to events in the first machine
91
+ #
92
+ # state_machine do
93
+ # # Define private events here
94
+ # end
95
+ #
96
+ # # Allow both machines to share the same state
97
+ # state_machine :public_state, :attribute => :state do
98
+ # # Define public events here
99
+ # end
100
+ # end
101
+ #
102
+ # == Transactions
103
+ #
104
+ # In order to ensure that any changes made during transition callbacks
105
+ # are rolled back during a failed attempt, every transition is wrapped
106
+ # within a transaction.
107
+ #
108
+ # For example,
109
+ #
110
+ # class Message < Sequel::Model
111
+ # end
112
+ #
113
+ # Vehicle.state_machine do
114
+ # before_transition do |transition|
115
+ # Message.create(:content => transition.inspect)
116
+ # false
117
+ # end
118
+ # end
119
+ #
120
+ # vehicle = Vehicle.create # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
121
+ # vehicle.ignite # => false
122
+ # Message.count # => 0
123
+ #
124
+ # *Note* that only before callbacks that halt the callback chain and
125
+ # failed attempts to save the record will result in the transaction being
126
+ # rolled back. If an after callback halts the chain, the previous result
127
+ # still applies and the transaction is *not* rolled back.
128
+ #
129
+ # To turn off transactions:
130
+ #
131
+ # class Vehicle < Sequel::Model
132
+ # state_machine :initial => :parked, :use_transactions => false do
133
+ # ...
134
+ # end
135
+ # end
136
+ #
137
+ # == Validation errors
138
+ #
139
+ # If an event fails to successfully fire because there are no matching
140
+ # transitions for the current record, a validation error is added to the
141
+ # record's state attribute to help in determining why it failed and for
142
+ # reporting via the UI.
143
+ #
144
+ # For example,
145
+ #
146
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle @values={:state=>"parked", :name=>nil, :id=>1}>
147
+ # vehicle.ignite # => false
148
+ # vehicle.errors.full_messages # => ["state cannot transition via \"ignite\""]
149
+ #
150
+ # If an event fails to fire because of a validation error on the record and
151
+ # *not* because a matching transition was not available, no error messages
152
+ # will be added to the state attribute.
153
+ #
154
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
155
+ # then the failure reason (such as the current validation errors) will be
156
+ # included in the exception that gets raised when the event fails. For
157
+ # example, assuming there's a validation on a field called +name+ on the class:
158
+ #
159
+ # vehicle = Vehicle.new
160
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
161
+ #
162
+ # == Scopes
163
+ #
164
+ # To assist in filtering models with specific states, a series of class
165
+ # methods are defined on the model for finding records with or without a
166
+ # particular set of states.
167
+ #
168
+ # These named scopes are the functional equivalent of the following
169
+ # definitions:
170
+ #
171
+ # class Vehicle < Sequel::Model
172
+ # class << self
173
+ # def with_states(*states)
174
+ # filter(:state => states)
175
+ # end
176
+ # alias_method :with_state, :with_states
177
+ #
178
+ # def without_states(*states)
179
+ # filter(~{:state => states})
180
+ # end
181
+ # alias_method :without_state, :without_states
182
+ # end
183
+ # end
184
+ #
185
+ # *Note*, however, that the states are converted to their stored values
186
+ # before being passed into the query.
187
+ #
188
+ # Because of the way scopes work in Sequel, they can be chained like so:
189
+ #
190
+ # Vehicle.with_state(:parked).order(:id.desc)
191
+ #
192
+ # Note that states can also be referenced by the string version of their
193
+ # name:
194
+ #
195
+ # Vehicle.with_state('parked')
196
+ #
197
+ # == Callbacks
198
+ #
199
+ # All before/after transition callbacks defined for Sequel resources
200
+ # behave in the same way that other Sequel hooks behave. Rather than
201
+ # passing in the record as an argument to the callback, the callback is
202
+ # instead bound to the object and evaluated within its context.
203
+ #
204
+ # For example,
205
+ #
206
+ # class Vehicle < Sequel::Model
207
+ # state_machine :initial => :parked do
208
+ # before_transition any => :idling do
209
+ # put_on_seatbelt
210
+ # end
211
+ #
212
+ # before_transition do |transition|
213
+ # # log message
214
+ # end
215
+ #
216
+ # event :ignite do
217
+ # transition :parked => :idling
218
+ # end
219
+ # end
220
+ #
221
+ # def put_on_seatbelt
222
+ # ...
223
+ # end
224
+ # end
225
+ #
226
+ # Note, also, that the transition can be accessed by simply defining
227
+ # additional arguments in the callback block.
228
+ #
229
+ # === Failure callbacks
230
+ #
231
+ # +after_failure+ callbacks allow you to execute behaviors when a transition
232
+ # is allowed, but fails to save. This could be useful for something like
233
+ # auditing transition attempts. Since callbacks run within transactions in
234
+ # Sequel, a save failure will cause any records that get created in
235
+ # your callback to roll back. You can work around this issue like so:
236
+ #
237
+ # DB = Sequel.connect('mysql://localhost/app')
238
+ # DB_LOGS = Sequel.connect('mysql://localhost/app')
239
+ #
240
+ # class TransitionLog < Sequel::Model(DB_LOGS[:transition_logs])
241
+ # end
242
+ #
243
+ # class Vehicle < Sequel::Model(DB[:vehicles])
244
+ # state_machine do
245
+ # after_failure do |transition|
246
+ # TransitionLog.create(:vehicle => vehicle, :transition => transition)
247
+ # end
248
+ #
249
+ # ...
250
+ # end
251
+ # end
252
+ #
253
+ # The +TransitionLog+ model uses a second connection to the database that
254
+ # allows new records to be saved without being affected by rollbacks in the
255
+ # +Vehicle+ model's transaction.
256
+ #
257
+ # === Callback Order
258
+ #
259
+ # Callbacks occur in the following order. Callbacks specific to state_machine
260
+ # are bolded. The remaining callbacks are part of Sequel.
261
+ #
262
+ # * (-) save
263
+ # * (-) begin transaction (if enabled)
264
+ # * (1) *before_transition*
265
+ # * (2) before_validation
266
+ # * (-) validate
267
+ # * (3) after_validation
268
+ # * (4) before_save
269
+ # * (5) before_create
270
+ # * (-) create
271
+ # * (6) after_create
272
+ # * (7) after_save
273
+ # * (8) *after_transition*
274
+ # * (-) end transaction (if enabled)
275
+ # * (9) after_commit
276
+ module Sequel
277
+ include Base
278
+
279
+ require 'state_machine/integrations/sequel/versions'
280
+
281
+ # The default options to use for state machines using this integration
282
+ @defaults = {:action => :save}
283
+
284
+ # Classes that include Sequel::Model will automatically use the Sequel
285
+ # integration.
286
+ def self.matching_ancestors
287
+ %w(Sequel::Model)
288
+ end
289
+
290
+ # Forces the change in state to be recognized regardless of whether the
291
+ # state value actually changed
292
+ def write(object, attribute, value, *args)
293
+ result = super
294
+
295
+ column = self.attribute.to_sym
296
+ if (attribute == :state || attribute == :event && value) && owner_class.columns.include?(column) && !object.changed_columns.include?(column)
297
+ object.changed_columns << column
298
+ end
299
+
300
+ result
301
+ end
302
+
303
+ # Adds a validation error to the given object
304
+ def invalidate(object, attribute, message, values = [])
305
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
306
+ end
307
+
308
+ # Describes the current validation errors on the given object. If none
309
+ # are specific, then the default error is interpeted as a "halt".
310
+ def errors_for(object)
311
+ object.errors.empty? ? 'Transition halted' : object.errors.full_messages * ', '
312
+ end
313
+
314
+ # Resets any errors previously added when invalidating the given object
315
+ def reset(object)
316
+ object.errors.clear
317
+ end
318
+
319
+ # Pluralizes the name using the built-in inflector
320
+ def pluralize(word)
321
+ load_inflector
322
+ super
323
+ end
324
+
325
+ protected
326
+ # Initializes class-level extensions for this machine
327
+ def define_helpers
328
+ load_plugins
329
+ super
330
+ end
331
+
332
+ # Loads all of the Sequel plugins necessary to run
333
+ def load_plugins
334
+ owner_class.plugin(:hook_class_methods)
335
+ end
336
+
337
+ # Loads the built-in inflector
338
+ def load_inflector
339
+ require 'sequel/extensions/inflector'
340
+ end
341
+
342
+ # Defines an initialization hook into the owner class for setting the
343
+ # initial state of the machine *before* any attributes are set on the
344
+ # object
345
+ def define_state_initializer
346
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
347
+ def initialize_set(*)
348
+ self.class.state_machines.initialize_states(self, :static => :force) { super }
349
+ end
350
+ end_eval
351
+ end
352
+
353
+ # Skips defining reader/writer methods since this is done automatically
354
+ def define_state_accessor
355
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
356
+ def validate(*)
357
+ super
358
+ machine = self.class.state_machine(#{name.inspect})
359
+ machine.invalidate(self, :state, :invalid) unless machine.states.match(self)
360
+ end
361
+ end_eval
362
+ end
363
+
364
+ # Defines validation hooks if the machine's action is to save the model
365
+ def define_action_helpers
366
+ super
367
+ define_validation_hook if action == :save
368
+ end
369
+
370
+ # Uses around callbacks to run state events if using the :save hook
371
+ def define_action_hook
372
+ if action == :save
373
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
374
+ def #{action_hook}(*args)
375
+ opts = args.last.is_a?(Hash) ? args.last : {}
376
+ yielded = false
377
+ result = self.class.state_machine(#{name.inspect}).send(:around_save, self) do
378
+ yielded = true
379
+ super
380
+ end
381
+
382
+ if yielded || result
383
+ result
384
+ else
385
+ #{handle_save_failure}
386
+ end
387
+ end
388
+ end_eval
389
+ else
390
+ super
391
+ end
392
+ end
393
+
394
+ # Handles how save failures (due to invalid transitions) are raised
395
+ def handle_save_failure
396
+ 'raise_hook_failure(:before_transition) if raise_on_failure?(opts)'
397
+ end
398
+
399
+ # Runs state events around the machine's :save action
400
+ def around_save(object)
401
+ result = transaction(object) do
402
+ object.class.state_machines.transitions(object, action).perform { yield }
403
+ end
404
+ result
405
+ end
406
+
407
+ # Adds hooks into validation for automatically firing events
408
+ def define_validation_hook
409
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
410
+ def around_validation(*)
411
+ self.class.state_machines.transitions(self, :save, :after => false).perform { super }
412
+ end
413
+ end_eval
414
+ end
415
+
416
+ # Gets the db default for the machine's attribute
417
+ def owner_class_attribute_default
418
+ if owner_class.db.table_exists?(owner_class.table_name) && column = owner_class.db_schema[attribute.to_sym]
419
+ column[:default]
420
+ end
421
+ end
422
+
423
+ # Uses the DB literal to match the default against the specified state
424
+ def owner_class_attribute_default_matches?(state)
425
+ owner_class.db.literal(state.value) == owner_class_attribute_default
426
+ end
427
+
428
+ # Creates a scope for finding records *with* a particular state or
429
+ # states for the attribute
430
+ def create_with_scope(name)
431
+ create_scope(name, lambda {|dataset, values| dataset.filter(attribute_column => values)})
432
+ end
433
+
434
+ # Creates a scope for finding records *without* a particular state or
435
+ # states for the attribute
436
+ def create_without_scope(name)
437
+ create_scope(name, lambda {|dataset, values| dataset.exclude(attribute_column => values)})
438
+ end
439
+
440
+ # Creates a new named scope with the given name
441
+ def create_scope(name, scope)
442
+ machine = self
443
+ owner_class.def_dataset_method(name) do |*states|
444
+ machine.send(:run_scope, scope, self, states)
445
+ end
446
+
447
+ false
448
+ end
449
+
450
+ # Generates the results for the given scope based on one or more states to
451
+ # filter by
452
+ def run_scope(scope, dataset, states)
453
+ super(scope, model_from_dataset(dataset).state_machine(name), dataset, states)
454
+ end
455
+
456
+ # Determines the model associated with the given dataset
457
+ def model_from_dataset(dataset)
458
+ dataset.model
459
+ end
460
+
461
+ # Generates the fully-qualifed column name for this machine's attribute
462
+ def attribute_column
463
+ ::Sequel::SQL::QualifiedIdentifier.new(owner_class.table_name, attribute)
464
+ end
465
+
466
+ # Runs a new database transaction, rolling back any changes if the
467
+ # yielded block fails (i.e. returns false).
468
+ def transaction(object)
469
+ result = nil
470
+ object.db.transaction do
471
+ raise ::Sequel::Error::Rollback unless result = yield
472
+ end
473
+ result
474
+ end
475
+
476
+ # Creates a new callback in the callback chain, always ensuring that
477
+ # it's configured to bind to the object as this is the convention for
478
+ # Sequel callbacks
479
+ def add_callback(type, options, &block)
480
+ options[:bind_to_object] = true
481
+ options[:terminator] = @terminator ||= lambda {|result| result == false}
482
+ super
483
+ end
484
+ end
485
+ end
486
+ end