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,20 @@
1
+ filename = "#{File.dirname(__FILE__)}/../active_model/locale.rb"
2
+ translations = eval(IO.read(File.expand_path(filename)), binding, filename)
3
+ translations[:en][:activerecord] = translations[:en].delete(:activemodel)
4
+
5
+ # Only ActiveRecord 2.3.5+ can pull i18n >= 0.1.3 from system-wide gems (and
6
+ # therefore possibly have I18n::VERSION available)
7
+ begin
8
+ require 'i18n/version'
9
+ rescue Exception => ex
10
+ end unless ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 5)
11
+
12
+ # Only i18n 0.4.0+ has the new %{key} syntax
13
+ if !defined?(I18n::VERSION) || I18n::VERSION < '0.4.0'
14
+ translations[:en][:activerecord][:errors][:messages].each do |key, message|
15
+ message.gsub!('%{', '{{')
16
+ message.gsub!('}', '}}')
17
+ end
18
+ end
19
+
20
+ translations
@@ -0,0 +1,123 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ module ActiveRecord
4
+ version '2.x - 3.0.x' do
5
+ def self.active?
6
+ ::ActiveRecord::VERSION::MAJOR == 2 || ::ActiveRecord::VERSION::MAJOR == 3 && ::ActiveRecord::VERSION::MINOR == 0
7
+ end
8
+
9
+ def define_static_state_initializer
10
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
11
+ def attributes_from_column_definition(*)
12
+ result = super
13
+ self.class.state_machines.initialize_states(self, :static => :force, :dynamic => false, :to => result)
14
+ result
15
+ end
16
+ end_eval
17
+ end
18
+ end
19
+
20
+ version '2.x' do
21
+ def self.active?
22
+ ::ActiveRecord::VERSION::MAJOR == 2
23
+ end
24
+
25
+ def load_locale
26
+ super if defined?(I18n)
27
+ end
28
+
29
+ def create_scope(name, scope)
30
+ if owner_class.respond_to?(:named_scope)
31
+ name = name.to_sym
32
+ machine_name = self.name
33
+
34
+ # Since ActiveRecord does not allow direct access to the model
35
+ # being used within the evaluation of a dynamic named scope, the
36
+ # scope must be generated manually. It's necessary to have access
37
+ # to the model so that the state names can be translated to their
38
+ # associated values and so that inheritance is respected properly.
39
+ owner_class.named_scope(name)
40
+ owner_class.scopes[name] = lambda do |model, *states|
41
+ machine_states = model.state_machine(machine_name).states
42
+ values = states.flatten.map {|state| machine_states.fetch(state).value}
43
+
44
+ ::ActiveRecord::NamedScope::Scope.new(model, :conditions => scope.call(values))
45
+ end
46
+ end
47
+
48
+ # Prevent the Machine class from wrapping the scope
49
+ false
50
+ end
51
+
52
+ def invalidate(object, attribute, message, values = [])
53
+ if defined?(I18n)
54
+ super
55
+ else
56
+ object.errors.add(self.attribute(attribute), generate_message(message, values))
57
+ end
58
+ end
59
+
60
+ def translate(klass, key, value)
61
+ if defined?(I18n)
62
+ super
63
+ else
64
+ value ? value.to_s.humanize.downcase : 'nil'
65
+ end
66
+ end
67
+
68
+ def supports_observers?
69
+ true
70
+ end
71
+
72
+ def supports_validations?
73
+ true
74
+ end
75
+
76
+ def supports_mass_assignment_security?
77
+ true
78
+ end
79
+
80
+ def i18n_scope(klass)
81
+ :activerecord
82
+ end
83
+
84
+ def load_observer_extensions
85
+ super
86
+ ::ActiveRecord::Observer.class_eval do
87
+ include StateMachine::Integrations::ActiveModel::Observer
88
+ end unless ::ActiveRecord::Observer < StateMachine::Integrations::ActiveModel::Observer
89
+ end
90
+ end
91
+
92
+ version '2.0 - 2.2.x' do
93
+ def self.active?
94
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR < 3
95
+ end
96
+
97
+ def default_error_message_options(object, attribute, message)
98
+ {:default => @messages[message]}
99
+ end
100
+ end
101
+
102
+ version '2.0 - 2.3.1' do
103
+ def self.active?
104
+ ::ActiveRecord::VERSION::MAJOR == 2 && (::ActiveRecord::VERSION::MINOR < 3 || ::ActiveRecord::VERSION::TINY < 2)
105
+ end
106
+
107
+ def ancestors_for(klass)
108
+ klass.self_and_descendents_from_active_record
109
+ end
110
+ end
111
+
112
+ version '2.3.2 - 2.3.x' do
113
+ def self.active?
114
+ ::ActiveRecord::VERSION::MAJOR == 2 && ::ActiveRecord::VERSION::MINOR == 3 && ::ActiveRecord::VERSION::TINY >= 2
115
+ end
116
+
117
+ def ancestors_for(klass)
118
+ klass.self_and_descendants_from_active_record
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,100 @@
1
+ module StateMachine
2
+ module Integrations
3
+ # Provides a set of base helpers for managing individual integrations
4
+ module Base
5
+ module ClassMethods
6
+ # The default options to use for state machines using this integration
7
+ attr_reader :defaults
8
+
9
+ # The name of the integration
10
+ def integration_name
11
+ @integration_name ||= begin
12
+ name = self.name.split('::').last
13
+ name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
14
+ name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
15
+ name.downcase!
16
+ name.to_sym
17
+ end
18
+ end
19
+
20
+ # Whether this integration is available for the current library. This
21
+ # is only true if the ORM that the integration is for is currently
22
+ # defined.
23
+ def available?
24
+ matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
25
+ end
26
+
27
+ # The list of ancestor names that cause this integration to matched.
28
+ def matching_ancestors
29
+ []
30
+ end
31
+
32
+ # Whether the integration should be used for the given class.
33
+ def matches?(klass)
34
+ matches_ancestors?(klass.ancestors.map {|ancestor| ancestor.name})
35
+ end
36
+
37
+ # Whether the integration should be used for the given list of ancestors.
38
+ def matches_ancestors?(ancestors)
39
+ (ancestors & matching_ancestors).any?
40
+ end
41
+
42
+ # Tracks the various version overrides for an integration
43
+ def versions
44
+ @versions ||= []
45
+ end
46
+
47
+ # Creates a new version override for an integration. When this
48
+ # integration is activated, each version that is marked as active will
49
+ # also extend the integration.
50
+ #
51
+ # == Example
52
+ #
53
+ # module StateMachine
54
+ # module Integrations
55
+ # module ORMLibrary
56
+ # version '0.2.x - 0.3.x' do
57
+ # def self.active?
58
+ # ::ORMLibrary::VERSION >= '0.2.0' && ::ORMLibrary::VERSION < '0.4.0'
59
+ # end
60
+ #
61
+ # def invalidate(object, attribute, message, values = [])
62
+ # # Override here...
63
+ # end
64
+ # end
65
+ # end
66
+ # end
67
+ # end
68
+ #
69
+ # In the above example, a version override is defined for the ORMLibrary
70
+ # integration when the version is between 0.2.x and 0.3.x.
71
+ def version(name, &block)
72
+ versions << mod = Module.new(&block)
73
+ mod
74
+ end
75
+
76
+ # The path to the locale file containing translations for this
77
+ # integration. This file will only exist for integrations that actually
78
+ # support i18n.
79
+ def locale_path
80
+ path = "#{File.dirname(__FILE__)}/#{integration_name}/locale.rb"
81
+ path if File.exist?(path)
82
+ end
83
+
84
+ # Extends the given object with any version overrides that are currently
85
+ # active
86
+ def extended(base)
87
+ versions.each do |version|
88
+ base.extend(version) if version.active?
89
+ end
90
+ end
91
+ end
92
+
93
+ extend ClassMethods
94
+
95
+ def self.included(base) #:nodoc:
96
+ base.class_eval { extend ClassMethods }
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,511 @@
1
+ module StateMachine
2
+ module Integrations #:nodoc:
3
+ # Adds support for integrating state machines with DataMapper resources.
4
+ #
5
+ # == Examples
6
+ #
7
+ # Below is an example of a simple state machine defined within a
8
+ # DataMapper resource:
9
+ #
10
+ # class Vehicle
11
+ # include DataMapper::Resource
12
+ #
13
+ # property :id, Serial
14
+ # property :name, String
15
+ # property :state, String
16
+ #
17
+ # state_machine :initial => :parked do
18
+ # event :ignite do
19
+ # transition :parked => :idling
20
+ # end
21
+ # end
22
+ # end
23
+ #
24
+ # The examples in the sections below will use the above class as a
25
+ # reference.
26
+ #
27
+ # == Actions
28
+ #
29
+ # By default, the action that will be invoked when a state is transitioned
30
+ # is the +save+ action. This will cause the resource to save the changes
31
+ # made to the state machine's attribute. *Note* that if any other changes
32
+ # were made to the resource prior to transition, then those changes will
33
+ # be saved as well.
34
+ #
35
+ # For example,
36
+ #
37
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
38
+ # vehicle.name = 'Ford Explorer'
39
+ # vehicle.ignite # => true
40
+ # vehicle.reload # => #<Vehicle id=1 name="Ford Explorer" state="idling">
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 DataMapper, these automated events are run in the following order:
50
+ # * before validation - If validation feature loaded, run before callbacks and persist new states, then validate
51
+ # * before save - If validation feature was skipped/not loaded, 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 # => #<DataMapper::Validate::ValidationErrors:0xb7a48b54 @errors={"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
84
+ # include DataMapper::Resource
85
+ # ...
86
+ #
87
+ # state_machine do
88
+ # ...
89
+ # end
90
+ # protected :state_event
91
+ # end
92
+ #
93
+ # If you want to only have *some* events be able to fire via mass-assignment,
94
+ # you can build two state machines (one public and one protected) like so:
95
+ #
96
+ # class Vehicle
97
+ # include DataMapper::Resource
98
+ # ...
99
+ #
100
+ # state_machine do
101
+ # # Define private events here
102
+ # end
103
+ # protected :state_event= # Prevent access to events in the first machine
104
+ #
105
+ # # Allow both machines to share the same state
106
+ # state_machine :public_state, :attribute => :state do
107
+ # # Define public events here
108
+ # end
109
+ # end
110
+ #
111
+ # === Within DataMapper Hooks
112
+ #
113
+ # DataMapper protects against the potential for system stack errors resulting
114
+ # from infinite loops by preventing records from being saved multiple times
115
+ # within save hooks. You need to be acutely aware of this when interacting
116
+ # with state_machine within save hooks. There are two things to keep in mind:
117
+ #
118
+ # 1. You cannot run a state_machine event during an `after :save/:create` hook.
119
+ # 2. If you need to run a state_machine event during a `before :save/:create/etc.`
120
+ # hook, then you have to force the machine's action to be skipped by passing
121
+ # `false` in as an argument to the event.
122
+ #
123
+ # For example:
124
+ #
125
+ # class Vehicle
126
+ # include DataMapper::Resource
127
+ # ...
128
+ #
129
+ # state_machine :initial => :parked do
130
+ # event :ignite do
131
+ # transition :parked => :idling
132
+ # end
133
+ # end
134
+ #
135
+ # # This will allow the event to transition without attempting to save a second time
136
+ # before :create { ignite(false) }
137
+ #
138
+ # # This will never work because DataMapper will refuse to save the
139
+ # # changes since we're still inside of a transaction
140
+ # # after :create { ignite }
141
+ # end
142
+ #
143
+ # While the above will work, in reality you should typically just set the
144
+ # `state_event` attribute in `#initialize` to automatically transition an
145
+ # object on creation.
146
+ #
147
+ # == Transactions
148
+ #
149
+ # By default, the use of transactions during an event transition is
150
+ # turned off to be consistent with DataMapper. This means that if
151
+ # changes are made to the database during a before callback, but the
152
+ # transition fails to complete, those changes will *not* be rolled back.
153
+ #
154
+ # For example,
155
+ #
156
+ # class Message
157
+ # include DataMapper::Resource
158
+ #
159
+ # property :id, Serial
160
+ # property :content, String
161
+ # end
162
+ #
163
+ # Vehicle.state_machine do
164
+ # before_transition do |transition|
165
+ # Message.create(:content => transition.inspect)
166
+ # throw :halt
167
+ # end
168
+ # end
169
+ #
170
+ # vehicle = Vehicle.create # => #<Vehicle id=1 name=nil state="parked">
171
+ # vehicle.ignite # => false
172
+ # Message.all.count # => 1
173
+ #
174
+ # To turn on transactions:
175
+ #
176
+ # class Vehicle
177
+ # include DataMapper::Resource
178
+ # ...
179
+ #
180
+ # state_machine :initial => :parked, :use_transactions => true do
181
+ # ...
182
+ # end
183
+ # end
184
+ #
185
+ # == Validation errors
186
+ #
187
+ # If an event fails to successfully fire because there are no matching
188
+ # transitions for the current record, a validation error is added to the
189
+ # record's state attribute to help in determining why it failed and for
190
+ # reporting via the UI.
191
+ #
192
+ # For example,
193
+ #
194
+ # vehicle = Vehicle.create(:state => 'idling') # => #<Vehicle id=1 name=nil state="idling">
195
+ # vehicle.ignite # => false
196
+ # vehicle.errors.full_messages # => ["cannot transition via \"ignite\""]
197
+ #
198
+ # If an event fails to fire because of a validation error on the record and
199
+ # *not* because a matching transition was not available, no error messages
200
+ # will be added to the state attribute.
201
+ #
202
+ # In addition, if you're using the <tt>ignite!</tt> version of the event,
203
+ # then the failure reason (such as the current validation errors) will be
204
+ # included in the exception that gets raised when the event fails. For
205
+ # example, assuming there's a validation on a field called +name+ on the class:
206
+ #
207
+ # vehicle = Vehicle.new
208
+ # vehicle.ignite! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
209
+ #
210
+ # == Scopes
211
+ #
212
+ # To assist in filtering models with specific states, a series of class
213
+ # methods are defined on the model for finding records with or without a
214
+ # particular set of states.
215
+ #
216
+ # These named scopes are the functional equivalent of the following
217
+ # definitions:
218
+ #
219
+ # class Vehicle
220
+ # include DataMapper::Resource
221
+ #
222
+ # property :id, Serial
223
+ # property :state, String
224
+ #
225
+ # class << self
226
+ # def with_states(*states)
227
+ # all(:state => states.flatten)
228
+ # end
229
+ # alias_method :with_state, :with_states
230
+ #
231
+ # def without_states(*states)
232
+ # all(:state.not => states.flatten)
233
+ # end
234
+ # alias_method :without_state, :without_states
235
+ # end
236
+ # end
237
+ #
238
+ # *Note*, however, that the states are converted to their stored values
239
+ # before being passed into the query.
240
+ #
241
+ # Because of the way scopes work in DataMapper, they can be chained like
242
+ # so:
243
+ #
244
+ # Vehicle.with_state(:parked).all(:order => [:id.desc])
245
+ #
246
+ # Note that states can also be referenced by the string version of their
247
+ # name:
248
+ #
249
+ # Vehicle.with_state('parked')
250
+ #
251
+ # == Callbacks / Observers
252
+ #
253
+ # All before/after transition callbacks defined for DataMapper resources
254
+ # behave in the same way that other DataMapper hooks behave. Rather than
255
+ # passing in the record as an argument to the callback, the callback is
256
+ # instead bound to the object and evaluated within its context.
257
+ #
258
+ # For example,
259
+ #
260
+ # class Vehicle
261
+ # include DataMapper::Resource
262
+ #
263
+ # property :id, Serial
264
+ # property :state, String
265
+ #
266
+ # state_machine :initial => :parked do
267
+ # before_transition any => :idling do
268
+ # put_on_seatbelt
269
+ # end
270
+ #
271
+ # before_transition do |transition|
272
+ # # log message
273
+ # end
274
+ #
275
+ # event :ignite do
276
+ # transition :parked => :idling
277
+ # end
278
+ # end
279
+ #
280
+ # def put_on_seatbelt
281
+ # ...
282
+ # end
283
+ # end
284
+ #
285
+ # Note, also, that the transition can be accessed by simply defining
286
+ # additional arguments in the callback block.
287
+ #
288
+ # In addition to support for DataMapper-like hooks, there is additional
289
+ # support for DataMapper observers. See StateMachine::Integrations::DataMapper::Observer
290
+ # for more information.
291
+ #
292
+ # === Failure callbacks
293
+ #
294
+ # +after_failure+ callbacks allow you to execute behaviors when a transition
295
+ # is allowed, but fails to save. This could be useful for something like
296
+ # auditing transition attempts. Since callbacks run within transactions in
297
+ # DataMapper, a save failure will cause any records that get created in
298
+ # your callback to roll back. *Note* that this is only a problem if the
299
+ # machine is configured to use transactions. If it is, you can work around
300
+ # this issue like so:
301
+ #
302
+ # DataMapper.setup(:default, 'mysql://localhost/app')
303
+ # DataMapper.setup(:logs, 'mysql://localhost/app')
304
+ #
305
+ # class TransitionLog
306
+ # include DataMapper::Resource
307
+ # end
308
+ #
309
+ # class Vehicle < ActiveRecord::Base
310
+ # include DataMapper::Resource
311
+ #
312
+ # state_machine :use_transactions => true do
313
+ # after_failure do |transition|
314
+ # DataMapper.repository(:logs) do
315
+ # TransitionLog.create(:vehicle => vehicle, :transition => transition)
316
+ # end
317
+ # end
318
+ #
319
+ # ...
320
+ # end
321
+ # end
322
+ #
323
+ # The failure callback creates +TransitionLog+ records using a second
324
+ # connection to the database, allowing them to be saved without being
325
+ # affected by rollbacks in the +Vehicle+ resource's transaction.
326
+ #
327
+ # === Callback Order
328
+ #
329
+ # Callbacks occur in the following order. Callbacks specific to state_machine
330
+ # are bolded. The remaining callbacks are part of ActiveRecord.
331
+ #
332
+ # * (-) save
333
+ # * (-) begin transaction (if enabled)
334
+ # * (1) *before_transition*
335
+ # * (2) before :valid?
336
+ # * (-) valid?
337
+ # * (3) after :valid?
338
+ # * (4) before :save
339
+ # * (-) save
340
+ # * (5) before :create
341
+ # * (-) create
342
+ # * (6) after :create
343
+ # * (7) after :save
344
+ # * (8) *after_transition*
345
+ # * (-) end transaction (if enabled)
346
+ module DataMapper
347
+ include Base
348
+
349
+ require 'state_machine/integrations/data_mapper/versions'
350
+
351
+ # The default options to use for state machines using this integration
352
+ @defaults = {:action => :save, :use_transactions => false}
353
+
354
+ # Classes that include DataMapper::Resource will automatically use the
355
+ # DataMapper integration.
356
+ def self.matching_ancestors
357
+ %w(DataMapper::Resource)
358
+ end
359
+
360
+ # Loads additional files specific to DataMapper
361
+ def self.extended(base) #:nodoc:
362
+ require 'dm-core/version' unless ::DataMapper.const_defined?('VERSION')
363
+ super
364
+ end
365
+
366
+ # Adds a validation error to the given object
367
+ def invalidate(object, attribute, message, values = [])
368
+ object.errors.add(self.attribute(attribute), generate_message(message, values)) if supports_validations?
369
+ end
370
+
371
+ # Describes the current validation errors on the given object. If none
372
+ # are specific, then the default error is interpeted as a "halt".
373
+ def errors_for(object)
374
+ if object.errors.empty?
375
+ 'Transition halted'
376
+ else
377
+ errors = []
378
+ object.errors.each_pair do |field_name, field_errors|
379
+ field_errors.each {|error| errors << "#{field_name} #{error}"}
380
+ end
381
+ errors * ', '
382
+ end
383
+ end
384
+
385
+ # Resets any errors previously added when invalidating the given object
386
+ def reset(object)
387
+ object.errors.clear if supports_validations?
388
+ end
389
+
390
+ protected
391
+ # Initializes class-level extensions and defaults for this machine
392
+ def after_initialize
393
+ super
394
+ load_observer_extensions
395
+ end
396
+
397
+ # Loads extensions to DataMapper's Observers
398
+ def load_observer_extensions
399
+ require 'state_machine/integrations/data_mapper/observer' if ::DataMapper.const_defined?('Observer')
400
+ end
401
+
402
+ # Is validation support currently loaded?
403
+ def supports_validations?
404
+ @supports_validations ||= ::DataMapper.const_defined?('Validate')
405
+ end
406
+
407
+ # Gets the db default for the machine's attribute
408
+ def owner_class_attribute_default
409
+ attribute_property && attribute_property.default
410
+ end
411
+
412
+ # Gets the property for this machine's attribute (if it exists)
413
+ def attribute_property
414
+ owner_class.properties.detect {|property| property.name == attribute}
415
+ end
416
+
417
+ # Pluralizes the name using the built-in inflector
418
+ def pluralize(word)
419
+ ::DataMapper::Inflector.pluralize(word.to_s)
420
+ end
421
+
422
+ # Defines an initialization hook into the owner class for setting the
423
+ # initial state of the machine *before* any attributes are set on the
424
+ # object
425
+ def define_state_initializer
426
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
427
+ def initialize(*args)
428
+ self.class.state_machines.initialize_states(self, :static => :force) { super }
429
+ end
430
+ end_eval
431
+ end
432
+
433
+ # Skips defining reader/writer methods since this is done automatically
434
+ def define_state_accessor
435
+ owner_class.property(attribute, String) unless attribute_property
436
+
437
+ if supports_validations?
438
+ name = self.name
439
+ owner_class.validates_with_block(attribute) do
440
+ machine = self.class.state_machine(name)
441
+ machine.states.match(self) ? true : [false, machine.generate_message(:invalid)]
442
+ end
443
+ end
444
+ end
445
+
446
+ # Adds hooks into validation for automatically firing events
447
+ def define_action_helpers
448
+ if action_hook == :save
449
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
450
+ def save(*)
451
+ result = self.class.state_machines.transitions(self, :save).perform { super }
452
+ assert_save_successful(:save, result)
453
+ result
454
+ end
455
+
456
+ def save!(*)
457
+ result = self.class.state_machines.transitions(self, :save).perform { super }
458
+ assert_save_successful(:save!, result)
459
+ result
460
+ end
461
+
462
+ def save_self(*)
463
+ self.class.state_machines.transitions(self, :save).perform { super }
464
+ end
465
+ end_eval
466
+
467
+ define_validation_hook
468
+ else
469
+ super
470
+ end
471
+ end
472
+
473
+ # Adds hooks into validation for automatically firing events
474
+ def define_validation_hook
475
+ if supports_validations?
476
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
477
+ def valid?(*)
478
+ self.class.state_machines.transitions(self, :save, :after => false).perform { super }
479
+ end
480
+ end_eval
481
+ end
482
+ end
483
+
484
+ # Creates a scope for finding records *with* a particular state or
485
+ # states for the attribute
486
+ def create_with_scope(name)
487
+ lambda {|resource, values| resource.all(attribute => values)}
488
+ end
489
+
490
+ # Creates a scope for finding records *without* a particular state or
491
+ # states for the attribute
492
+ def create_without_scope(name)
493
+ lambda {|resource, values| resource.all(attribute.to_sym.not => values)}
494
+ end
495
+
496
+ # Runs a new database transaction, rolling back any changes if the
497
+ # yielded block fails (i.e. returns false).
498
+ def transaction(object)
499
+ object.class.transaction {|t| t.rollback unless yield}
500
+ end
501
+
502
+ # Creates a new callback in the callback chain, always ensuring that
503
+ # it's configured to bind to the object as this is the convention for
504
+ # DataMapper/Extlib callbacks
505
+ def add_callback(type, options, &block)
506
+ options[:bind_to_object] = true
507
+ super
508
+ end
509
+ end
510
+ end
511
+ end