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,90 @@
1
+ require 'state_machine/path'
2
+
3
+ module StateMachine
4
+ # Represents a collection of paths that are generated based on a set of
5
+ # requirements regarding what states to start and end on
6
+ class PathCollection < Array
7
+ include Assertions
8
+
9
+ # The object whose state machine is being walked
10
+ attr_reader :object
11
+
12
+ # The state machine these path are walking
13
+ attr_reader :machine
14
+
15
+ # The initial state to start each path from
16
+ attr_reader :from_name
17
+
18
+ # The target state for each path
19
+ attr_reader :to_name
20
+
21
+ # Creates a new collection of paths with the given requirements.
22
+ #
23
+ # Configuration options:
24
+ # * <tt>:from</tt> - The initial state to start from
25
+ # * <tt>:to</tt> - The target end state
26
+ # * <tt>:deep</tt> - Whether to enable deep searches for the target state.
27
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
28
+ # conditionals defined for each one
29
+ def initialize(object, machine, options = {})
30
+ options = {:deep => false, :from => machine.states.match!(object).name}.merge(options)
31
+ assert_valid_keys(options, :from, :to, :deep, :guard)
32
+
33
+ @object = object
34
+ @machine = machine
35
+ @from_name = machine.states.fetch(options[:from]).name
36
+ @to_name = options[:to] && machine.states.fetch(options[:to]).name
37
+ @guard = options[:guard]
38
+ @deep = options[:deep]
39
+
40
+ initial_paths.each {|path| walk(path)}
41
+ end
42
+
43
+ # Lists all of the states that can be transitioned from through the paths in
44
+ # this collection.
45
+ #
46
+ # For example,
47
+ #
48
+ # paths.from_states # => [:parked, :idling, :first_gear, ...]
49
+ def from_states
50
+ map {|path| path.from_states}.flatten.uniq
51
+ end
52
+
53
+ # Lists all of the states that can be transitioned to through the paths in
54
+ # this collection.
55
+ #
56
+ # For example,
57
+ #
58
+ # paths.to_states # => [:idling, :first_gear, :second_gear, ...]
59
+ def to_states
60
+ map {|path| path.to_states}.flatten.uniq
61
+ end
62
+
63
+ # Lists all of the events that can be fired through the paths in this
64
+ # collection.
65
+ #
66
+ # For example,
67
+ #
68
+ # paths.events # => [:park, :ignite, :shift_up, ...]
69
+ def events
70
+ map {|path| path.events}.flatten.uniq
71
+ end
72
+
73
+ private
74
+ # Gets the initial set of paths to walk
75
+ def initial_paths
76
+ machine.events.transitions_for(object, :from => from_name, :guard => @guard).map do |transition|
77
+ path = Path.new(object, machine, :target => to_name, :guard => @guard)
78
+ path << transition
79
+ path
80
+ end
81
+ end
82
+
83
+ # Walks down the given path. Each new path that matches the configured
84
+ # requirements will be added to this collection.
85
+ def walk(path)
86
+ self << path if path.complete?
87
+ path.walk {|next_path| walk(next_path)} unless to_name && path.complete? && !@deep
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,297 @@
1
+ require 'state_machine/assertions'
2
+ require 'state_machine/state_context'
3
+
4
+ module StateMachine
5
+ # A state defines a value that an attribute can be in after being transitioned
6
+ # 0 or more times. States can represent a value of any type in Ruby, though
7
+ # the most common (and default) type is String.
8
+ #
9
+ # In addition to defining the machine's value, a state can also define a
10
+ # behavioral context for an object when that object is in the state. See
11
+ # StateMachine::Machine#state for more information about how state-driven
12
+ # behavior can be utilized.
13
+ class State
14
+ include Assertions
15
+
16
+ # The state machine for which this state is defined
17
+ attr_accessor :machine
18
+
19
+ # The unique identifier for the state used in event and callback definitions
20
+ attr_reader :name
21
+
22
+ # The fully-qualified identifier for the state, scoped by the machine's
23
+ # namespace
24
+ attr_reader :qualified_name
25
+
26
+ # The human-readable name for the state
27
+ attr_writer :human_name
28
+
29
+ # The value that is written to a machine's attribute when an object
30
+ # transitions into this state
31
+ attr_writer :value
32
+
33
+ # Whether this state's value should be cached after being evaluated
34
+ attr_accessor :cache
35
+
36
+ # Whether or not this state is the initial state to use for new objects
37
+ attr_accessor :initial
38
+ alias_method :initial?, :initial
39
+
40
+ # A custom lambda block for determining whether a given value matches this
41
+ # state
42
+ attr_accessor :matcher
43
+
44
+ # Creates a new state within the context of the given machine.
45
+ #
46
+ # Configuration options:
47
+ # * <tt>:initial</tt> - Whether this state is the beginning state for the
48
+ # machine. Default is false.
49
+ # * <tt>:value</tt> - The value to store when an object transitions to this
50
+ # state. Default is the name (stringified).
51
+ # * <tt>:cache</tt> - If a dynamic value (via a lambda block) is being used,
52
+ # then setting this to true will cache the evaluated result
53
+ # * <tt>:if</tt> - Determines whether a value matches this state
54
+ # (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
55
+ # By default, the configured value is matched.
56
+ # * <tt>:human_name</tt> - The human-readable version of this state's name
57
+ def initialize(machine, name, options = {}) #:nodoc:
58
+ assert_valid_keys(options, :initial, :value, :cache, :if, :human_name)
59
+
60
+ @machine = machine
61
+ @name = name
62
+ @qualified_name = name && machine.namespace ? :"#{machine.namespace}_#{name}" : name
63
+ @human_name = options[:human_name] || (@name ? @name.to_s.tr('_', ' ') : 'nil')
64
+ @value = options.include?(:value) ? options[:value] : name && name.to_s
65
+ @cache = options[:cache]
66
+ @matcher = options[:if]
67
+ @initial = options[:initial] == true
68
+ @context = StateContext.new(self)
69
+
70
+ if name
71
+ conflicting_machines = machine.owner_class.state_machines.select {|other_name, other_machine| other_machine != machine && other_machine.states[qualified_name, :qualified_name]}
72
+
73
+ # Output a warning if another machine has a conflicting qualified name
74
+ # for a different attribute
75
+ if conflict = conflicting_machines.detect {|other_name, other_machine| other_machine.attribute != machine.attribute}
76
+ name, other_machine = conflict
77
+ warn "State #{qualified_name.inspect} for #{machine.name.inspect} is already defined in #{other_machine.name.inspect}"
78
+ elsif conflicting_machines.empty?
79
+ # Only bother adding predicates when another machine for the same
80
+ # attribute hasn't already done so
81
+ add_predicate
82
+ end
83
+ end
84
+ end
85
+
86
+ # Creates a copy of this state, excluding the context to prevent conflicts
87
+ # across different machines.
88
+ def initialize_copy(orig) #:nodoc:
89
+ super
90
+ @context = StateContext.new(self)
91
+ end
92
+
93
+ # Determines whether there are any states that can be transitioned to from
94
+ # this state. If there are none, then this state is considered *final*.
95
+ # Any objects in a final state will remain so forever given the current
96
+ # machine's definition.
97
+ def final?
98
+ !machine.events.any? do |event|
99
+ event.branches.any? do |branch|
100
+ branch.state_requirements.any? do |requirement|
101
+ requirement[:from].matches?(name) && !requirement[:to].matches?(name, :from => name)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # Transforms the state name into a more human-readable format, such as
108
+ # "first gear" instead of "first_gear"
109
+ def human_name(klass = @machine.owner_class)
110
+ @human_name.is_a?(Proc) ? @human_name.call(self, klass) : @human_name
111
+ end
112
+
113
+ # Generates a human-readable description of this state's name / value:
114
+ #
115
+ # For example,
116
+ #
117
+ # State.new(machine, :parked).description # => "parked"
118
+ # State.new(machine, :parked, :value => :parked).description # => "parked"
119
+ # State.new(machine, :parked, :value => nil).description # => "parked (nil)"
120
+ # State.new(machine, :parked, :value => 1).description # => "parked (1)"
121
+ # State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
122
+ #
123
+ # Configuration options:
124
+ # * <tt>:human_name</tt> - Whether to use this state's human name in the
125
+ # description or just the internal name
126
+ def description(options = {})
127
+ label = options[:human_name] ? human_name : name
128
+ description = label ? label.to_s : label.inspect
129
+ description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
130
+ description
131
+ end
132
+
133
+ # The value that represents this state. This will optionally evaluate the
134
+ # original block if it's a lambda block. Otherwise, the static value is
135
+ # returned.
136
+ #
137
+ # For example,
138
+ #
139
+ # State.new(machine, :parked, :value => 1).value # => 1
140
+ # State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
141
+ # State.new(machine, :parked, :value => lambda {Time.now}).value(false) # => <Proc:0xb6ea7ca0@...>
142
+ def value(eval = true)
143
+ if @value.is_a?(Proc) && eval
144
+ if cache_value?
145
+ @value = @value.call
146
+ machine.states.update(self)
147
+ @value
148
+ else
149
+ @value.call
150
+ end
151
+ else
152
+ @value
153
+ end
154
+ end
155
+
156
+ # Determines whether this state matches the given value. If no matcher is
157
+ # configured, then this will check whether the values are equivalent.
158
+ # Otherwise, the matcher will determine the result.
159
+ #
160
+ # For example,
161
+ #
162
+ # # Without a matcher
163
+ # state = State.new(machine, :parked, :value => 1)
164
+ # state.matches?(1) # => true
165
+ # state.matches?(2) # => false
166
+ #
167
+ # # With a matcher
168
+ # state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
169
+ # state.matches?(nil) # => false
170
+ # state.matches?(Time.now) # => true
171
+ def matches?(other_value)
172
+ matcher ? matcher.call(other_value) : other_value == value
173
+ end
174
+
175
+ # Defines a context for the state which will be enabled on instances of
176
+ # the owner class when the machine is in this state.
177
+ #
178
+ # This can be called multiple times. Each time a new context is created,
179
+ # a new module will be included in the owner class.
180
+ def context(&block)
181
+ # Include the context
182
+ context = @context
183
+ machine.owner_class.class_eval { include context }
184
+
185
+ # Evaluate the method definitions and track which ones were added
186
+ old_methods = context_methods
187
+ context.class_eval(&block)
188
+ new_methods = context_methods.to_a.select {|(name, method)| old_methods[name] != method}
189
+
190
+ # Alias new methods so that the only execute when the object is in this state
191
+ new_methods.each do |(method_name, method)|
192
+ context_name = context_name_for(method_name)
193
+ context.class_eval <<-end_eval, __FILE__, __LINE__ + 1
194
+ alias_method :"#{context_name}", :#{method_name}
195
+ def #{method_name}(*args, &block)
196
+ state = self.class.state_machine(#{machine.name.inspect}).states.fetch(#{name.inspect})
197
+ options = {:method_missing => lambda {super(*args, &block)}, :method_name => #{method_name.inspect}}
198
+ state.call(self, :"#{context_name}", *(args + [options]), &block)
199
+ end
200
+ end_eval
201
+ end
202
+
203
+ true
204
+ end
205
+
206
+ # The list of methods that have been defined in this state's context
207
+ def context_methods
208
+ @context.instance_methods.inject({}) do |methods, name|
209
+ methods.merge(name.to_sym => @context.instance_method(name))
210
+ end
211
+ end
212
+
213
+ # Calls a method defined in this state's context on the given object. All
214
+ # arguments and any block will be passed into the method defined.
215
+ #
216
+ # If the method has never been defined for this state, then a NoMethodError
217
+ # will be raised.
218
+ def call(object, method, *args, &block)
219
+ options = args.last.is_a?(Hash) ? args.pop : {}
220
+ options = {:method_name => method}.merge(options)
221
+ state = machine.states.match!(object)
222
+
223
+ if state == self && object.respond_to?(method)
224
+ object.send(method, *args, &block)
225
+ elsif method_missing = options[:method_missing]
226
+ # Dispatch to the superclass since the object either isn't in this state
227
+ # or this state doesn't handle the method
228
+ begin
229
+ method_missing.call
230
+ rescue NoMethodError => ex
231
+ if ex.name.to_s == options[:method_name].to_s && ex.args == args
232
+ # No valid context for this method
233
+ raise InvalidContext.new(object, "State #{state.name.inspect} for #{machine.name.inspect} is not a valid context for calling ##{options[:method_name]}")
234
+ else
235
+ raise
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ # Draws a representation of this state on the given machine. This will
242
+ # create a new node on the graph with the following properties:
243
+ # * +label+ - The human-friendly description of the state.
244
+ # * +width+ - The width of the node. Always 1.
245
+ # * +height+ - The height of the node. Always 1.
246
+ # * +shape+ - The actual shape of the node. If the state is a final
247
+ # state, then "doublecircle", otherwise "ellipse".
248
+ #
249
+ # Configuration options:
250
+ # * <tt>:human_name</tt> - Whether to use the state's human name for the
251
+ # node's label that gets drawn on the graph
252
+ def draw(graph, options = {})
253
+ node = graph.add_nodes(name ? name.to_s : 'nil',
254
+ :label => description(options),
255
+ :width => '1',
256
+ :height => '1',
257
+ :shape => final? ? 'doublecircle' : 'ellipse'
258
+ )
259
+
260
+ # Add open arrow for initial state
261
+ graph.add_edges(graph.add_nodes('starting_state', :shape => 'point'), node) if initial?
262
+
263
+ true
264
+ end
265
+
266
+ # Generates a nicely formatted description of this state's contents.
267
+ #
268
+ # For example,
269
+ #
270
+ # state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
271
+ # state # => #<StateMachine::State name=:parked value=1 initial=true context=[]>
272
+ def inspect
273
+ attributes = [[:name, name], [:value, @value], [:initial, initial?]]
274
+ "#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
275
+ end
276
+
277
+ private
278
+ # Should the value be cached after it's evaluated for the first time?
279
+ def cache_value?
280
+ @cache
281
+ end
282
+
283
+ # Adds a predicate method to the owner class so long as a name has
284
+ # actually been configured for the state
285
+ def add_predicate
286
+ # Checks whether the current value matches this state
287
+ machine.define_helper(:instance, "#{qualified_name}?") do |machine, object|
288
+ machine.states.matches?(object, name)
289
+ end
290
+ end
291
+
292
+ # Generates the name of the method containing the actual implementation
293
+ def context_name_for(method)
294
+ :"__#{machine.name}_#{name}_#{method}_#{@context.object_id}__"
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,112 @@
1
+ require 'state_machine/node_collection'
2
+
3
+ module StateMachine
4
+ # Represents a collection of states in a state machine
5
+ class StateCollection < NodeCollection
6
+ def initialize(machine) #:nodoc:
7
+ super(machine, :index => [:name, :qualified_name, :value])
8
+ end
9
+
10
+ # Determines whether the given object is in a specific state. If the
11
+ # object's current value doesn't match the state, then this will return
12
+ # false, otherwise true. If the given state is unknown, then an IndexError
13
+ # will be raised.
14
+ #
15
+ # == Examples
16
+ #
17
+ # class Vehicle
18
+ # state_machine :initial => :parked do
19
+ # other_states :idling
20
+ # end
21
+ # end
22
+ #
23
+ # states = Vehicle.state_machine.states
24
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
25
+ #
26
+ # states.matches?(vehicle, :parked) # => true
27
+ # states.matches?(vehicle, :idling) # => false
28
+ # states.matches?(vehicle, :invalid) # => IndexError: :invalid is an invalid key for :name index
29
+ def matches?(object, name)
30
+ fetch(name).matches?(machine.read(object, :state))
31
+ end
32
+
33
+ # Determines the current state of the given object as configured by this
34
+ # state machine. This will attempt to find a known state that matches
35
+ # the value of the attribute on the object.
36
+ #
37
+ # == Examples
38
+ #
39
+ # class Vehicle
40
+ # state_machine :initial => :parked do
41
+ # other_states :idling
42
+ # end
43
+ # end
44
+ #
45
+ # states = Vehicle.state_machine.states
46
+ #
47
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
48
+ # states.match(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
49
+ #
50
+ # vehicle.state = 'idling'
51
+ # states.match(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=true>
52
+ #
53
+ # vehicle.state = 'invalid'
54
+ # states.match(vehicle) # => nil
55
+ def match(object)
56
+ value = machine.read(object, :state)
57
+ self[value, :value] || detect {|state| state.matches?(value)}
58
+ end
59
+
60
+ # Determines the current state of the given object as configured by this
61
+ # state machine. If no state is found, then an ArgumentError will be
62
+ # raised.
63
+ #
64
+ # == Examples
65
+ #
66
+ # class Vehicle
67
+ # state_machine :initial => :parked do
68
+ # other_states :idling
69
+ # end
70
+ # end
71
+ #
72
+ # states = Vehicle.state_machine.states
73
+ #
74
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c464b0 @state="parked">
75
+ # states.match!(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=true>
76
+ #
77
+ # vehicle.state = 'invalid'
78
+ # states.match!(vehicle) # => ArgumentError: "invalid" is not a known state value
79
+ def match!(object)
80
+ match(object) || raise(ArgumentError, "#{machine.read(object, :state).inspect} is not a known #{machine.name} value")
81
+ end
82
+
83
+ # Gets the order in which states should be displayed based on where they
84
+ # were first referenced. This will order states in the following priority:
85
+ #
86
+ # 1. Initial state
87
+ # 2. Event transitions (:from, :except_from, :to, :except_to options)
88
+ # 3. States with behaviors
89
+ # 4. States referenced via +state+ or +other_states+
90
+ # 5. States referenced in callbacks
91
+ #
92
+ # This order will determine how the GraphViz visualizations are rendered.
93
+ def by_priority
94
+ order = select {|state| state.initial}.map {|state| state.name}
95
+
96
+ machine.events.each {|event| order += event.known_states}
97
+ order += select {|state| state.context_methods.any?}.map {|state| state.name}
98
+ order += keys(:name) - machine.callbacks.values.flatten.map {|callback| callback.known_states}.flatten
99
+ order += keys(:name)
100
+
101
+ order.uniq!
102
+ order.map! {|name| self[name]}
103
+ order
104
+ end
105
+
106
+ private
107
+ # Gets the value for the given attribute on the node
108
+ def value(node, attribute)
109
+ attribute == :value ? node.value(false) : super
110
+ end
111
+ end
112
+ end