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,138 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/eval_helpers'
3
+
4
+ module StateMachine
5
+ # A method was called in an invalid state context
6
+ class InvalidContext < Error
7
+ end
8
+
9
+ # Represents a module which will get evaluated within the context of a state.
10
+ #
11
+ # Class-level methods are proxied to the owner class, injecting a custom
12
+ # <tt>:if</tt> condition along with method. This assumes that the method has
13
+ # support for a set of configuration options, including <tt>:if</tt>. This
14
+ # condition will check that the object's state matches this context's state.
15
+ #
16
+ # Instance-level methods are used to define state-driven behavior on the
17
+ # state's owner class.
18
+ #
19
+ # == Examples
20
+ #
21
+ # class Vehicle
22
+ # class << self
23
+ # attr_accessor :validations
24
+ #
25
+ # def validate(options, &block)
26
+ # validations << options
27
+ # end
28
+ # end
29
+ #
30
+ # self.validations = []
31
+ # attr_accessor :state, :simulate
32
+ #
33
+ # def moving?
34
+ # self.class.validations.all? {|validation| validation[:if].call(self)}
35
+ # end
36
+ # end
37
+ #
38
+ # In the above class, a simple set of validation behaviors have been defined.
39
+ # Each validation consists of a configuration like so:
40
+ #
41
+ # Vehicle.validate :unless => :simulate
42
+ # Vehicle.validate :if => lambda {|vehicle| ...}
43
+ #
44
+ # In order to scope validations to a particular state context, the class-level
45
+ # +validate+ method can be invoked like so:
46
+ #
47
+ # machine = StateMachine::Machine.new(Vehicle)
48
+ # context = StateMachine::StateContext.new(machine.state(:first_gear))
49
+ # context.validate(:unless => :simulate)
50
+ #
51
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7ce491c @simulate=nil, @state=nil>
52
+ # vehicle.moving? # => false
53
+ #
54
+ # vehicle.state = 'first_gear'
55
+ # vehicle.moving? # => true
56
+ #
57
+ # vehicle.simulate = true
58
+ # vehicle.moving? # => false
59
+ class StateContext < Module
60
+ include Assertions
61
+ include EvalHelpers
62
+
63
+ # The state machine for which this context's state is defined
64
+ attr_reader :machine
65
+
66
+ # The state that must be present in an object for this context to be active
67
+ attr_reader :state
68
+
69
+ # Creates a new context for the given state
70
+ def initialize(state)
71
+ @state = state
72
+ @machine = state.machine
73
+
74
+ state_name = state.name
75
+ machine_name = machine.name
76
+ @condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
77
+ end
78
+
79
+ # Creates a new transition that determines what to change the current state
80
+ # to when an event fires from this state.
81
+ #
82
+ # Since this transition is being defined within a state context, you do
83
+ # *not* need to specify the <tt>:from</tt> option for the transition. For
84
+ # example:
85
+ #
86
+ # state_machine do
87
+ # state :parked do
88
+ # transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
89
+ # transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
90
+ # end
91
+ # end
92
+ #
93
+ # See StateMachine::Machine#transition for a description of the possible
94
+ # configurations for defining transitions.
95
+ def transition(options)
96
+ assert_valid_keys(options, :from, :to, :on, :if, :unless)
97
+ raise ArgumentError, 'Must specify :on event' unless options[:on]
98
+ raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
99
+
100
+ machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
101
+ end
102
+
103
+ # Hooks in condition-merging to methods that don't exist in this module
104
+ def method_missing(*args, &block)
105
+ # Get the configuration
106
+ if args.last.is_a?(Hash)
107
+ options = args.last
108
+ else
109
+ args << options = {}
110
+ end
111
+
112
+ # Get any existing condition that may need to be merged
113
+ if_condition = options.delete(:if)
114
+ unless_condition = options.delete(:unless)
115
+
116
+ # Provide scope access to configuration in case the block is evaluated
117
+ # within the object instance
118
+ proxy = self
119
+ proxy_condition = @condition
120
+
121
+ # Replace the configuration condition with the one configured for this
122
+ # proxy, merging together any existing conditions
123
+ options[:if] = lambda do |*condition_args|
124
+ # Block may be executed within the context of the actual object, so
125
+ # it'll either be the first argument or the executing context
126
+ object = condition_args.first || self
127
+
128
+ proxy.evaluate_method(object, proxy_condition) &&
129
+ Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
130
+ !Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
131
+ end
132
+
133
+ # Evaluate the method on the owner class with the condition proxied
134
+ # through
135
+ machine.owner_class.send(*args, &block)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,470 @@
1
+ require 'state_machine/transition_collection'
2
+ require 'state_machine/error'
3
+
4
+ module StateMachine
5
+ # An invalid transition was attempted
6
+ class InvalidTransition < Error
7
+ # The machine attempting to be transitioned
8
+ attr_reader :machine
9
+
10
+ # The current state value for the machine
11
+ attr_reader :from
12
+
13
+ def initialize(object, machine, event) #:nodoc:
14
+ @machine = machine
15
+ @from_state = machine.states.match!(object)
16
+ @from = machine.read(object, :state)
17
+ @event = machine.events.fetch(event)
18
+ errors = machine.errors_for(object)
19
+
20
+ message = "Cannot transition #{machine.name} via :#{self.event} from #{from_name.inspect}"
21
+ message << " (Reason(s): #{errors})" unless errors.empty?
22
+ super(object, message)
23
+ end
24
+
25
+ # The event that triggered the failed transition
26
+ def event
27
+ @event.name
28
+ end
29
+
30
+ # The fully-qualified name of the event that triggered the failed transition
31
+ def qualified_event
32
+ @event.qualified_name
33
+ end
34
+
35
+ # The name for the current state
36
+ def from_name
37
+ @from_state.name
38
+ end
39
+
40
+ # The fully-qualified name for the current state
41
+ def qualified_from_name
42
+ @from_state.qualified_name
43
+ end
44
+ end
45
+
46
+ # A set of transition failed to run in parallel
47
+ class InvalidParallelTransition < Error
48
+ # The set of events that failed the transition(s)
49
+ attr_reader :events
50
+
51
+ def initialize(object, events) #:nodoc:
52
+ @events = events
53
+
54
+ super(object, "Cannot run events in parallel: #{events * ', '}")
55
+ end
56
+ end
57
+
58
+ # A transition represents a state change for a specific attribute.
59
+ #
60
+ # Transitions consist of:
61
+ # * An event
62
+ # * A starting state
63
+ # * An ending state
64
+ class Transition
65
+ # The object being transitioned
66
+ attr_reader :object
67
+
68
+ # The state machine for which this transition is defined
69
+ attr_reader :machine
70
+
71
+ # The original state value *before* the transition
72
+ attr_reader :from
73
+
74
+ # The new state value *after* the transition
75
+ attr_reader :to
76
+
77
+ # The arguments passed in to the event that triggered the transition
78
+ # (does not include the +run_action+ boolean argument if specified)
79
+ attr_accessor :args
80
+
81
+ # The result of invoking the action associated with the machine
82
+ attr_reader :result
83
+
84
+ # Whether the transition is only existing temporarily for the object
85
+ attr_writer :transient
86
+
87
+ # Determines whether the curreny ruby implementation supports pausing and
88
+ # resuming transitions
89
+ def self.pause_supported?
90
+ !defined?(RUBY_ENGINE) || %w(ruby maglev).include?(RUBY_ENGINE)
91
+ end
92
+
93
+ # Creates a new, specific transition
94
+ def initialize(object, machine, event, from_name, to_name, read_state = true) #:nodoc:
95
+ @object = object
96
+ @machine = machine
97
+ @args = []
98
+ @transient = false
99
+ @resume_block = nil
100
+
101
+ @event = machine.events.fetch(event)
102
+ @from_state = machine.states.fetch(from_name)
103
+ @from = read_state ? machine.read(object, :state) : @from_state.value
104
+ @to_state = machine.states.fetch(to_name)
105
+ @to = @to_state.value
106
+
107
+ reset
108
+ end
109
+
110
+ # The attribute which this transition's machine is defined for
111
+ def attribute
112
+ machine.attribute
113
+ end
114
+
115
+ # The action that will be run when this transition is performed
116
+ def action
117
+ machine.action
118
+ end
119
+
120
+ # The event that triggered the transition
121
+ def event
122
+ @event.name
123
+ end
124
+
125
+ # The fully-qualified name of the event that triggered the transition
126
+ def qualified_event
127
+ @event.qualified_name
128
+ end
129
+
130
+ # The human-readable name of the event that triggered the transition
131
+ def human_event
132
+ @event.human_name(@object.class)
133
+ end
134
+
135
+ # The state name *before* the transition
136
+ def from_name
137
+ @from_state.name
138
+ end
139
+
140
+ # The fully-qualified state name *before* the transition
141
+ def qualified_from_name
142
+ @from_state.qualified_name
143
+ end
144
+
145
+ # The human-readable state name *before* the transition
146
+ def human_from_name
147
+ @from_state.human_name(@object.class)
148
+ end
149
+
150
+ # The new state name *after* the transition
151
+ def to_name
152
+ @to_state.name
153
+ end
154
+
155
+ # The new fully-qualified state name *after* the transition
156
+ def qualified_to_name
157
+ @to_state.qualified_name
158
+ end
159
+
160
+ # The new human-readable state name *after* the transition
161
+ def human_to_name
162
+ @to_state.human_name(@object.class)
163
+ end
164
+
165
+ # Does this transition represent a loopback (i.e. the from and to state
166
+ # are the same)
167
+ #
168
+ # == Example
169
+ #
170
+ # machine = StateMachine.new(Vehicle)
171
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
172
+ # StateMachine::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
173
+ def loopback?
174
+ from_name == to_name
175
+ end
176
+
177
+ # Is this transition existing for a short period only? If this is set, it
178
+ # indicates that the transition (or the event backing it) should not be
179
+ # written to the object if it fails.
180
+ def transient?
181
+ @transient
182
+ end
183
+
184
+ # A hash of all the core attributes defined for this transition with their
185
+ # names as keys and values of the attributes as values.
186
+ #
187
+ # == Example
188
+ #
189
+ # machine = StateMachine.new(Vehicle)
190
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
191
+ # transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
192
+ def attributes
193
+ @attributes ||= {:object => object, :attribute => attribute, :event => event, :from => from, :to => to}
194
+ end
195
+
196
+ # Runs the actual transition and any before/after callbacks associated
197
+ # with the transition. The action associated with the transition/machine
198
+ # can be skipped by passing in +false+.
199
+ #
200
+ # == Examples
201
+ #
202
+ # class Vehicle
203
+ # state_machine :action => :save do
204
+ # ...
205
+ # end
206
+ # end
207
+ #
208
+ # vehicle = Vehicle.new
209
+ # transition = StateMachine::Transition.new(vehicle, machine, :ignite, :parked, :idling)
210
+ # transition.perform # => Runs the +save+ action after setting the state attribute
211
+ # transition.perform(false) # => Only sets the state attribute
212
+ # transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
213
+ # transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
214
+ def perform(*args)
215
+ run_action = [true, false].include?(args.last) ? args.pop : true
216
+ self.args = args
217
+
218
+ # Run the transition
219
+ !!TransitionCollection.new([self], :actions => run_action).perform
220
+ end
221
+
222
+ # Runs a block within a transaction for the object being transitioned.
223
+ # By default, transactions are a no-op unless otherwise defined by the
224
+ # machine's integration.
225
+ def within_transaction
226
+ machine.within_transaction(object) do
227
+ yield
228
+ end
229
+ end
230
+
231
+ # Runs the before / after callbacks for this transition. If a block is
232
+ # provided, then it will be executed between the before and after callbacks.
233
+ #
234
+ # Configuration options:
235
+ # * +before+ - Whether to run before callbacks.
236
+ # * +after+ - Whether to run after callbacks. If false, then any around
237
+ # callbacks will be paused until called again with +after+ enabled.
238
+ # Default is true.
239
+ #
240
+ # This will return true if all before callbacks gets executed. After
241
+ # callbacks will not have an effect on the result.
242
+ def run_callbacks(options = {}, &block)
243
+ options = {:before => true, :after => true}.merge(options)
244
+ @success = false
245
+
246
+ halted = pausable { before(options[:after], &block) } if options[:before]
247
+
248
+ # After callbacks are only run if:
249
+ # * An around callback didn't halt after yielding
250
+ # * They're enabled or the run didn't succeed
251
+ after if !(@before_run && halted) && (options[:after] || !@success)
252
+
253
+ @before_run
254
+ end
255
+
256
+ # Transitions the current value of the state to that specified by the
257
+ # transition. Once the state is persisted, it cannot be persisted again
258
+ # until this transition is reset.
259
+ #
260
+ # == Example
261
+ #
262
+ # class Vehicle
263
+ # state_machine do
264
+ # event :ignite do
265
+ # transition :parked => :idling
266
+ # end
267
+ # end
268
+ # end
269
+ #
270
+ # vehicle = Vehicle.new
271
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
272
+ # transition.persist
273
+ #
274
+ # vehicle.state # => 'idling'
275
+ def persist
276
+ unless @persisted
277
+ machine.write(object, :state, to)
278
+ @persisted = true
279
+ end
280
+ end
281
+
282
+ # Rolls back changes made to the object's state via this transition. This
283
+ # will revert the state back to the +from+ value.
284
+ #
285
+ # == Example
286
+ #
287
+ # class Vehicle
288
+ # state_machine :initial => :parked do
289
+ # event :ignite do
290
+ # transition :parked => :idling
291
+ # end
292
+ # end
293
+ # end
294
+ #
295
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
296
+ # transition = StateMachine::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
297
+ #
298
+ # # Persist the new state
299
+ # vehicle.state # => "parked"
300
+ # transition.persist
301
+ # vehicle.state # => "idling"
302
+ #
303
+ # # Roll back to the original state
304
+ # transition.rollback
305
+ # vehicle.state # => "parked"
306
+ def rollback
307
+ reset
308
+ machine.write(object, :state, from)
309
+ end
310
+
311
+ # Resets any tracking of which callbacks have already been run and whether
312
+ # the state has already been persisted
313
+ def reset
314
+ @before_run = @persisted = @after_run = false
315
+ @paused_block = nil
316
+ end
317
+
318
+ # Determines equality of transitions by testing whether the object, states,
319
+ # and event involved in the transition are equal
320
+ def ==(other)
321
+ other.instance_of?(self.class) &&
322
+ other.object == object &&
323
+ other.machine == machine &&
324
+ other.from_name == from_name &&
325
+ other.to_name == to_name &&
326
+ other.event == event
327
+ end
328
+
329
+ # Generates a nicely formatted description of this transitions's contents.
330
+ #
331
+ # For example,
332
+ #
333
+ # transition = StateMachine::Transition.new(object, machine, :ignite, :parked, :idling)
334
+ # transition # => #<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
335
+ def inspect
336
+ "#<#{self.class} #{%w(attribute event from from_name to to_name).map {|attr| "#{attr}=#{send(attr).inspect}"} * ' '}>"
337
+ end
338
+
339
+ private
340
+ # Runs a block that may get paused. If the block doesn't pause, then
341
+ # execution will continue as normal. If the block gets paused, then it
342
+ # will take care of switching the execution context when it's resumed.
343
+ #
344
+ # This will return true if the given block halts for a reason other than
345
+ # getting paused.
346
+ def pausable
347
+ begin
348
+ halted = !catch(:halt) { yield; true }
349
+ rescue Exception => error
350
+ raise unless @resume_block
351
+ end
352
+
353
+ if @resume_block
354
+ @resume_block.call(halted, error)
355
+ else
356
+ halted
357
+ end
358
+ end
359
+
360
+ # Pauses the current callback execution. This should only occur within
361
+ # around callbacks when the remainder of the callback will be executed at
362
+ # a later point in time.
363
+ def pause
364
+ raise ArgumentError, 'around_transition callbacks cannot be called in multiple execution contexts in java implementations of Ruby. Use before/after_transitions instead.' unless self.class.pause_supported?
365
+
366
+ unless @resume_block
367
+ require 'continuation' unless defined?(callcc)
368
+ callcc do |block|
369
+ @paused_block = block
370
+ throw :halt, true
371
+ end
372
+ end
373
+ end
374
+
375
+ # Resumes the execution of a previously paused callback execution. Once
376
+ # the paused callbacks complete, the current execution will continue.
377
+ def resume
378
+ if @paused_block
379
+ halted, error = callcc do |block|
380
+ @resume_block = block
381
+ @paused_block.call
382
+ end
383
+
384
+ @resume_block = @paused_block = nil
385
+
386
+ raise error if error
387
+ !halted
388
+ else
389
+ true
390
+ end
391
+ end
392
+
393
+ # Runs the machine's +before+ callbacks for this transition. Only
394
+ # callbacks that are configured to match the event, from state, and to
395
+ # state will be invoked.
396
+ #
397
+ # Once the callbacks are run, they cannot be run again until this transition
398
+ # is reset.
399
+ def before(complete = true, index = 0, &block)
400
+ unless @before_run
401
+ while callback = machine.callbacks[:before][index]
402
+ index += 1
403
+
404
+ if callback.type == :around
405
+ # Around callback: need to handle recursively. Execution only gets
406
+ # paused if:
407
+ # * The block fails and the callback doesn't run on failures OR
408
+ # * The block succeeds, but after callbacks are disabled (in which
409
+ # case a continuation is stored for later execution)
410
+ return if catch(:cancel) do
411
+ callback.call(object, context, self) do
412
+ before(complete, index, &block)
413
+
414
+ pause if @success && !complete
415
+ throw :cancel, true unless @success
416
+ end
417
+ end
418
+ else
419
+ # Normal before callback
420
+ callback.call(object, context, self)
421
+ end
422
+ end
423
+
424
+ @before_run = true
425
+ end
426
+
427
+ action = {:success => true}.merge(block_given? ? yield : {})
428
+ @result, @success = action[:result], action[:success]
429
+ end
430
+
431
+ # Runs the machine's +after+ callbacks for this transition. Only
432
+ # callbacks that are configured to match the event, from state, and to
433
+ # state will be invoked.
434
+ #
435
+ # Once the callbacks are run, they cannot be run again until this transition
436
+ # is reset.
437
+ #
438
+ # == Halting
439
+ #
440
+ # If any callback throws a <tt>:halt</tt> exception, it will be caught
441
+ # and the callback chain will be automatically stopped. However, this
442
+ # exception will not bubble up to the caller since +after+ callbacks
443
+ # should never halt the execution of a +perform+.
444
+ def after
445
+ unless @after_run
446
+ # First resume previously paused callbacks
447
+ if resume
448
+ catch(:halt) do
449
+ type = @success ? :after : :failure
450
+ machine.callbacks[type].each {|callback| callback.call(object, context, self)}
451
+ end
452
+ end
453
+
454
+ @after_run = true
455
+ end
456
+ end
457
+
458
+ # Gets a hash of the context defining this unique transition (including
459
+ # event, from state, and to state).
460
+ #
461
+ # == Example
462
+ #
463
+ # machine = StateMachine.new(Vehicle)
464
+ # transition = StateMachine::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
465
+ # transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
466
+ def context
467
+ @context ||= {:on => event, :from => from_name, :to => to_name}
468
+ end
469
+ end
470
+ end