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,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