glebtv_state_machine 1.3.0

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