state_machines 0.5.0 → 0.50.0

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 (486) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +443 -22
  4. data/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
  5. data/lib/state_machines/async_mode/async_events.rb +282 -0
  6. data/lib/state_machines/async_mode/async_machine.rb +60 -0
  7. data/lib/state_machines/async_mode/async_transition_collection.rb +141 -0
  8. data/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
  9. data/lib/state_machines/async_mode.rb +64 -0
  10. data/lib/state_machines/branch.rb +146 -86
  11. data/lib/state_machines/callback.rb +35 -32
  12. data/lib/state_machines/core.rb +3 -3
  13. data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
  14. data/lib/state_machines/core_ext.rb +2 -0
  15. data/lib/state_machines/error.rb +7 -4
  16. data/lib/state_machines/eval_helpers.rb +197 -39
  17. data/lib/state_machines/event.rb +77 -58
  18. data/lib/state_machines/event_collection.rb +49 -39
  19. data/lib/state_machines/extensions.rb +6 -4
  20. data/lib/state_machines/helper_module.rb +4 -2
  21. data/lib/state_machines/integrations/base.rb +3 -1
  22. data/lib/state_machines/integrations.rb +19 -20
  23. data/lib/state_machines/machine/action_hooks.rb +53 -0
  24. data/lib/state_machines/machine/async_extensions.rb +88 -0
  25. data/lib/state_machines/machine/callbacks.rb +59 -0
  26. data/lib/state_machines/machine/class_methods.rb +97 -0
  27. data/lib/state_machines/machine/configuration.rb +134 -0
  28. data/lib/state_machines/machine/event_methods.rb +59 -0
  29. data/lib/state_machines/machine/helper_generators.rb +125 -0
  30. data/lib/state_machines/machine/integration.rb +70 -0
  31. data/lib/state_machines/machine/parsing.rb +77 -0
  32. data/lib/state_machines/machine/rendering.rb +17 -0
  33. data/lib/state_machines/machine/scoping.rb +44 -0
  34. data/lib/state_machines/machine/state_methods.rb +101 -0
  35. data/lib/state_machines/machine/utilities.rb +85 -0
  36. data/lib/state_machines/machine/validation.rb +39 -0
  37. data/lib/state_machines/machine.rb +425 -1011
  38. data/lib/state_machines/machine_collection.rb +28 -19
  39. data/lib/state_machines/macro_methods.rb +104 -102
  40. data/lib/state_machines/matcher.rb +31 -28
  41. data/lib/state_machines/matcher_helpers.rb +14 -12
  42. data/lib/state_machines/node_collection.rb +36 -29
  43. data/lib/state_machines/options_validator.rb +72 -0
  44. data/lib/state_machines/path.rb +60 -57
  45. data/lib/state_machines/path_collection.rb +39 -36
  46. data/lib/state_machines/state.rb +84 -47
  47. data/lib/state_machines/state_collection.rb +22 -19
  48. data/lib/state_machines/state_context.rb +40 -39
  49. data/lib/state_machines/stdio_renderer.rb +74 -0
  50. data/lib/state_machines/syntax_validator.rb +57 -0
  51. data/lib/state_machines/test_helper.rb +896 -0
  52. data/lib/state_machines/transition.rb +215 -199
  53. data/lib/state_machines/transition_collection.rb +187 -170
  54. data/lib/state_machines/version.rb +3 -1
  55. data/lib/state_machines.rb +4 -1
  56. metadata +39 -446
  57. data/.gitignore +0 -21
  58. data/.rspec +0 -3
  59. data/.ruby-gemset +0 -1
  60. data/.ruby-version +0 -1
  61. data/.travis.yml +0 -16
  62. data/Changelog.md +0 -22
  63. data/Contributors.md +0 -39
  64. data/Gemfile +0 -8
  65. data/Rakefile +0 -12
  66. data/Testing.md +0 -0
  67. data/lib/state_machines/assertions.rb +0 -40
  68. data/state_machines.gemspec +0 -22
  69. data/test/files/integrations/event_on_failure_integration.rb +0 -10
  70. data/test/files/integrations/vehicle.rb +0 -7
  71. data/test/files/models/auto_shop.rb +0 -31
  72. data/test/files/models/car.rb +0 -21
  73. data/test/files/models/driver.rb +0 -13
  74. data/test/files/models/model_base.rb +0 -6
  75. data/test/files/models/motorcycle.rb +0 -16
  76. data/test/files/models/traffic_light.rb +0 -47
  77. data/test/files/models/vehicle.rb +0 -127
  78. data/test/files/node.rb +0 -5
  79. data/test/files/switch.rb +0 -15
  80. data/test/functional/auto_shop_available_test.rb +0 -20
  81. data/test/functional/auto_shop_busy_test.rb +0 -25
  82. data/test/functional/car_backing_up_test.rb +0 -45
  83. data/test/functional/car_test.rb +0 -49
  84. data/test/functional/driver_default_nonstandard_test.rb +0 -13
  85. data/test/functional/motorcycle_test.rb +0 -52
  86. data/test/functional/traffic_light_caution_test.rb +0 -17
  87. data/test/functional/traffic_light_proceed_test.rb +0 -17
  88. data/test/functional/traffic_light_stop_test.rb +0 -26
  89. data/test/functional/vehicle_first_gear_test.rb +0 -42
  90. data/test/functional/vehicle_idling_test.rb +0 -59
  91. data/test/functional/vehicle_locked_test.rb +0 -29
  92. data/test/functional/vehicle_parked_test.rb +0 -53
  93. data/test/functional/vehicle_repaired_test.rb +0 -20
  94. data/test/functional/vehicle_second_gear_test.rb +0 -42
  95. data/test/functional/vehicle_stalled_test.rb +0 -65
  96. data/test/functional/vehicle_test.rb +0 -20
  97. data/test/functional/vehicle_third_gear_test.rb +0 -42
  98. data/test/functional/vehicle_unsaved_test.rb +0 -181
  99. data/test/functional/vehicle_with_event_attributes_test.rb +0 -30
  100. data/test/functional/vehicle_with_parallel_events_test.rb +0 -36
  101. data/test/test_helper.rb +0 -15
  102. data/test/unit/assertions/assert_exclusive_keys_test.rb +0 -22
  103. data/test/unit/assertions/assert_valid_key_test.rb +0 -12
  104. data/test/unit/branch/branch_test.rb +0 -28
  105. data/test/unit/branch/branch_with_conflicting_conditionals_test.rb +0 -27
  106. data/test/unit/branch/branch_with_conflicting_from_requirements_test.rb +0 -8
  107. data/test/unit/branch/branch_with_conflicting_on_requirements_test.rb +0 -8
  108. data/test/unit/branch/branch_with_conflicting_to_requirements_test.rb +0 -8
  109. data/test/unit/branch/branch_with_different_requirements_test.rb +0 -41
  110. data/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb +0 -8
  111. data/test/unit/branch/branch_with_except_from_requirement_test.rb +0 -36
  112. data/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb +0 -8
  113. data/test/unit/branch/branch_with_except_on_requirement_test.rb +0 -36
  114. data/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb +0 -8
  115. data/test/unit/branch/branch_with_except_to_requirement_test.rb +0 -36
  116. data/test/unit/branch/branch_with_from_matcher_requirement_test.rb +0 -20
  117. data/test/unit/branch/branch_with_from_requirement_test.rb +0 -45
  118. data/test/unit/branch/branch_with_if_conditional_test.rb +0 -27
  119. data/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb +0 -23
  120. data/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb +0 -20
  121. data/test/unit/branch/branch_with_implicit_requirement_test.rb +0 -20
  122. data/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb +0 -16
  123. data/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb +0 -20
  124. data/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb +0 -16
  125. data/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb +0 -20
  126. data/test/unit/branch/branch_with_multiple_from_requirements_test.rb +0 -16
  127. data/test/unit/branch/branch_with_multiple_if_conditionals_test.rb +0 -20
  128. data/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb +0 -53
  129. data/test/unit/branch/branch_with_multiple_to_requirements_test.rb +0 -20
  130. data/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb +0 -20
  131. data/test/unit/branch/branch_with_nil_requirements_test.rb +0 -28
  132. data/test/unit/branch/branch_with_no_requirements_test.rb +0 -36
  133. data/test/unit/branch/branch_with_on_matcher_requirement_test.rb +0 -16
  134. data/test/unit/branch/branch_with_on_requirement_test.rb +0 -45
  135. data/test/unit/branch/branch_with_to_matcher_requirement_test.rb +0 -20
  136. data/test/unit/branch/branch_with_to_requirement_test.rb +0 -45
  137. data/test/unit/branch/branch_with_unless_conditional_test.rb +0 -27
  138. data/test/unit/branch/branch_without_guards_test.rb +0 -27
  139. data/test/unit/callback/callback_by_default_test.rb +0 -25
  140. data/test/unit/callback/callback_test.rb +0 -53
  141. data/test/unit/callback/callback_with_application_bound_object_test.rb +0 -23
  142. data/test/unit/callback/callback_with_application_terminator_test.rb +0 -24
  143. data/test/unit/callback/callback_with_arguments_test.rb +0 -14
  144. data/test/unit/callback/callback_with_around_type_and_arguments_test.rb +0 -25
  145. data/test/unit/callback/callback_with_around_type_and_block_test.rb +0 -44
  146. data/test/unit/callback/callback_with_around_type_and_bound_method_test.rb +0 -23
  147. data/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb +0 -93
  148. data/test/unit/callback/callback_with_around_type_and_terminator_test.rb +0 -17
  149. data/test/unit/callback/callback_with_block_test.rb +0 -20
  150. data/test/unit/callback/callback_with_bound_method_and_arguments_test.rb +0 -28
  151. data/test/unit/callback/callback_with_bound_method_test.rb +0 -35
  152. data/test/unit/callback/callback_with_do_method_test.rb +0 -18
  153. data/test/unit/callback/callback_with_explicit_requirements_test.rb +0 -32
  154. data/test/unit/callback/callback_with_if_condition_test.rb +0 -17
  155. data/test/unit/callback/callback_with_implicit_requirements_test.rb +0 -32
  156. data/test/unit/callback/callback_with_method_argument_test.rb +0 -18
  157. data/test/unit/callback/callback_with_mixed_methods_test.rb +0 -31
  158. data/test/unit/callback/callback_with_multiple_bound_methods_test.rb +0 -21
  159. data/test/unit/callback/callback_with_multiple_do_methods_test.rb +0 -29
  160. data/test/unit/callback/callback_with_multiple_method_arguments_test.rb +0 -29
  161. data/test/unit/callback/callback_with_terminator_test.rb +0 -22
  162. data/test/unit/callback/callback_with_unbound_method_test.rb +0 -14
  163. data/test/unit/callback/callback_with_unless_condition_test.rb +0 -17
  164. data/test/unit/callback/callback_without_arguments_test.rb +0 -14
  165. data/test/unit/callback/callback_without_terminator_test.rb +0 -12
  166. data/test/unit/error/error_by_default_test.rb +0 -21
  167. data/test/unit/error/error_with_message_test.rb +0 -23
  168. data/test/unit/eval_helper/eval_helpers_base_test.rb +0 -8
  169. data/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb +0 -14
  170. data/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb +0 -14
  171. data/test/unit/eval_helper/eval_helpers_proc_test.rb +0 -13
  172. data/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb +0 -13
  173. data/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb +0 -13
  174. data/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb +0 -18
  175. data/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb +0 -14
  176. data/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb +0 -19
  177. data/test/unit/eval_helper/eval_helpers_string_test.rb +0 -25
  178. data/test/unit/eval_helper/eval_helpers_string_with_block_test.rb +0 -12
  179. data/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb +0 -20
  180. data/test/unit/eval_helper/eval_helpers_symbol_private_test.rb +0 -17
  181. data/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb +0 -17
  182. data/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb +0 -18
  183. data/test/unit/eval_helper/eval_helpers_symbol_test.rb +0 -16
  184. data/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb +0 -16
  185. data/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb +0 -16
  186. data/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb +0 -16
  187. data/test/unit/eval_helper/eval_helpers_test.rb +0 -13
  188. data/test/unit/event/event_after_being_copied_test.rb +0 -17
  189. data/test/unit/event/event_by_default_test.rb +0 -60
  190. data/test/unit/event/event_context_test.rb +0 -16
  191. data/test/unit/event/event_on_failure_test.rb +0 -44
  192. data/test/unit/event/event_test.rb +0 -34
  193. data/test/unit/event/event_transitions_test.rb +0 -62
  194. data/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb +0 -79
  195. data/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb +0 -58
  196. data/test/unit/event/event_with_conflicting_machine_test.rb +0 -48
  197. data/test/unit/event/event_with_dynamic_human_name_test.rb +0 -26
  198. data/test/unit/event/event_with_human_name_test.rb +0 -13
  199. data/test/unit/event/event_with_invalid_current_state_test.rb +0 -30
  200. data/test/unit/event/event_with_machine_action_test.rb +0 -33
  201. data/test/unit/event/event_with_marshalling_test.rb +0 -47
  202. data/test/unit/event/event_with_matching_disabled_transitions_test.rb +0 -115
  203. data/test/unit/event/event_with_matching_enabled_transitions_test.rb +0 -75
  204. data/test/unit/event/event_with_multiple_transitions_test.rb +0 -61
  205. data/test/unit/event/event_with_namespace_test.rb +0 -34
  206. data/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb +0 -60
  207. data/test/unit/event/event_with_transition_with_loopback_state_test.rb +0 -36
  208. data/test/unit/event/event_with_transition_with_nil_to_state_test.rb +0 -36
  209. data/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb +0 -51
  210. data/test/unit/event/event_with_transition_without_to_state_test.rb +0 -36
  211. data/test/unit/event/event_with_transitions_test.rb +0 -32
  212. data/test/unit/event/event_without_matching_transitions_test.rb +0 -41
  213. data/test/unit/event/event_without_transitions_test.rb +0 -28
  214. data/test/unit/event/invalid_event_test.rb +0 -20
  215. data/test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb +0 -62
  216. data/test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb +0 -36
  217. data/test/unit/event_collection/event_collection_by_default_test.rb +0 -26
  218. data/test/unit/event_collection/event_collection_test.rb +0 -39
  219. data/test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb +0 -31
  220. data/test/unit/event_collection/event_collection_with_events_with_transitions_test.rb +0 -76
  221. data/test/unit/event_collection/event_collection_with_multiple_events_test.rb +0 -27
  222. data/test/unit/event_collection/event_collection_with_validations_test.rb +0 -74
  223. data/test/unit/event_collection/event_collection_without_machine_action_test.rb +0 -18
  224. data/test/unit/event_collection/event_string_collection_test.rb +0 -31
  225. data/test/unit/helper_module_test.rb +0 -17
  226. data/test/unit/integrations/integration_finder_test.rb +0 -16
  227. data/test/unit/integrations/integration_matcher_test.rb +0 -29
  228. data/test/unit/invalid_transition/invalid_parallel_transition_test.rb +0 -18
  229. data/test/unit/invalid_transition/invalid_transition_test.rb +0 -47
  230. data/test/unit/invalid_transition/invalid_transition_with_integration_test.rb +0 -45
  231. data/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb +0 -32
  232. data/test/unit/machine/machine_after_being_copied_test.rb +0 -62
  233. data/test/unit/machine/machine_after_changing_initial_state.rb +0 -28
  234. data/test/unit/machine/machine_after_changing_owner_class_test.rb +0 -31
  235. data/test/unit/machine/machine_by_default_test.rb +0 -160
  236. data/test/unit/machine/machine_finder_custom_options_test.rb +0 -17
  237. data/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb +0 -85
  238. data/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb +0 -23
  239. data/test/unit/machine/machine_finder_without_existing_machine_test.rb +0 -25
  240. data/test/unit/machine/machine_persistence_test.rb +0 -52
  241. data/test/unit/machine/machine_state_initialization_test.rb +0 -56
  242. data/test/unit/machine/machine_test.rb +0 -30
  243. data/test/unit/machine/machine_with_action_already_overridden_test.rb +0 -23
  244. data/test/unit/machine/machine_with_action_defined_in_class_test.rb +0 -37
  245. data/test/unit/machine/machine_with_action_defined_in_included_module_test.rb +0 -46
  246. data/test/unit/machine/machine_with_action_defined_in_superclass_test.rb +0 -43
  247. data/test/unit/machine/machine_with_action_undefined_test.rb +0 -33
  248. data/test/unit/machine/machine_with_cached_state_test.rb +0 -20
  249. data/test/unit/machine/machine_with_class_helpers_test.rb +0 -179
  250. data/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb +0 -244
  251. data/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb +0 -175
  252. data/test/unit/machine/machine_with_custom_action_test.rb +0 -11
  253. data/test/unit/machine/machine_with_custom_attribute_test.rb +0 -103
  254. data/test/unit/machine/machine_with_custom_initialize_test.rb +0 -24
  255. data/test/unit/machine/machine_with_custom_integration_test.rb +0 -72
  256. data/test/unit/machine/machine_with_custom_invalidation_test.rb +0 -39
  257. data/test/unit/machine/machine_with_custom_name_test.rb +0 -57
  258. data/test/unit/machine/machine_with_custom_plural_test.rb +0 -52
  259. data/test/unit/machine/machine_with_dynamic_initial_state_test.rb +0 -65
  260. data/test/unit/machine/machine_with_event_matchers_test.rb +0 -41
  261. data/test/unit/machine/machine_with_events_test.rb +0 -52
  262. data/test/unit/machine/machine_with_events_with_custom_human_names_test.rb +0 -18
  263. data/test/unit/machine/machine_with_events_with_transitions_test.rb +0 -37
  264. data/test/unit/machine/machine_with_existing_event_test.rb +0 -17
  265. data/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb +0 -20
  266. data/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb +0 -71
  267. data/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb +0 -31
  268. data/test/unit/machine/machine_with_existing_state_test.rb +0 -27
  269. data/test/unit/machine/machine_with_failure_callbacks_test.rb +0 -48
  270. data/test/unit/machine/machine_with_helpers_test.rb +0 -14
  271. data/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb +0 -25
  272. data/test/unit/machine/machine_with_initialize_and_super_test.rb +0 -17
  273. data/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb +0 -31
  274. data/test/unit/machine/machine_with_initialize_without_super_test.rb +0 -17
  275. data/test/unit/machine/machine_with_instance_helpers_test.rb +0 -179
  276. data/test/unit/machine/machine_with_integration_test.rb +0 -72
  277. data/test/unit/machine/machine_with_multiple_events_test.rb +0 -32
  278. data/test/unit/machine/machine_with_namespace_test.rb +0 -48
  279. data/test/unit/machine/machine_with_nil_action_test.rb +0 -27
  280. data/test/unit/machine/machine_with_other_states.rb +0 -22
  281. data/test/unit/machine/machine_with_owner_subclass_test.rb +0 -18
  282. data/test/unit/machine/machine_with_paths_test.rb +0 -25
  283. data/test/unit/machine/machine_with_private_action_test.rb +0 -43
  284. data/test/unit/machine/machine_with_state_matchers_test.rb +0 -41
  285. data/test/unit/machine/machine_with_state_with_matchers_test.rb +0 -19
  286. data/test/unit/machine/machine_with_states_test.rb +0 -55
  287. data/test/unit/machine/machine_with_states_with_behaviors_test.rb +0 -23
  288. data/test/unit/machine/machine_with_states_with_custom_human_names_test.rb +0 -18
  289. data/test/unit/machine/machine_with_states_with_custom_values_test.rb +0 -21
  290. data/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb +0 -19
  291. data/test/unit/machine/machine_with_static_initial_state_test.rb +0 -49
  292. data/test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb +0 -36
  293. data/test/unit/machine/machine_with_transition_callbacks_test.rb +0 -144
  294. data/test/unit/machine/machine_with_transitions_test.rb +0 -87
  295. data/test/unit/machine/machine_without_initialization_test.rb +0 -31
  296. data/test/unit/machine/machine_without_initialize_test.rb +0 -14
  297. data/test/unit/machine/machine_without_integration_test.rb +0 -31
  298. data/test/unit/machine_collection/machine_collection_by_default_test.rb +0 -11
  299. data/test/unit/machine_collection/machine_collection_fire_test.rb +0 -80
  300. data/test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb +0 -54
  301. data/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb +0 -76
  302. data/test/unit/machine_collection/machine_collection_state_initialization_test.rb +0 -111
  303. data/test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb +0 -25
  304. data/test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb +0 -20
  305. data/test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb +0 -26
  306. data/test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb +0 -25
  307. data/test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb +0 -25
  308. data/test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb +0 -31
  309. data/test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb +0 -26
  310. data/test/unit/machine_collection/machine_collection_transitions_without_events_test.rb +0 -25
  311. data/test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb +0 -27
  312. data/test/unit/matcher/all_matcher_test.rb +0 -29
  313. data/test/unit/matcher/blacklist_matcher_test.rb +0 -30
  314. data/test/unit/matcher/loopback_matcher_test.rb +0 -27
  315. data/test/unit/matcher/matcher_by_default_test.rb +0 -15
  316. data/test/unit/matcher/matcher_with_multiple_values_test.rb +0 -15
  317. data/test/unit/matcher/matcher_with_value_test.rb +0 -15
  318. data/test/unit/matcher/whitelist_matcher_test.rb +0 -30
  319. data/test/unit/matcher_helpers/matcher_helpers_all_test.rb +0 -14
  320. data/test/unit/matcher_helpers/matcher_helpers_any_test.rb +0 -14
  321. data/test/unit/matcher_helpers/matcher_helpers_same_test.rb +0 -13
  322. data/test/unit/node_collection/node_collection_after_being_copied_test.rb +0 -46
  323. data/test/unit/node_collection/node_collection_after_update_test.rb +0 -36
  324. data/test/unit/node_collection/node_collection_by_default_test.rb +0 -22
  325. data/test/unit/node_collection/node_collection_test.rb +0 -23
  326. data/test/unit/node_collection/node_collection_with_indices_test.rb +0 -42
  327. data/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb +0 -25
  328. data/test/unit/node_collection/node_collection_with_nodes_test.rb +0 -46
  329. data/test/unit/node_collection/node_collection_with_numeric_index_test.rb +0 -24
  330. data/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb +0 -22
  331. data/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb +0 -23
  332. data/test/unit/node_collection/node_collection_with_string_index_test.rb +0 -20
  333. data/test/unit/node_collection/node_collection_with_symbol_index_test.rb +0 -20
  334. data/test/unit/node_collection/node_collection_without_indices_test.rb +0 -30
  335. data/test/unit/path/path_by_default_test.rb +0 -54
  336. data/test/unit/path/path_test.rb +0 -14
  337. data/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb +0 -40
  338. data/test/unit/path/path_with_available_transitions_test.rb +0 -54
  339. data/test/unit/path/path_with_deep_target_reached_test.rb +0 -50
  340. data/test/unit/path/path_with_deep_target_test.rb +0 -40
  341. data/test/unit/path/path_with_duplicates_test.rb +0 -32
  342. data/test/unit/path/path_with_encountered_transitions_test.rb +0 -34
  343. data/test/unit/path/path_with_guarded_transitions_test.rb +0 -42
  344. data/test/unit/path/path_with_reached_target_test.rb +0 -35
  345. data/test/unit/path/path_with_transitions_test.rb +0 -54
  346. data/test/unit/path/path_with_unreached_target_test.rb +0 -31
  347. data/test/unit/path/path_without_transitions_test.rb +0 -24
  348. data/test/unit/path_collection/path_collection_by_default_test.rb +0 -46
  349. data/test/unit/path_collection/path_collection_test.rb +0 -24
  350. data/test/unit/path_collection/path_collection_with_deep_paths_test.rb +0 -43
  351. data/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb +0 -31
  352. data/test/unit/path_collection/path_collection_with_from_state_test.rb +0 -27
  353. data/test/unit/path_collection/path_collection_with_paths_test.rb +0 -47
  354. data/test/unit/path_collection/path_collection_with_to_state_test.rb +0 -29
  355. data/test/unit/path_collection/path_with_guarded_paths_test.rb +0 -25
  356. data/test/unit/state/state_after_being_copied_test.rb +0 -19
  357. data/test/unit/state/state_by_default_test.rb +0 -41
  358. data/test/unit/state/state_final_test.rb +0 -28
  359. data/test/unit/state/state_initial_test.rb +0 -13
  360. data/test/unit/state/state_not_final_test.rb +0 -32
  361. data/test/unit/state/state_not_initial_test.rb +0 -13
  362. data/test/unit/state/state_test.rb +0 -44
  363. data/test/unit/state/state_with_cached_lambda_value_test.rb +0 -29
  364. data/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb +0 -38
  365. data/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb +0 -29
  366. data/test/unit/state/state_with_conflicting_machine_name_test.rb +0 -20
  367. data/test/unit/state/state_with_conflicting_machine_test.rb +0 -37
  368. data/test/unit/state/state_with_context_test.rb +0 -60
  369. data/test/unit/state/state_with_dynamic_human_name_test.rb +0 -25
  370. data/test/unit/state/state_with_existing_context_method_test.rb +0 -24
  371. data/test/unit/state/state_with_human_name_test.rb +0 -13
  372. data/test/unit/state/state_with_integer_value_test.rb +0 -32
  373. data/test/unit/state/state_with_invalid_method_call_test.rb +0 -21
  374. data/test/unit/state/state_with_lambda_value_test.rb +0 -37
  375. data/test/unit/state/state_with_matcher_test.rb +0 -18
  376. data/test/unit/state/state_with_multiple_contexts_test.rb +0 -57
  377. data/test/unit/state/state_with_name_test.rb +0 -43
  378. data/test/unit/state/state_with_namespace_test.rb +0 -22
  379. data/test/unit/state/state_with_nil_value_test.rb +0 -35
  380. data/test/unit/state/state_with_redefined_context_method_test.rb +0 -45
  381. data/test/unit/state/state_with_symbolic_value_test.rb +0 -32
  382. data/test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb +0 -40
  383. data/test/unit/state/state_with_valid_method_call_for_current_state_test.rb +0 -33
  384. data/test/unit/state/state_with_valid_method_call_for_different_state_test.rb +0 -41
  385. data/test/unit/state/state_without_cached_lambda_value_test.rb +0 -25
  386. data/test/unit/state/state_without_name_test.rb +0 -39
  387. data/test/unit/state_collection/state_collection_by_default_test.rb +0 -21
  388. data/test/unit/state_collection/state_collection_string_test.rb +0 -35
  389. data/test/unit/state_collection/state_collection_test.rb +0 -74
  390. data/test/unit/state_collection/state_collection_with_custom_state_values_test.rb +0 -29
  391. data/test/unit/state_collection/state_collection_with_event_transitions_test.rb +0 -39
  392. data/test/unit/state_collection/state_collection_with_initial_state_test.rb +0 -40
  393. data/test/unit/state_collection/state_collection_with_namespace_test.rb +0 -21
  394. data/test/unit/state_collection/state_collection_with_state_behaviors_test.rb +0 -40
  395. data/test/unit/state_collection/state_collection_with_state_matchers_test.rb +0 -29
  396. data/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb +0 -40
  397. data/test/unit/state_context/state_context_proxy_test.rb +0 -26
  398. data/test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb +0 -42
  399. data/test/unit/state_context/state_context_proxy_with_if_condition_test.rb +0 -64
  400. data/test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb +0 -32
  401. data/test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb +0 -32
  402. data/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb +0 -64
  403. data/test/unit/state_context/state_context_proxy_without_conditions_test.rb +0 -31
  404. data/test/unit/state_context/state_context_test.rb +0 -28
  405. data/test/unit/state_context/state_context_transition_test.rb +0 -104
  406. data/test/unit/state_context/state_context_with_matching_transition_test.rb +0 -27
  407. data/test/unit/state_machine/state_machine_by_default_test.rb +0 -12
  408. data/test/unit/state_machine/state_machine_test.rb +0 -20
  409. data/test/unit/transition/transition_after_being_performed_test.rb +0 -48
  410. data/test/unit/transition/transition_after_being_persisted_test.rb +0 -46
  411. data/test/unit/transition/transition_after_being_rolled_back_test.rb +0 -35
  412. data/test/unit/transition/transition_equality_test.rb +0 -52
  413. data/test/unit/transition/transition_loopback_test.rb +0 -18
  414. data/test/unit/transition/transition_test.rb +0 -96
  415. data/test/unit/transition/transition_transient_test.rb +0 -20
  416. data/test/unit/transition/transition_with_action_test.rb +0 -27
  417. data/test/unit/transition/transition_with_after_callbacks_skipped_test.rb +0 -127
  418. data/test/unit/transition/transition_with_after_callbacks_test.rb +0 -93
  419. data/test/unit/transition/transition_with_around_callbacks_test.rb +0 -141
  420. data/test/unit/transition/transition_with_before_callbacks_skipped_test.rb +0 -30
  421. data/test/unit/transition/transition_with_before_callbacks_test.rb +0 -104
  422. data/test/unit/transition/transition_with_custom_machine_attribute_test.rb +0 -28
  423. data/test/unit/transition/transition_with_different_states_test.rb +0 -18
  424. data/test/unit/transition/transition_with_dynamic_to_value_test.rb +0 -19
  425. data/test/unit/transition/transition_with_failure_callbacks_test.rb +0 -84
  426. data/test/unit/transition/transition_with_invalid_nodes_test.rb +0 -29
  427. data/test/unit/transition/transition_with_mixed_callbacks_test.rb +0 -105
  428. data/test/unit/transition/transition_with_multiple_after_callbacks_test.rb +0 -40
  429. data/test/unit/transition/transition_with_multiple_around_callbacks_test.rb +0 -114
  430. data/test/unit/transition/transition_with_multiple_before_callbacks_test.rb +0 -40
  431. data/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb +0 -40
  432. data/test/unit/transition/transition_with_namespace_test.rb +0 -47
  433. data/test/unit/transition/transition_with_perform_arguments_test.rb +0 -35
  434. data/test/unit/transition/transition_with_transactions_test.rb +0 -42
  435. data/test/unit/transition/transition_without_callbacks_test.rb +0 -33
  436. data/test/unit/transition/transition_without_reading_state_test.rb +0 -22
  437. data/test/unit/transition/transition_without_running_action_test.rb +0 -47
  438. data/test/unit/transition_collection/attribute_transition_collection_by_default_test.rb +0 -23
  439. data/test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb +0 -64
  440. data/test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb +0 -44
  441. data/test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb +0 -44
  442. data/test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb +0 -32
  443. data/test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb +0 -33
  444. data/test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb +0 -32
  445. data/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb +0 -32
  446. data/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb +0 -33
  447. data/test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb +0 -33
  448. data/test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb +0 -32
  449. data/test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb +0 -33
  450. data/test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb +0 -68
  451. data/test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb +0 -41
  452. data/test/unit/transition_collection/attribute_transition_collection_with_events_test.rb +0 -44
  453. data/test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb +0 -42
  454. data/test/unit/transition_collection/transition_collection_by_default_test.rb +0 -23
  455. data/test/unit/transition_collection/transition_collection_empty_with_block_test.rb +0 -23
  456. data/test/unit/transition_collection/transition_collection_empty_without_block_test.rb +0 -12
  457. data/test/unit/transition_collection/transition_collection_invalid_test.rb +0 -21
  458. data/test/unit/transition_collection/transition_collection_partial_invalid_test.rb +0 -69
  459. data/test/unit/transition_collection/transition_collection_test.rb +0 -26
  460. data/test/unit/transition_collection/transition_collection_valid_test.rb +0 -57
  461. data/test/unit/transition_collection/transition_collection_with_action_error_test.rb +0 -66
  462. data/test/unit/transition_collection/transition_collection_with_action_failed_test.rb +0 -60
  463. data/test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb +0 -17
  464. data/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb +0 -17
  465. data/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb +0 -37
  466. data/test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb +0 -34
  467. data/test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb +0 -29
  468. data/test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb +0 -17
  469. data/test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb +0 -79
  470. data/test/unit/transition_collection/transition_collection_with_action_hook_test.rb +0 -45
  471. data/test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb +0 -48
  472. data/test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb +0 -42
  473. data/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb +0 -47
  474. data/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb +0 -51
  475. data/test/unit/transition_collection/transition_collection_with_block_test.rb +0 -46
  476. data/test/unit/transition_collection/transition_collection_with_callbacks_test.rb +0 -135
  477. data/test/unit/transition_collection/transition_collection_with_different_actions_test.rb +0 -189
  478. data/test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb +0 -48
  479. data/test/unit/transition_collection/transition_collection_with_empty_actions_test.rb +0 -41
  480. data/test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb +0 -41
  481. data/test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb +0 -34
  482. data/test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb +0 -69
  483. data/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb +0 -53
  484. data/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb +0 -34
  485. data/test/unit/transition_collection/transition_collection_with_transactions_test.rb +0 -65
  486. data/test/unit/transition_collection/transition_collection_without_transactions_test.rb +0 -29
@@ -1,328 +1,347 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+ require_relative 'machine/class_methods'
5
+ require_relative 'machine/utilities'
6
+ require_relative 'machine/parsing'
7
+ require_relative 'machine/validation'
8
+ require_relative 'machine/helper_generators'
9
+ require_relative 'machine/action_hooks'
10
+ require_relative 'machine/scoping'
11
+ require_relative 'machine/configuration'
12
+ require_relative 'machine/state_methods'
13
+ require_relative 'machine/event_methods'
14
+ require_relative 'machine/callbacks'
15
+ require_relative 'machine/rendering'
16
+ require_relative 'machine/integration'
17
+ require_relative 'machine/async_extensions'
18
+ require_relative 'syntax_validator'
19
+
1
20
  module StateMachines
2
21
  # Represents a state machine for a particular attribute. State machines
3
22
  # consist of states, events and a set of transitions that define how the
4
23
  # state changes after a particular event is fired.
5
- #
24
+ #
6
25
  # A state machine will not know all of the possible states for an object
7
26
  # unless they are referenced *somewhere* in the state machine definition.
8
27
  # As a result, any unused states should be defined with the +other_states+
9
28
  # or +state+ helper.
10
- #
29
+ #
11
30
  # == Actions
12
- #
31
+ #
13
32
  # When an action is configured for a state machine, it is invoked when an
14
33
  # object transitions via an event. The success of the event becomes
15
34
  # dependent on the success of the action. If the action is successful, then
16
35
  # the transitioned state remains persisted. However, if the action fails
17
36
  # (by returning false), the transitioned state will be rolled back.
18
- #
37
+ #
19
38
  # For example,
20
- #
39
+ #
21
40
  # class Vehicle
22
41
  # attr_accessor :fail, :saving_state
23
- #
42
+ #
24
43
  # state_machine :initial => :parked, :action => :save do
25
44
  # event :ignite do
26
45
  # transition :parked => :idling
27
46
  # end
28
- #
47
+ #
29
48
  # event :park do
30
49
  # transition :idling => :parked
31
50
  # end
32
51
  # end
33
- #
52
+ #
34
53
  # def save
35
54
  # @saving_state = state
36
55
  # fail != true
37
56
  # end
38
57
  # end
39
- #
58
+ #
40
59
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
41
60
  # vehicle.save # => true
42
61
  # vehicle.saving_state # => "parked" # The state was "parked" was save was called
43
- #
62
+ #
44
63
  # # Successful event
45
64
  # vehicle.ignite # => true
46
65
  # vehicle.saving_state # => "idling" # The state was "idling" when save was called
47
66
  # vehicle.state # => "idling"
48
- #
67
+ #
49
68
  # # Failed event
50
69
  # vehicle.fail = true
51
70
  # vehicle.park # => false
52
71
  # vehicle.saving_state # => "parked"
53
72
  # vehicle.state # => "idling"
54
- #
73
+ #
55
74
  # As shown, even though the state is set prior to calling the +save+ action
56
75
  # on the object, it will be rolled back to the original state if the action
57
76
  # fails. *Note* that this will also be the case if an exception is raised
58
77
  # while calling the action.
59
- #
78
+ #
60
79
  # === Indirect transitions
61
- #
80
+ #
62
81
  # In addition to the action being run as the _result_ of an event, the action
63
82
  # can also be used to run events itself. For example, using the above as an
64
83
  # example:
65
- #
84
+ #
66
85
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
67
- #
86
+ #
68
87
  # vehicle.state_event = 'ignite'
69
88
  # vehicle.save # => true
70
89
  # vehicle.state # => "idling"
71
90
  # vehicle.state_event # => nil
72
- #
91
+ #
73
92
  # As can be seen, the +save+ action automatically invokes the event stored in
74
93
  # the +state_event+ attribute (<tt>:ignite</tt> in this case).
75
- #
94
+ #
76
95
  # One important note about using this technique for running transitions is
77
96
  # that if the class in which the state machine is defined *also* defines the
78
97
  # action being invoked (and not a superclass), then it must manually run the
79
98
  # StateMachine hook that checks for event attributes.
80
- #
99
+ #
81
100
  # For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel,
82
101
  # the default action (+save+) is already defined in a base class. As a result,
83
102
  # when a state machine is defined in a model / resource, StateMachine can
84
103
  # automatically hook into the +save+ action.
85
- #
104
+ #
86
105
  # On the other hand, the Vehicle class from above defined its own +save+
87
106
  # method (and there is no +save+ method in its superclass). As a result, it
88
107
  # must be modified like so:
89
- #
108
+ #
90
109
  # def save
91
110
  # self.class.state_machines.transitions(self, :save).perform do
92
111
  # @saving_state = state
93
112
  # fail != true
94
113
  # end
95
114
  # end
96
- #
115
+ #
97
116
  # This will add in the functionality for firing the event stored in the
98
117
  # +state_event+ attribute.
99
- #
118
+ #
100
119
  # == Callbacks
101
- #
120
+ #
102
121
  # Callbacks are supported for hooking before and after every possible
103
122
  # transition in the machine. Each callback is invoked in the order in which
104
123
  # it was defined. See StateMachines::Machine#before_transition and
105
124
  # StateMachines::Machine#after_transition for documentation on how to define
106
125
  # new callbacks.
107
- #
126
+ #
108
127
  # *Note* that callbacks only get executed within the context of an event. As
109
128
  # a result, if a class has an initial state when it's created, any callbacks
110
129
  # that would normally get executed when the object enters that state will
111
130
  # *not* get triggered.
112
- #
131
+ #
113
132
  # For example,
114
- #
133
+ #
115
134
  # class Vehicle
116
- # state_machine :initial => :parked do
135
+ # state_machine initial: :parked do
117
136
  # after_transition all => :parked do
118
137
  # raise ArgumentError
119
138
  # end
120
139
  # ...
121
140
  # end
122
141
  # end
123
- #
142
+ #
124
143
  # vehicle = Vehicle.new # => #<Vehicle id: 1, state: "parked">
125
144
  # vehicle.save # => true (no exception raised)
126
- #
145
+ #
127
146
  # If you need callbacks to get triggered when an object is created, this
128
147
  # should be done by one of the following techniques:
129
148
  # * Use a <tt>before :create</tt> or equivalent hook:
130
- #
149
+ #
131
150
  # class Vehicle
132
151
  # before :create, :track_initial_transition
133
- #
152
+ #
134
153
  # state_machine do
135
154
  # ...
136
155
  # end
137
156
  # end
138
- #
157
+ #
139
158
  # * Set an initial state and use the correct event to create the
140
159
  # object with the proper state, resulting in callbacks being triggered and
141
160
  # the object getting persisted (note that the <tt>:pending</tt> state is
142
161
  # actually stored as nil):
143
- #
162
+ #
144
163
  # class Vehicle
145
- # state_machine :initial => :pending
146
- # after_transition :pending => :parked, :do => :track_initial_transition
147
- #
164
+ # state_machine initial: :pending
165
+ # after_transition pending: :parked, do: :track_initial_transition
166
+ #
148
167
  # event :park do
149
- # transition :pending => :parked
168
+ # transition pending: :parked
150
169
  # end
151
- #
152
- # state :pending, :value => nil
170
+ #
171
+ # state :pending, value: nil
153
172
  # end
154
173
  # end
155
- #
174
+ #
156
175
  # vehicle = Vehicle.new
157
176
  # vehicle.park
158
- #
177
+ #
159
178
  # * Use a default event attribute that will automatically trigger when the
160
179
  # configured action gets run (note that the <tt>:pending</tt> state is
161
180
  # actually stored as nil):
162
- #
181
+ #
163
182
  # class Vehicle < ActiveRecord::Base
164
- # state_machine :initial => :pending
165
- # after_transition :pending => :parked, :do => :track_initial_transition
166
- #
183
+ # state_machine initial: :pending
184
+ # after_transition pending: :parked, do: :track_initial_transition
185
+ #
167
186
  # event :park do
168
- # transition :pending => :parked
187
+ # transition pending: :parked
169
188
  # end
170
- #
171
- # state :pending, :value => nil
189
+ #
190
+ # state :pending, value: nil
172
191
  # end
173
- #
192
+ #
174
193
  # def initialize(*)
175
194
  # super
176
195
  # self.state_event = 'park'
177
196
  # end
178
197
  # end
179
- #
198
+ #
180
199
  # vehicle = Vehicle.new
181
200
  # vehicle.save
182
- #
201
+ #
183
202
  # === Canceling callbacks
184
- #
203
+ #
185
204
  # Callbacks can be canceled by throwing :halt at any point during the
186
205
  # callback. For example,
187
- #
206
+ #
188
207
  # ...
189
208
  # throw :halt
190
209
  # ...
191
- #
210
+ #
192
211
  # If a +before+ callback halts the chain, the associated transition and all
193
212
  # later callbacks are canceled. If an +after+ callback halts the chain,
194
213
  # the later callbacks are canceled, but the transition is still successful.
195
- #
214
+ #
196
215
  # These same rules apply to +around+ callbacks with the exception that any
197
216
  # +around+ callback that doesn't yield will essentially result in :halt being
198
217
  # thrown. Any code executed after the yield will behave in the same way as
199
218
  # +after+ callbacks.
200
- #
219
+ #
201
220
  # *Note* that if a +before+ callback fails and the bang version of an event
202
221
  # was invoked, an exception will be raised instead of returning false. For
203
222
  # example,
204
- #
223
+ #
205
224
  # class Vehicle
206
225
  # state_machine :initial => :parked do
207
226
  # before_transition any => :idling, :do => lambda {|vehicle| throw :halt}
208
227
  # ...
209
228
  # end
210
229
  # end
211
- #
230
+ #
212
231
  # vehicle = Vehicle.new
213
232
  # vehicle.park # => false
214
233
  # vehicle.park! # => StateMachines::InvalidTransition: Cannot transition state via :park from "idling"
215
- #
234
+ #
216
235
  # == Observers
217
- #
236
+ #
218
237
  # Observers, in the sense of external classes and *not* Ruby's Observable
219
238
  # mechanism, can hook into state machines as well. Such observers use the
220
239
  # same callback api that's used internally.
221
- #
240
+ #
222
241
  # Below are examples of defining observers for the following state machine:
223
- #
242
+ #
224
243
  # class Vehicle
225
244
  # state_machine do
226
245
  # event :park do
227
- # transition :idling => :parked
246
+ # transition idling: :parked
228
247
  # end
229
248
  # ...
230
249
  # end
231
250
  # ...
232
251
  # end
233
- #
252
+ #
234
253
  # Event/Transition behaviors:
235
- #
254
+ #
236
255
  # class VehicleObserver
237
256
  # def self.before_park(vehicle, transition)
238
257
  # logger.info "#{vehicle} instructed to park... state is: #{transition.from}, state will be: #{transition.to}"
239
258
  # end
240
- #
259
+ #
241
260
  # def self.after_park(vehicle, transition, result)
242
261
  # logger.info "#{vehicle} instructed to park... state was: #{transition.from}, state is: #{transition.to}"
243
262
  # end
244
- #
263
+ #
245
264
  # def self.before_transition(vehicle, transition)
246
265
  # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} is: #{transition.from}, #{transition.attribute} will be: #{transition.to}"
247
266
  # end
248
- #
267
+ #
249
268
  # def self.after_transition(vehicle, transition)
250
269
  # logger.info "#{vehicle} instructed to #{transition.event}... #{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}"
251
270
  # end
252
- #
271
+ #
253
272
  # def self.around_transition(vehicle, transition)
254
273
  # logger.info Benchmark.measure { yield }
255
274
  # end
256
275
  # end
257
- #
276
+ #
258
277
  # Vehicle.state_machine do
259
278
  # before_transition :on => :park, :do => VehicleObserver.method(:before_park)
260
279
  # before_transition VehicleObserver.method(:before_transition)
261
- #
280
+ #
262
281
  # after_transition :on => :park, :do => VehicleObserver.method(:after_park)
263
282
  # after_transition VehicleObserver.method(:after_transition)
264
- #
283
+ #
265
284
  # around_transition VehicleObserver.method(:around_transition)
266
285
  # end
267
- #
286
+ #
268
287
  # One common callback is to record transitions for all models in the system
269
288
  # for auditing/debugging purposes. Below is an example of an observer that
270
289
  # can easily automate this process for all models:
271
- #
290
+ #
272
291
  # class StateMachineObserver
273
292
  # def self.before_transition(object, transition)
274
293
  # Audit.log_transition(object.attributes)
275
294
  # end
276
295
  # end
277
- #
296
+ #
278
297
  # [Vehicle, Switch, Project].each do |klass|
279
298
  # klass.state_machines.each do |attribute, machine|
280
299
  # machine.before_transition StateMachineObserver.method(:before_transition)
281
300
  # end
282
301
  # end
283
- #
302
+ #
284
303
  # Additional observer-like behavior may be exposed by the various integrations
285
304
  # available. See below for more information on integrations.
286
- #
305
+ #
287
306
  # == Overriding instance / class methods
288
- #
307
+ #
289
308
  # Hooking in behavior to the generated instance / class methods from the
290
309
  # state machine, events, and states is very simple because of the way these
291
310
  # methods are generated on the class. Using the class's ancestors, the
292
311
  # original generated method can be referred to via +super+. For example,
293
- #
312
+ #
294
313
  # class Vehicle
295
314
  # state_machine do
296
315
  # event :park do
297
316
  # ...
298
317
  # end
299
318
  # end
300
- #
319
+ #
301
320
  # def park(*args)
302
321
  # logger.info "..."
303
322
  # super
304
323
  # end
305
324
  # end
306
- #
325
+ #
307
326
  # In the above example, the +park+ instance method that's generated on the
308
327
  # Vehicle class (by the associated event) is overridden with custom behavior.
309
328
  # Once this behavior is complete, the original method from the state machine
310
329
  # is invoked by simply calling +super+.
311
- #
330
+ #
312
331
  # The same technique can be used for +state+, +state_name+, and all other
313
332
  # instance *and* class methods on the Vehicle class.
314
333
  #
315
334
  # == Method conflicts
316
- #
335
+ #
317
336
  # By default state_machine does not redefine methods that exist on
318
337
  # superclasses (*including* Object) or any modules (*including* Kernel) that
319
338
  # were included before it was defined. This is in order to ensure that
320
339
  # existing behavior on the class is not broken by the inclusion of
321
340
  # state_machine.
322
- #
341
+ #
323
342
  # If a conflicting method is detected, state_machine will generate a warning.
324
343
  # For example, consider the following class:
325
- #
344
+ #
326
345
  # class Vehicle
327
346
  # state_machine do
328
347
  # event :open do
@@ -330,67 +349,67 @@ module StateMachines
330
349
  # end
331
350
  # end
332
351
  # end
333
- #
352
+ #
334
353
  # In the above class, an event named "open" is defined for its state machine.
335
354
  # However, "open" is already defined as an instance method in Ruby's Kernel
336
355
  # module that gets included in every Object. As a result, state_machine will
337
356
  # generate the following warning:
338
- #
357
+ #
339
358
  # Instance method "open" is already defined in Object, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true.
340
- #
359
+ #
341
360
  # Even though you may not be using Kernel's implementation of the "open"
342
361
  # instance method, state_machine isn't aware of this and, as a result, stays
343
362
  # safe and just skips redefining the method.
344
- #
363
+ #
345
364
  # As with almost all helpers methods defined by state_machine in your class,
346
365
  # there are generic methods available for working around this method conflict.
347
366
  # In the example above, you can invoke the "open" event like so:
348
- #
367
+ #
349
368
  # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
350
369
  # vehicle.fire_events(:open) # => true
351
- #
370
+ #
352
371
  # # This will not work
353
372
  # vehicle.open # => NoMethodError: private method `open' called for #<Vehicle:0xb72686b4 @state=nil>
354
- #
373
+ #
355
374
  # If you want to take on the risk of overriding existing methods and just
356
375
  # ignore method conflicts altogether, you can do so by setting the following
357
376
  # configuration:
358
- #
377
+ #
359
378
  # StateMachines::Machine.ignore_method_conflicts = true
360
- #
379
+ #
361
380
  # This will allow you to define events like "open" as described above and
362
381
  # still generate the "open" instance helper method. For example:
363
- #
382
+ #
364
383
  # StateMachines::Machine.ignore_method_conflicts = true
365
- #
384
+ #
366
385
  # class Vehicle
367
386
  # state_machine do
368
387
  # event :open do
369
388
  # ...
370
389
  # end
371
390
  # end
372
- #
391
+ #
373
392
  # vehicle = Vehicle.new # => #<Vehicle:0xb72686b4 @state=nil>
374
393
  # vehicle.open # => true
375
- #
394
+ #
376
395
  # By default, state_machine helps prevent you from making mistakes and
377
396
  # accidentally overriding methods that you didn't intend to. Once you
378
397
  # understand this and what the consequences are, setting the
379
398
  # +ignore_method_conflicts+ option is a perfectly reasonable workaround.
380
- #
399
+ #
381
400
  # == Integrations
382
- #
401
+ #
383
402
  # By default, state machines are library-agnostic, meaning that they work
384
403
  # on any Ruby class and have no external dependencies. However, there are
385
404
  # certain libraries which expose additional behavior that can be taken
386
405
  # advantage of by state machines.
387
- #
406
+ #
388
407
  # This library is built to work out of the box with a few popular Ruby
389
408
  # libraries that allow for additional behavior to provide a cleaner and
390
409
  # smoother experience. This is especially the case for objects backed by a
391
410
  # database that may allow for transactions, persistent storage,
392
411
  # search/filters, callbacks, etc.
393
- #
412
+ #
394
413
  # When a state machine is defined for classes using any of the above libraries,
395
414
  # it will try to automatically determine the integration to use (Agnostic,
396
415
  # ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel)
@@ -398,67 +417,25 @@ module StateMachines
398
417
  # machine's behavior, refer to all constants defined under the
399
418
  # StateMachines::Integrations namespace.
400
419
  class Machine
401
-
420
+ extend ClassMethods
402
421
  include EvalHelpers
403
422
  include MatcherHelpers
404
-
405
- class << self
406
- # Attempts to find or create a state machine for the given class. For
407
- # example,
408
- #
409
- # StateMachines::Machine.find_or_create(Vehicle)
410
- # StateMachines::Machine.find_or_create(Vehicle, :initial => :parked)
411
- # StateMachines::Machine.find_or_create(Vehicle, :status)
412
- # StateMachines::Machine.find_or_create(Vehicle, :status, :initial => :parked)
413
- #
414
- # If a machine of the given name already exists in one of the class's
415
- # superclasses, then a copy of that machine will be created and stored
416
- # in the new owner class (the original will remain unchanged).
417
- def find_or_create(owner_class, *args, &block)
418
- options = args.last.is_a?(Hash) ? args.pop : {}
419
- name = args.first || :state
420
-
421
- # Find an existing machine
422
- machine = owner_class.respond_to?(:state_machines) &&
423
- (args.first && owner_class.state_machines[name] || !args.first &&
424
- owner_class.state_machines.values.first) || nil
425
-
426
- if machine
427
- # Only create a new copy if changes are being made to the machine in
428
- # a subclass
429
- if machine.owner_class != owner_class && (options.any? || block_given?)
430
- machine = machine.clone
431
- machine.initial_state = options[:initial] if options.include?(:initial)
432
- machine.owner_class = owner_class
433
- end
434
-
435
- # Evaluate DSL
436
- machine.instance_eval(&block) if block_given?
437
- else
438
- # No existing machine: create a new one
439
- machine = new(owner_class, name, options, &block)
440
- end
441
-
442
- machine
443
- end
444
-
445
-
446
- def draw(*)
447
- fail NotImplementedError
448
- end
449
-
450
- # Default messages to use for validation errors in ORM integrations
451
- attr_accessor :default_messages
452
- attr_accessor :ignore_method_conflicts
453
- end
454
- @default_messages = {
455
- :invalid => 'is invalid',
456
- :invalid_event => 'cannot transition when %s',
457
- :invalid_transition => 'cannot transition via "%1$s"'
458
- }
423
+ include Utilities
424
+ include Parsing
425
+ include Validation
426
+ include HelperGenerators
427
+ include ActionHooks
428
+ include Scoping
429
+ include Configuration
430
+ include StateMethods
431
+ include EventMethods
432
+ include Callbacks
433
+ include Rendering
434
+ include Integration
459
435
 
460
436
  # Whether to ignore any conflicts that are detected for helper methods that
461
437
  # get generated for a machine's owner class. Default is false.
438
+ # Thread-safe via atomic reference updates
462
439
  @ignore_method_conflicts = false
463
440
 
464
441
  # The class that the machine is defined in
@@ -479,12 +456,12 @@ module StateMachines
479
456
  # * Event transitions (:to, :from, and :except_from options)
480
457
  # * Transition callbacks (:to, :from, :except_to, and :except_from options)
481
458
  # * Unreferenced states (using +other_states+ helper)
482
- #
459
+ #
483
460
  # These are sorted, by default, in the order in which they were referenced.
484
461
  attr_reader :states
485
462
 
486
463
  # The callbacks to invoke before/after a transition is performed
487
- #
464
+ #
488
465
  # Maps :before => callbacks and :after => callbacks
489
466
  attr_reader :callbacks
490
467
 
@@ -500,243 +477,102 @@ module StateMachines
500
477
  attr_reader :use_transactions
501
478
 
502
479
  # Creates a new state machine for the given attribute
503
- def initialize(owner_class, *args, &block)
504
- options = args.last.is_a?(Hash) ? args.pop : {}
505
- options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
506
-
507
- # Find an integration that matches this machine's owner class
508
- if options.include?(:integration)
509
- @integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
510
- else
511
- @integration = StateMachines::Integrations.match(owner_class)
512
- end
513
-
514
- if @integration
515
- extend @integration
516
- options = (@integration.defaults || {}).merge(options)
517
- end
518
-
519
- # Add machine-wide defaults
520
- options = {:use_transactions => true, :initialize => true}.merge(options)
521
-
522
- # Set machine configuration
523
- @name = args.first || :state
524
- @attribute = options[:attribute] || @name
525
- @events = EventCollection.new(self)
526
- @states = StateCollection.new(self)
527
- @callbacks = {:before => [], :after => [], :failure => []}
528
- @namespace = options[:namespace]
529
- @messages = options[:messages] || {}
530
- @action = options[:action]
531
- @use_transactions = options[:use_transactions]
532
- @initialize_state = options[:initialize]
533
- @action_hook_defined = false
534
- self.owner_class = owner_class
535
-
536
- # Merge with sibling machine configurations
537
- add_sibling_machine_configs
538
-
539
- # Define class integration
540
- define_helpers
541
- define_scopes(options[:plural])
542
- after_initialize
543
-
544
- # Evaluate DSL
545
- instance_eval(&block) if block_given?
546
- self.initial_state = options[:initial] unless sibling_machines.any?
547
- end
548
-
549
- # Creates a copy of this machine in addition to copies of each associated
550
- # event/states/callback, so that the modifications to those collections do
551
- # not affect the original machine.
552
- def initialize_copy(orig) #:nodoc:
553
- super
554
-
555
- @events = @events.dup
556
- @events.machine = self
557
- @states = @states.dup
558
- @states.machine = self
559
- @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
560
- end
561
-
562
- # Sets the class which is the owner of this state machine. Any methods
563
- # generated by states, events, or other parts of the machine will be defined
564
- # on the given owner class.
565
- def owner_class=(klass)
566
- @owner_class = klass
567
-
568
- # Create modules for extending the class with state/event-specific methods
569
- @helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
570
- owner_class.class_eval do
571
- extend helper_modules[:class]
572
- include helper_modules[:instance]
573
- end
574
-
575
- # Add class-/instance-level methods to the owner class for state initialization
576
- unless owner_class < StateMachines::InstanceMethods
577
- owner_class.class_eval do
578
- extend StateMachines::ClassMethods
579
- include StateMachines::InstanceMethods
580
- end
581
-
582
- define_state_initializer if @initialize_state
583
- end
584
-
585
- # Record this machine as matched to the name in the current owner class.
586
- # This will override any machines mapped to the same name in any superclasses.
587
- owner_class.state_machines[name] = self
588
- end
589
-
590
- # Sets the initial state of the machine. This can be either the static name
591
- # of a state or a lambda block which determines the initial state at
592
- # creation time.
593
- def initial_state=(new_initial_state)
594
- @initial_state = new_initial_state
595
- add_states([@initial_state]) unless dynamic_initial_state?
596
-
597
- # Update all states to reflect the new initial state
598
- states.each { |state| state.initial = (state.name == @initial_state) }
599
-
600
- # Output a warning if there are conflicting initial states for the machine's
601
- # attribute
602
- initial_state = states.detect { |state| state.initial }
603
- if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
604
- warn(
605
- "Both #{owner_class.name} and its #{name.inspect} machine have defined "\
606
- "a different default for \"#{attribute}\". Use only one or the other for "\
607
- "defining defaults to avoid unexpected behaviors."
608
- )
609
- end
610
- end
611
480
 
612
481
  # Gets the initial state of the machine for the given object. If a dynamic
613
482
  # initial state was configured for this machine, then the object will be
614
483
  # passed into the lambda block to help determine the actual state.
615
- #
484
+ #
616
485
  # == Examples
617
- #
486
+ #
618
487
  # With a static initial state:
619
- #
488
+ #
620
489
  # class Vehicle
621
490
  # state_machine :initial => :parked do
622
491
  # ...
623
492
  # end
624
493
  # end
625
- #
494
+ #
626
495
  # vehicle = Vehicle.new
627
496
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
628
- #
497
+ #
629
498
  # With a dynamic initial state:
630
- #
499
+ #
631
500
  # class Vehicle
632
501
  # attr_accessor :force_idle
633
- #
502
+ #
634
503
  # state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
635
504
  # ...
636
505
  # end
637
506
  # end
638
- #
507
+ #
639
508
  # vehicle = Vehicle.new
640
- #
509
+ #
641
510
  # vehicle.force_idle = true
642
511
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
643
- #
512
+ #
644
513
  # vehicle.force_idle = false
645
514
  # Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
646
- def initial_state(object)
647
- states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
648
- end
649
-
650
- # Whether a dynamic initial state is being used in the machine
651
- def dynamic_initial_state?
652
- instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
653
- end
654
-
655
- # Initializes the state on the given object. Initial values are only set if
656
- # the machine's attribute hasn't been previously initialized.
657
- #
658
- # Configuration options:
659
- # * <tt>:force</tt> - Whether to initialize the state regardless of its
660
- # current value
661
- # * <tt>:to</tt> - A hash to set the initial value in instead of writing
662
- # directly to the object
663
- def initialize_state(object, options = {})
664
- state = initial_state(object)
665
- if state && (options[:force] || initialize_state?(object))
666
- value = state.value
667
-
668
- if hash = options[:to]
669
- hash[attribute.to_s] = value
670
- else
671
- write(object, :state, value)
672
- end
673
- end
674
- end
675
-
676
- # Gets the actual name of the attribute on the machine's owner class that
677
- # stores data with the given name.
678
- def attribute(name = :state)
679
- name == :state ? @attribute : :"#{self.name}_#{name}"
680
- end
681
515
 
682
516
  # Defines a new helper method in an instance or class scope with the given
683
517
  # name. If the method is already defined in the scope, then this will not
684
518
  # override it.
685
- #
519
+ #
686
520
  # If passing in a block, there are two side effects to be aware of
687
521
  # 1. The method cannot be chained, meaning that the block cannot call +super+
688
522
  # 2. If the method is already defined in an ancestor, then it will not get
689
523
  # overridden and a warning will be output.
690
- #
524
+ #
691
525
  # Example:
692
- #
526
+ #
693
527
  # # Instance helper
694
528
  # machine.define_helper(:instance, :state_name) do |machine, object|
695
529
  # machine.states.match(object).name
696
530
  # end
697
- #
531
+ #
698
532
  # # Class helper
699
533
  # machine.define_helper(:class, :state_machine_name) do |machine, klass|
700
534
  # "State"
701
535
  # end
702
- #
536
+ #
703
537
  # You can also define helpers using string evaluation like so:
704
- #
538
+ #
705
539
  # # Instance helper
706
540
  # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
707
541
  # def state_name
708
542
  # self.class.state_machine(:state).states.match(self).name
709
543
  # end
710
544
  # end_eval
711
- #
545
+ #
712
546
  # # Class helper
713
547
  # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
714
548
  # def state_machine_name
715
549
  # "State"
716
550
  # end
717
551
  # end_eval
718
- def define_helper(scope, method, *args, &block)
552
+ def define_helper(scope, method, *, **, &block)
719
553
  helper_module = @helper_modules.fetch(scope)
720
554
 
721
555
  if block_given?
722
- if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
556
+ if !self.class.ignore_method_conflicts && (conflicting_ancestor = owner_class_ancestor_has_method?(scope, method))
723
557
  ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
724
558
  warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead or set StateMachines::Machine.ignore_method_conflicts = true."
725
559
  else
726
560
  name = self.name
727
561
  helper_module.class_eval do
728
- define_method(method) do |*block_args|
729
- block.call((scope == :instance ? self.class : self).state_machine(name), self, *block_args)
562
+ define_method(method) do |*args, **kwargs|
563
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *args, **kwargs)
730
564
  end
731
565
  end
732
566
  end
733
567
  else
734
- helper_module.class_eval(method, *args)
568
+ # Validate string input before eval if method is a string
569
+ validate_eval_string(method) if method.is_a?(String)
570
+ helper_module.class_eval(method, __FILE__, __LINE__)
735
571
  end
736
572
  end
737
573
 
738
574
  # Customizes the definition of one or more states in the machine.
739
- #
575
+ #
740
576
  # Configuration options:
741
577
  # * <tt>:value</tt> - The actual value to store when an object transitions
742
578
  # to the state. Default is the name (stringified).
@@ -748,13 +584,13 @@ module StateMachines
748
584
  # * <tt>:human_name</tt> - The human-readable version of this state's name.
749
585
  # By default, this is either defined by the integration or stringifies the
750
586
  # name and converts underscores to spaces.
751
- #
587
+ #
752
588
  # == Customizing the stored value
753
- #
589
+ #
754
590
  # Whenever a state is automatically discovered in the state machine, its
755
591
  # default value is assumed to be the stringified version of the name. For
756
592
  # example,
757
- #
593
+ #
758
594
  # class Vehicle
759
595
  # state_machine :initial => :parked do
760
596
  # event :ignite do
@@ -762,124 +598,124 @@ module StateMachines
762
598
  # end
763
599
  # end
764
600
  # end
765
- #
601
+ #
766
602
  # In the above state machine, there are two states automatically discovered:
767
603
  # :parked and :idling. These states, by default, will store their stringified
768
604
  # equivalents when an object moves into that state (e.g. "parked" / "idling").
769
- #
605
+ #
770
606
  # For legacy systems or when tying state machines into existing frameworks,
771
607
  # it's oftentimes necessary to need to store a different value for a state
772
608
  # than the default. In order to continue taking advantage of an expressive
773
609
  # state machine and helper methods, every defined state can be re-configured
774
610
  # with a custom stored value. For example,
775
- #
611
+ #
776
612
  # class Vehicle
777
613
  # state_machine :initial => :parked do
778
614
  # event :ignite do
779
615
  # transition :parked => :idling
780
616
  # end
781
- #
617
+ #
782
618
  # state :idling, :value => 'IDLING'
783
619
  # state :parked, :value => 'PARKED
784
620
  # end
785
621
  # end
786
- #
622
+ #
787
623
  # This is also useful if being used in association with a database and,
788
624
  # instead of storing the state name in a column, you want to store the
789
625
  # state's foreign key:
790
- #
626
+ #
791
627
  # class VehicleState < ActiveRecord::Base
792
628
  # end
793
- #
629
+ #
794
630
  # class Vehicle < ActiveRecord::Base
795
631
  # state_machine :attribute => :state_id, :initial => :parked do
796
632
  # event :ignite do
797
633
  # transition :parked => :idling
798
634
  # end
799
- #
635
+ #
800
636
  # states.each do |state|
801
637
  # self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
802
638
  # end
803
639
  # end
804
640
  # end
805
- #
641
+ #
806
642
  # In the above example, each known state is configured to store it's
807
643
  # associated database id in the +state_id+ attribute. Also, notice that a
808
644
  # lambda block is used to define the state's value. This is required in
809
645
  # situations (like testing) where the model is loaded without any existing
810
646
  # data (i.e. no VehicleState records available).
811
- #
647
+ #
812
648
  # One caveat to the above example is to keep performance in mind. To avoid
813
649
  # constant db hits for looking up the VehicleState ids, the value is cached
814
650
  # by specifying the <tt>:cache</tt> option. Alternatively, a custom
815
651
  # caching strategy can be used like so:
816
- #
652
+ #
817
653
  # class VehicleState < ActiveRecord::Base
818
654
  # cattr_accessor :cache_store
819
655
  # self.cache_store = ActiveSupport::Cache::MemoryStore.new
820
- #
656
+ #
821
657
  # def self.find_by_name(name)
822
658
  # cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
823
659
  # end
824
660
  # end
825
- #
661
+ #
826
662
  # === Dynamic values
827
- #
663
+ #
828
664
  # In addition to customizing states with other value types, lambda blocks
829
665
  # can also be specified to allow for a state's value to be determined
830
666
  # dynamically at runtime. For example,
831
- #
667
+ #
832
668
  # class Vehicle
833
669
  # state_machine :purchased_at, :initial => :available do
834
670
  # event :purchase do
835
671
  # transition all => :purchased
836
672
  # end
837
- #
673
+ #
838
674
  # event :restock do
839
675
  # transition all => :available
840
676
  # end
841
- #
677
+ #
842
678
  # state :available, :value => nil
843
679
  # state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
844
680
  # end
845
681
  # end
846
- #
682
+ #
847
683
  # In the above definition, the <tt>:purchased</tt> state is customized with
848
684
  # both a dynamic value *and* a value matcher.
849
- #
685
+ #
850
686
  # When an object transitions to the purchased state, the value's lambda
851
687
  # block will be called. This will get the current time and store it in the
852
688
  # object's +purchased_at+ attribute.
853
- #
689
+ #
854
690
  # *Note* that the custom matcher is very important here. Since there's no
855
691
  # way for the state machine to figure out an object's state when it's set to
856
692
  # a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
857
693
  # were not configured for the state, then an ArgumentError exception would
858
694
  # be raised at runtime, indicating that the state machine could not figure
859
695
  # out what the current state of the object was.
860
- #
696
+ #
861
697
  # == Behaviors
862
- #
698
+ #
863
699
  # Behaviors define a series of methods to mixin with objects when the current
864
700
  # state matches the given one(s). This allows instance methods to behave
865
701
  # a specific way depending on what the value of the object's state is.
866
- #
702
+ #
867
703
  # For example,
868
- #
704
+ #
869
705
  # class Vehicle
870
706
  # attr_accessor :driver
871
707
  # attr_accessor :passenger
872
- #
708
+ #
873
709
  # state_machine :initial => :parked do
874
710
  # event :ignite do
875
711
  # transition :parked => :idling
876
712
  # end
877
- #
713
+ #
878
714
  # state :parked do
879
715
  # def speed
880
716
  # 0
881
717
  # end
882
- #
718
+ #
883
719
  # def rotate_driver
884
720
  # driver = self.driver
885
721
  # self.driver = passenger
@@ -887,97 +723,97 @@ module StateMachines
887
723
  # true
888
724
  # end
889
725
  # end
890
- #
726
+ #
891
727
  # state :idling, :first_gear do
892
728
  # def speed
893
729
  # 20
894
730
  # end
895
- #
731
+ #
896
732
  # def rotate_driver
897
733
  # self.state = 'parked'
898
734
  # rotate_driver
899
735
  # end
900
736
  # end
901
- #
737
+ #
902
738
  # other_states :backing_up
903
739
  # end
904
740
  # end
905
- #
741
+ #
906
742
  # In the above example, there are two dynamic behaviors defined for the
907
743
  # class:
908
744
  # * +speed+
909
745
  # * +rotate_driver+
910
- #
746
+ #
911
747
  # Each of these behaviors are instance methods on the Vehicle class. However,
912
748
  # which method actually gets invoked is based on the current state of the
913
749
  # object. Using the above class as the example:
914
- #
750
+ #
915
751
  # vehicle = Vehicle.new
916
752
  # vehicle.driver = 'John'
917
753
  # vehicle.passenger = 'Jane'
918
- #
754
+ #
919
755
  # # Behaviors in the "parked" state
920
756
  # vehicle.state # => "parked"
921
757
  # vehicle.speed # => 0
922
758
  # vehicle.rotate_driver # => true
923
759
  # vehicle.driver # => "Jane"
924
760
  # vehicle.passenger # => "John"
925
- #
761
+ #
926
762
  # vehicle.ignite # => true
927
- #
763
+ #
928
764
  # # Behaviors in the "idling" state
929
765
  # vehicle.state # => "idling"
930
766
  # vehicle.speed # => 20
931
767
  # vehicle.rotate_driver # => true
932
768
  # vehicle.driver # => "John"
933
769
  # vehicle.passenger # => "Jane"
934
- #
770
+ #
935
771
  # As can be seen, both the +speed+ and +rotate_driver+ instance method
936
772
  # implementations changed how they behave based on what the current state
937
773
  # of the vehicle was.
938
- #
774
+ #
939
775
  # === Invalid behaviors
940
- #
776
+ #
941
777
  # If a specific behavior has not been defined for a state, then a
942
778
  # NoMethodError exception will be raised, indicating that that method would
943
779
  # not normally exist for an object with that state.
944
- #
780
+ #
945
781
  # Using the example from before:
946
- #
782
+ #
947
783
  # vehicle = Vehicle.new
948
784
  # vehicle.state = 'backing_up'
949
785
  # vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
950
- #
786
+ #
951
787
  # === Using matchers
952
- #
788
+ #
953
789
  # The +all+ / +any+ matchers can be used to easily define behaviors for a
954
790
  # group of states. Note, however, that you cannot use these matchers to
955
791
  # set configurations for states. Behaviors using these matchers can be
956
792
  # defined at any point in the state machine and will always get applied to
957
793
  # the proper states.
958
- #
794
+ #
959
795
  # For example:
960
- #
796
+ #
961
797
  # state_machine :initial => :parked do
962
798
  # ...
963
- #
799
+ #
964
800
  # state all - [:parked, :idling, :stalled] do
965
801
  # validates_presence_of :speed
966
- #
802
+ #
967
803
  # def speed
968
804
  # gear * 10
969
805
  # end
970
806
  # end
971
807
  # end
972
- #
808
+ #
973
809
  # == State-aware class methods
974
- #
810
+ #
975
811
  # In addition to defining scopes for instance methods that are state-aware,
976
812
  # the same can be done for certain types of class methods.
977
- #
813
+ #
978
814
  # Some libraries have support for class-level methods that only run certain
979
815
  # behaviors based on a conditions hash passed in. For example:
980
- #
816
+ #
981
817
  # class Vehicle < ActiveRecord::Base
982
818
  # state_machine do
983
819
  # ...
@@ -987,110 +823,35 @@ module StateMachines
987
823
  # end
988
824
  # end
989
825
  # end
990
- #
826
+ #
991
827
  # In the above ActiveRecord model, two validations have been defined which
992
828
  # will *only* run when the Vehicle object is in one of the three states:
993
829
  # +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
994
830
  # conditions can continue to be used.
995
- #
831
+ #
996
832
  # This functionality is not library-specific and can work for any class-level
997
833
  # method that is defined like so:
998
- #
834
+ #
999
835
  # def validates_presence_of(attribute, options = {})
1000
836
  # ...
1001
837
  # end
1002
- #
838
+ #
1003
839
  # The minimum requirement is that the last argument in the method be an
1004
840
  # options hash which contains at least <tt>:if</tt> condition support.
1005
- def state(*names, &block)
1006
- options = names.last.is_a?(Hash) ? names.pop : {}
1007
- options.assert_valid_keys(:value, :cache, :if, :human_name)
1008
-
1009
- # Store the context so that it can be used for / matched against any state
1010
- # that gets added
1011
- @states.context(names, &block) if block_given?
1012
-
1013
- if names.first.is_a?(Matcher)
1014
- # Add any states referenced in the matcher. When matchers are used,
1015
- # states are not allowed to be configured.
1016
- raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
1017
- states = add_states(names.first.values)
1018
- else
1019
- states = add_states(names)
1020
-
1021
- # Update the configuration for the state(s)
1022
- states.each do |state|
1023
- if options.include?(:value)
1024
- state.value = options[:value]
1025
- self.states.update(state)
1026
- end
1027
-
1028
- state.human_name = options[:human_name] if options.include?(:human_name)
1029
- state.cache = options[:cache] if options.include?(:cache)
1030
- state.matcher = options[:if] if options.include?(:if)
1031
- end
1032
- end
1033
-
1034
- states.length == 1 ? states.first : states
1035
- end
1036
-
1037
- alias_method :other_states, :state
1038
-
1039
- # Gets the current value stored in the given object's attribute.
1040
- #
1041
- # For example,
1042
- #
1043
- # class Vehicle
1044
- # state_machine :initial => :parked do
1045
- # ...
1046
- # end
1047
- # end
1048
- #
1049
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1050
- # Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
1051
- # Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
1052
- def read(object, attribute, ivar = false)
1053
- attribute = self.attribute(attribute)
1054
- if ivar
1055
- object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
1056
- else
1057
- object.send(attribute)
1058
- end
1059
- end
1060
-
1061
- # Sets a new value in the given object's attribute.
1062
- #
1063
- # For example,
1064
- #
1065
- # class Vehicle
1066
- # state_machine :initial => :parked do
1067
- # ...
1068
- # end
1069
- # end
1070
- #
1071
- # vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
1072
- # Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
1073
- # Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
1074
- # vehicle.state # => "idling"
1075
- # vehicle.event # => "park"
1076
- def write(object, attribute, value, ivar = false)
1077
- attribute = self.attribute(attribute)
1078
- ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
1079
- end
1080
841
 
1081
842
  # Defines one or more events for the machine and the transitions that can
1082
843
  # be performed when those events are run.
1083
- #
844
+ #
1084
845
  # This method is also aliased as +on+ for improved compatibility with
1085
846
  # using a domain-specific language.
1086
- #
847
+ #
1087
848
  # Configuration options:
1088
849
  # * <tt>:human_name</tt> - The human-readable version of this event's name.
1089
850
  # By default, this is either defined by the integration or stringifies the
1090
851
  # name and converts underscores to spaces.
1091
- #
852
+ #
1092
853
  # == Instance methods
1093
- #
854
+ #
1094
855
  # The following instance methods are generated when a new event is defined
1095
856
  # (the "park" event is used as an example):
1096
857
  # * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
@@ -1114,13 +875,13 @@ module StateMachines
1114
875
  # object or nil if no transitions can be performed. Like <tt>can_park?</tt>
1115
876
  # this will also *not* run validations or callbacks. It will only
1116
877
  # determine if the state machine defines a valid transition for the event.
1117
- #
878
+ #
1118
879
  # With a namespace of "car", the above names map to the following methods:
1119
880
  # * <tt>can_park_car?</tt>
1120
881
  # * <tt>park_car_transition</tt>
1121
882
  # * <tt>park_car</tt>
1122
883
  # * <tt>park_car!</tt>
1123
- #
884
+ #
1124
885
  # The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
1125
886
  # optional set of requirements for determining what transitions are available
1126
887
  # for the current object. These requirements include:
@@ -1130,58 +891,58 @@ module StateMachines
1130
891
  # specified, then this will match any to state.
1131
892
  # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
1132
893
  # conditionals defined for each one. Default is true.
1133
- #
894
+ #
1134
895
  # == Defining transitions
1135
- #
896
+ #
1136
897
  # +event+ requires a block which allows you to define the possible
1137
898
  # transitions that can happen as a result of that event. For example,
1138
- #
899
+ #
1139
900
  # event :park, :stop do
1140
901
  # transition :idling => :parked
1141
902
  # end
1142
- #
903
+ #
1143
904
  # event :first_gear do
1144
905
  # transition :parked => :first_gear, :if => :seatbelt_on?
1145
906
  # transition :parked => same # Allow to loopback if seatbelt is off
1146
907
  # end
1147
- #
908
+ #
1148
909
  # See StateMachines::Event#transition for more information on
1149
910
  # the possible options that can be passed in.
1150
- #
911
+ #
1151
912
  # *Note* that this block is executed within the context of the actual event
1152
913
  # object. As a result, you will not be able to reference any class methods
1153
914
  # on the model without referencing the class itself. For example,
1154
- #
915
+ #
1155
916
  # class Vehicle
1156
917
  # def self.safe_states
1157
918
  # [:parked, :idling, :stalled]
1158
919
  # end
1159
- #
920
+ #
1160
921
  # state_machine do
1161
922
  # event :park do
1162
923
  # transition Vehicle.safe_states => :parked
1163
924
  # end
1164
925
  # end
1165
- # end
1166
- #
926
+ # end
927
+ #
1167
928
  # == Overriding the event method
1168
- #
929
+ #
1169
930
  # By default, this will define an instance method (with the same name as the
1170
931
  # event) that will fire the next possible transition for that. Although the
1171
932
  # +before_transition+, +after_transition+, and +around_transition+ hooks
1172
933
  # allow you to define behavior that gets executed as a result of the event's
1173
934
  # transition, you can also override the event method in order to have a
1174
935
  # little more fine-grained control.
1175
- #
936
+ #
1176
937
  # For example:
1177
- #
938
+ #
1178
939
  # class Vehicle
1179
940
  # state_machine do
1180
941
  # event :park do
1181
942
  # ...
1182
943
  # end
1183
944
  # end
1184
- #
945
+ #
1185
946
  # def park(*)
1186
947
  # take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
1187
948
  # if result = super # Runs the transition and all before/after/around hooks
@@ -1190,33 +951,33 @@ module StateMachines
1190
951
  # result
1191
952
  # end
1192
953
  # end
1193
- #
954
+ #
1194
955
  # There are a few important things to note here. First, the method
1195
956
  # signature is defined with an unlimited argument list in order to allow
1196
957
  # callers to continue passing arguments that are expected by state_machine.
1197
958
  # For example, it will still allow calls to +park+ with a single parameter
1198
959
  # for skipping the configured action.
1199
- #
960
+ #
1200
961
  # Second, the overridden event method must call +super+ in order to run the
1201
962
  # logic for running the next possible transition. In order to remain
1202
963
  # consistent with other events, the result of +super+ is returned.
1203
- #
964
+ #
1204
965
  # Third, any behavior defined in this method will *not* get executed if
1205
966
  # you're taking advantage of attribute-based event transitions. For example:
1206
- #
967
+ #
1207
968
  # vehicle = Vehicle.new
1208
969
  # vehicle.state_event = 'park'
1209
970
  # vehicle.save
1210
- #
971
+ #
1211
972
  # In this case, the +park+ event will run the before/after/around transition
1212
973
  # hooks and transition the state, but the behavior defined in the overriden
1213
974
  # +park+ method will *not* be executed.
1214
- #
975
+ #
1215
976
  # == Defining additional arguments
1216
- #
977
+ #
1217
978
  # Additional arguments can be passed into events and accessed by transition
1218
979
  # hooks like so:
1219
- #
980
+ #
1220
981
  # class Vehicle
1221
982
  # state_machine do
1222
983
  # after_transition :on => :park do |vehicle, transition|
@@ -1224,157 +985,128 @@ module StateMachines
1224
985
  # ...
1225
986
  # end
1226
987
  # after_transition :on => :park, :do => :take_deep_breath
1227
- #
988
+ #
1228
989
  # event :park do
1229
990
  # ...
1230
991
  # end
1231
- #
992
+ #
1232
993
  # def take_deep_breath(transition)
1233
994
  # kind = *transition.args # :parallel
1234
995
  # ...
1235
996
  # end
1236
997
  # end
1237
998
  # end
1238
- #
999
+ #
1239
1000
  # vehicle = Vehicle.new
1240
1001
  # vehicle.park(:parallel)
1241
- #
1002
+ #
1242
1003
  # *Remember* that if the last argument is a boolean, it will be used as the
1243
1004
  # +run_action+ parameter to the event action. Using the +park+ action
1244
1005
  # example from above, you can might call it like so:
1245
- #
1006
+ #
1246
1007
  # vehicle.park # => Uses default args and runs machine action
1247
1008
  # vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
1248
1009
  # vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
1249
- #
1010
+ #
1250
1011
  # If you decide to override the +park+ event method *and* define additional
1251
1012
  # arguments, you can do so as shown below:
1252
- #
1013
+ #
1253
1014
  # class Vehicle
1254
1015
  # state_machine do
1255
1016
  # event :park do
1256
1017
  # ...
1257
1018
  # end
1258
1019
  # end
1259
- #
1020
+ #
1260
1021
  # def park(kind = :parallel, *args)
1261
1022
  # take_deep_breath if kind == :parallel
1262
1023
  # super
1263
1024
  # end
1264
1025
  # end
1265
- #
1026
+ #
1266
1027
  # Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
1267
1028
  # the entire arguments list to be accessed by transition callbacks through
1268
1029
  # StateMachines::Transition#args.
1269
- #
1030
+ #
1270
1031
  # === Using matchers
1271
- #
1032
+ #
1272
1033
  # The +all+ / +any+ matchers can be used to easily execute blocks for a
1273
1034
  # group of events. Note, however, that you cannot use these matchers to
1274
1035
  # set configurations for events. Blocks using these matchers can be
1275
1036
  # defined at any point in the state machine and will always get applied to
1276
1037
  # the proper events.
1277
- #
1038
+ #
1278
1039
  # For example:
1279
- #
1040
+ #
1280
1041
  # state_machine :initial => :parked do
1281
1042
  # ...
1282
- #
1043
+ #
1283
1044
  # event all - [:crash] do
1284
1045
  # transition :stalled => :parked
1285
1046
  # end
1286
1047
  # end
1287
- #
1048
+ #
1288
1049
  # == Example
1289
- #
1050
+ #
1290
1051
  # class Vehicle
1291
1052
  # state_machine do
1292
1053
  # # The park, stop, and halt events will all share the given transitions
1293
1054
  # event :park, :stop, :halt do
1294
1055
  # transition [:idling, :backing_up] => :parked
1295
1056
  # end
1296
- #
1057
+ #
1297
1058
  # event :stop do
1298
1059
  # transition :first_gear => :idling
1299
1060
  # end
1300
- #
1061
+ #
1301
1062
  # event :ignite do
1302
1063
  # transition :parked => :idling
1303
1064
  # transition :idling => same # Allow ignite while still idling
1304
1065
  # end
1305
1066
  # end
1306
1067
  # end
1307
- def event(*names, &block)
1308
- options = names.last.is_a?(Hash) ? names.pop : {}
1309
- options.assert_valid_keys(:human_name)
1310
-
1311
- # Store the context so that it can be used for / matched against any event
1312
- # that gets added
1313
- @events.context(names, &block) if block_given?
1314
-
1315
- if names.first.is_a?(Matcher)
1316
- # Add any events referenced in the matcher. When matchers are used,
1317
- # events are not allowed to be configured.
1318
- raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
1319
- events = add_events(names.first.values)
1320
- else
1321
- events = add_events(names)
1322
-
1323
- # Update the configuration for the event(s)
1324
- events.each do |event|
1325
- event.human_name = options[:human_name] if options.include?(:human_name)
1326
-
1327
- # Add any states that may have been referenced within the event
1328
- add_states(event.known_states)
1329
- end
1330
- end
1331
-
1332
- events.length == 1 ? events.first : events
1333
- end
1334
-
1335
- alias_method :on, :event
1336
1068
 
1337
1069
  # Creates a new transition that determines what to change the current state
1338
1070
  # to when an event fires.
1339
- #
1071
+ #
1340
1072
  # == Defining transitions
1341
- #
1073
+ #
1342
1074
  # The options for a new transition uses the Hash syntax to map beginning
1343
1075
  # states to ending states. For example,
1344
- #
1076
+ #
1345
1077
  # transition :parked => :idling, :idling => :first_gear, :on => :ignite
1346
- #
1078
+ #
1347
1079
  # In this case, when the +ignite+ event is fired, this transition will cause
1348
1080
  # the state to be +idling+ if it's current state is +parked+ or +first_gear+
1349
1081
  # if it's current state is +idling+.
1350
- #
1082
+ #
1351
1083
  # To help define these implicit transitions, a set of helpers are available
1352
1084
  # for slightly more complex matching:
1353
1085
  # * <tt>all</tt> - Matches every state in the machine
1354
1086
  # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
1355
1087
  # * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
1356
1088
  # * <tt>same</tt> - Matches the same state being transitioned from
1357
- #
1089
+ #
1358
1090
  # See StateMachines::MatcherHelpers for more information.
1359
- #
1091
+ #
1360
1092
  # Examples:
1361
- #
1093
+ #
1362
1094
  # transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
1363
1095
  # transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
1364
1096
  # transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
1365
1097
  # transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
1366
1098
  # transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
1367
1099
  # transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
1368
- #
1100
+ #
1369
1101
  # transition :parked => same, :on => :park # Loops :parked back to :parked
1370
1102
  # transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
1371
1103
  # transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
1372
- #
1104
+ #
1373
1105
  # # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
1374
1106
  # transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
1375
- #
1107
+ #
1376
1108
  # == Verbose transitions
1377
- #
1109
+ #
1378
1110
  # Transitions can also be defined use an explicit set of configuration
1379
1111
  # options:
1380
1112
  # * <tt>:from</tt> - A state or array of states that can be transitioned from.
@@ -1383,24 +1115,24 @@ module StateMachines
1383
1115
  # then the transition will simply loop back (i.e. the state will not change).
1384
1116
  # * <tt>:except_from</tt> - A state or array of states that *cannot* be
1385
1117
  # transitioned from.
1386
- #
1118
+ #
1387
1119
  # These options must be used when defining transitions within the context
1388
1120
  # of a state.
1389
- #
1121
+ #
1390
1122
  # Examples:
1391
- #
1123
+ #
1392
1124
  # transition :to => nil, :on => :park
1393
1125
  # transition :to => :idling, :on => :ignite
1394
1126
  # transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
1395
1127
  # transition :from => nil, :to => :idling, :on => :ignite
1396
1128
  # transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
1397
- #
1129
+ #
1398
1130
  # == Conditions
1399
- #
1131
+ #
1400
1132
  # In addition to the state requirements for each transition, a condition
1401
1133
  # can also be defined to help determine whether that transition is
1402
1134
  # available. These options will work on both the normal and verbose syntax.
1403
- #
1135
+ #
1404
1136
  # Configuration options:
1405
1137
  # * <tt>:if</tt> - A method, proc or string to call to determine if the
1406
1138
  # transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
@@ -1408,39 +1140,30 @@ module StateMachines
1408
1140
  # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1409
1141
  # transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
1410
1142
  # The condition should return or evaluate to true or false.
1411
- #
1143
+ #
1412
1144
  # Examples:
1413
- #
1145
+ #
1414
1146
  # transition :parked => :idling, :on => :ignite, :if => :moving?
1415
1147
  # transition :parked => :idling, :on => :ignite, :unless => :stopped?
1416
1148
  # transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
1417
- #
1149
+ #
1418
1150
  # transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
1419
1151
  # transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
1420
- #
1152
+ #
1421
1153
  # == Order of operations
1422
- #
1154
+ #
1423
1155
  # Transitions are evaluated in the order in which they're defined. As a
1424
1156
  # result, if more than one transition applies to a given object, then the
1425
1157
  # first transition that matches will be performed.
1426
- def transition(options)
1427
- raise ArgumentError, 'Must specify :on event' unless options[:on]
1428
-
1429
- branches = []
1430
- options = options.dup
1431
- event(*Array(options.delete(:on))) { branches << transition(options) }
1432
-
1433
- branches.length == 1 ? branches.first : branches
1434
- end
1435
1158
 
1436
1159
  # Creates a callback that will be invoked *before* a transition is
1437
1160
  # performed so long as the given requirements match the transition.
1438
- #
1161
+ #
1439
1162
  # == The callback
1440
- #
1163
+ #
1441
1164
  # Callbacks must be defined as either an argument, in the :do option, or
1442
1165
  # as a block. For example,
1443
- #
1166
+ #
1444
1167
  # class Vehicle
1445
1168
  # state_machine do
1446
1169
  # before_transition :set_alarm
@@ -1452,13 +1175,13 @@ module StateMachines
1452
1175
  # ...
1453
1176
  # end
1454
1177
  # end
1455
- #
1178
+ #
1456
1179
  # Notice that the first three callbacks are the same in terms of how the
1457
1180
  # methods to invoke are defined. However, using the <tt>:do</tt> can
1458
1181
  # provide for a more fluid DSL.
1459
- #
1182
+ #
1460
1183
  # In addition, multiple callbacks can be defined like so:
1461
- #
1184
+ #
1462
1185
  # class Vehicle
1463
1186
  # state_machine do
1464
1187
  # before_transition :set_alarm, :lock_doors, all => :parked
@@ -1468,54 +1191,54 @@ module StateMachines
1468
1191
  # end
1469
1192
  # end
1470
1193
  # end
1471
- #
1194
+ #
1472
1195
  # Notice that the different ways of configuring methods can be mixed.
1473
- #
1196
+ #
1474
1197
  # == State requirements
1475
- #
1198
+ #
1476
1199
  # Callbacks can require that the machine be transitioning from and to
1477
1200
  # specific states. These requirements use a Hash syntax to map beginning
1478
1201
  # states to ending states. For example,
1479
- #
1202
+ #
1480
1203
  # before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
1481
- #
1204
+ #
1482
1205
  # In this case, the +set_alarm+ callback will only be called if the machine
1483
1206
  # is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
1484
- #
1207
+ #
1485
1208
  # To help define state requirements, a set of helpers are available for
1486
1209
  # slightly more complex matching:
1487
1210
  # * <tt>all</tt> - Matches every state/event in the machine
1488
1211
  # * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
1489
1212
  # * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
1490
1213
  # * <tt>same</tt> - Matches the same state being transitioned from
1491
- #
1214
+ #
1492
1215
  # See StateMachines::MatcherHelpers for more information.
1493
- #
1216
+ #
1494
1217
  # Examples:
1495
- #
1218
+ #
1496
1219
  # before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
1497
1220
  # before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
1498
1221
  # before_transition all => :parked, :do => ... # Matches all states to parked
1499
1222
  # before_transition any => same, :do => ... # Matches every loopback
1500
- #
1223
+ #
1501
1224
  # == Event requirements
1502
- #
1225
+ #
1503
1226
  # In addition to state requirements, an event requirement can be defined so
1504
1227
  # that the callback is only invoked on specific events using the +on+
1505
1228
  # option. This can also use the same matcher helpers as the state
1506
1229
  # requirements.
1507
- #
1230
+ #
1508
1231
  # Examples:
1509
- #
1232
+ #
1510
1233
  # before_transition :on => :ignite, :do => ... # Matches only on ignite
1511
1234
  # before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
1512
1235
  # before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
1513
- #
1236
+ #
1514
1237
  # == Verbose Requirements
1515
- #
1238
+ #
1516
1239
  # Requirements can also be defined using verbose options rather than the
1517
1240
  # implicit Hash syntax and helper methods described above.
1518
- #
1241
+ #
1519
1242
  # Configuration options:
1520
1243
  # * <tt>:from</tt> - One or more states being transitioned from. If none
1521
1244
  # are specified, then all states will match.
@@ -1526,163 +1249,173 @@ module StateMachines
1526
1249
  # * <tt>:except_from</tt> - One or more states *not* being transitioned from
1527
1250
  # * <tt>:except_to</tt> - One more states *not* being transitioned to
1528
1251
  # * <tt>:except_on</tt> - One or more events that *did not* fire the transition
1529
- #
1252
+ #
1530
1253
  # Examples:
1531
- #
1254
+ #
1532
1255
  # before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
1533
1256
  # before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
1534
- #
1257
+ #
1535
1258
  # == Conditions
1536
- #
1259
+ #
1537
1260
  # In addition to the state/event requirements, a condition can also be
1538
1261
  # defined to help determine whether the callback should be invoked.
1539
- #
1262
+ #
1540
1263
  # Configuration options:
1541
1264
  # * <tt>:if</tt> - A method, proc or string to call to determine if the
1542
1265
  # callback should occur (e.g. :if => :allow_callbacks, or
1543
1266
  # :if => lambda {|user| user.signup_step > 2}). The method, proc or string
1544
- # should return or evaluate to a true or false value.
1267
+ # should return or evaluate to a true or false value.
1545
1268
  # * <tt>:unless</tt> - A method, proc or string to call to determine if the
1546
1269
  # callback should not occur (e.g. :unless => :skip_callbacks, or
1547
1270
  # :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
1548
- # string should return or evaluate to a true or false value.
1549
- #
1271
+ # string should return or evaluate to a true or false value.
1272
+ #
1550
1273
  # Examples:
1551
- #
1274
+ #
1552
1275
  # before_transition :parked => :idling, :if => :moving?, :do => ...
1553
1276
  # before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
1554
- #
1277
+ #
1555
1278
  # == Accessing the transition
1556
- #
1279
+ #
1557
1280
  # In addition to passing the object being transitioned, the actual
1558
1281
  # transition describing the context (e.g. event, from, to) can be accessed
1559
1282
  # as well. This additional argument is only passed if the callback allows
1560
1283
  # for it.
1561
- #
1284
+ #
1562
1285
  # For example,
1563
- #
1286
+ #
1564
1287
  # class Vehicle
1565
1288
  # # Only specifies one parameter (the object being transitioned)
1566
1289
  # before_transition all => :parked do |vehicle|
1567
1290
  # vehicle.set_alarm
1568
1291
  # end
1569
- #
1292
+ #
1570
1293
  # # Specifies 2 parameters (object being transitioned and actual transition)
1571
1294
  # before_transition all => :parked do |vehicle, transition|
1572
1295
  # vehicle.set_alarm(transition)
1573
1296
  # end
1574
1297
  # end
1575
- #
1298
+ #
1576
1299
  # *Note* that the object in the callback will only be passed in as an
1577
1300
  # argument if callbacks are configured to *not* be bound to the object
1578
1301
  # involved. This is the default and may change on a per-integration basis.
1579
- #
1302
+ #
1580
1303
  # See StateMachines::Transition for more information about the
1581
1304
  # attributes available on the transition.
1582
- #
1305
+ #
1583
1306
  # == Usage with delegates
1584
- #
1307
+ #
1585
1308
  # As noted above, state_machine uses the callback method's argument list
1586
1309
  # arity to determine whether to include the transition in the method call.
1587
1310
  # If you're using delegates, such as those defined in ActiveSupport or
1588
1311
  # Forwardable, the actual arity of the delegated method gets masked. This
1589
1312
  # means that callbacks which reference delegates will always get passed the
1590
1313
  # transition as an argument. For example:
1591
- #
1314
+ #
1592
1315
  # class Vehicle
1593
1316
  # extend Forwardable
1594
1317
  # delegate :refresh => :dashboard
1595
- #
1318
+ #
1596
1319
  # state_machine do
1597
1320
  # before_transition :refresh
1598
1321
  # ...
1599
1322
  # end
1600
- #
1323
+ #
1601
1324
  # def dashboard
1602
1325
  # @dashboard ||= Dashboard.new
1603
1326
  # end
1604
1327
  # end
1605
- #
1328
+ #
1606
1329
  # class Dashboard
1607
1330
  # def refresh(transition)
1608
1331
  # # ...
1609
1332
  # end
1610
1333
  # end
1611
- #
1334
+ #
1612
1335
  # In the above example, <tt>Dashboard#refresh</tt> *must* defined a
1613
1336
  # +transition+ argument. Otherwise, an +ArgumentError+ exception will get
1614
1337
  # raised. The only way around this is to avoid the use of delegates and
1615
1338
  # manually define the delegate method so that the correct arity is used.
1616
- #
1339
+ #
1617
1340
  # == Examples
1618
- #
1341
+ #
1619
1342
  # Below is an example of a class with one state machine and various types
1620
1343
  # of +before+ transitions defined for it:
1621
- #
1344
+ #
1622
1345
  # class Vehicle
1623
1346
  # state_machine do
1624
1347
  # # Before all transitions
1625
1348
  # before_transition :update_dashboard
1626
- #
1349
+ #
1627
1350
  # # Before specific transition:
1628
1351
  # before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
1629
- #
1352
+ #
1630
1353
  # # With conditional callback:
1631
1354
  # before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
1632
- #
1355
+ #
1633
1356
  # # Using helpers:
1634
1357
  # before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
1635
1358
  # ...
1636
1359
  # end
1637
1360
  # end
1638
- #
1361
+ #
1639
1362
  # As can be seen, any number of transitions can be created using various
1640
1363
  # combinations of configuration options.
1641
- def before_transition(*args, &block)
1642
- options = (args.last.is_a?(Hash) ? args.pop : {})
1643
- options[:do] = args if args.any?
1644
- add_callback(:before, options, &block)
1364
+ def before_transition(*args, **options, &)
1365
+ # Extract legacy positional arguments and merge with keyword options
1366
+ parsed_options = parse_callback_arguments(args, options)
1367
+
1368
+ # Only validate callback-specific options, not state transition requirements
1369
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
1370
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
1371
+
1372
+ add_callback(:before, parsed_options, &)
1645
1373
  end
1646
1374
 
1647
1375
  # Creates a callback that will be invoked *after* a transition is
1648
1376
  # performed so long as the given requirements match the transition.
1649
- #
1377
+ #
1650
1378
  # See +before_transition+ for a description of the possible configurations
1651
1379
  # for defining callbacks.
1652
- def after_transition(*args, &block)
1653
- options = (args.last.is_a?(Hash) ? args.pop : {})
1654
- options[:do] = args if args.any?
1655
- add_callback(:after, options, &block)
1380
+ def after_transition(*args, **options, &)
1381
+ # Extract legacy positional arguments and merge with keyword options
1382
+ parsed_options = parse_callback_arguments(args, options)
1383
+
1384
+ # Only validate callback-specific options, not state transition requirements
1385
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
1386
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
1387
+
1388
+ add_callback(:after, parsed_options, &)
1656
1389
  end
1657
1390
 
1658
1391
  # Creates a callback that will be invoked *around* a transition so long as
1659
1392
  # the given requirements match the transition.
1660
- #
1393
+ #
1661
1394
  # == The callback
1662
- #
1395
+ #
1663
1396
  # Around callbacks wrap transitions, executing code both before and after.
1664
1397
  # These callbacks are defined in the exact same manner as before / after
1665
1398
  # callbacks with the exception that the transition must be yielded to in
1666
1399
  # order to finish running it.
1667
- #
1400
+ #
1668
1401
  # If defining +around+ callbacks using blocks, you must yield within the
1669
1402
  # transition by directly calling the block (since yielding is not allowed
1670
1403
  # within blocks).
1671
- #
1404
+ #
1672
1405
  # For example,
1673
- #
1406
+ #
1674
1407
  # class Vehicle
1675
1408
  # state_machine do
1676
1409
  # around_transition do |block|
1677
1410
  # Benchmark.measure { block.call }
1678
1411
  # end
1679
- #
1412
+ #
1680
1413
  # around_transition do |vehicle, block|
1681
1414
  # logger.info "vehicle was #{state}..."
1682
1415
  # block.call
1683
1416
  # logger.info "...and is now #{state}"
1684
1417
  # end
1685
- #
1418
+ #
1686
1419
  # around_transition do |vehicle, transition, block|
1687
1420
  # logger.info "before #{transition.event}: #{vehicle.state}"
1688
1421
  # block.call
@@ -1690,73 +1423,78 @@ module StateMachines
1690
1423
  # end
1691
1424
  # end
1692
1425
  # end
1693
- #
1426
+ #
1694
1427
  # Notice that referencing the block is similar to doing so within an
1695
1428
  # actual method definition in that it is always the last argument.
1696
- #
1429
+ #
1697
1430
  # On the other hand, if you're defining +around+ callbacks using method
1698
1431
  # references, you can yield like normal:
1699
- #
1432
+ #
1700
1433
  # class Vehicle
1701
1434
  # state_machine do
1702
1435
  # around_transition :benchmark
1703
1436
  # ...
1704
1437
  # end
1705
- #
1438
+ #
1706
1439
  # def benchmark
1707
1440
  # Benchmark.measure { yield }
1708
1441
  # end
1709
1442
  # end
1710
- #
1443
+ #
1711
1444
  # See +before_transition+ for a description of the possible configurations
1712
1445
  # for defining callbacks.
1713
- def around_transition(*args, &block)
1714
- options = (args.last.is_a?(Hash) ? args.pop : {})
1715
- options[:do] = args if args.any?
1716
- add_callback(:around, options, &block)
1446
+ def around_transition(*args, **options, &)
1447
+ # Extract legacy positional arguments and merge with keyword options
1448
+ parsed_options = parse_callback_arguments(args, options)
1449
+
1450
+ # Only validate callback-specific options, not state transition requirements
1451
+ callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
1452
+ StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
1453
+
1454
+ add_callback(:around, parsed_options, &)
1717
1455
  end
1718
1456
 
1719
1457
  # Creates a callback that will be invoked *after* a transition failures to
1720
1458
  # be performed so long as the given requirements match the transition.
1721
- #
1459
+ #
1722
1460
  # See +before_transition+ for a description of the possible configurations
1723
1461
  # for defining callbacks. *Note* however that you cannot define the state
1724
1462
  # requirements in these callbacks. You may only define event requirements.
1725
- #
1463
+ #
1726
1464
  # = The callback
1727
- #
1465
+ #
1728
1466
  # Failure callbacks get invoked whenever an event fails to execute. This
1729
1467
  # can happen when no transition is available, a +before+ callback halts
1730
1468
  # execution, or the action associated with this machine fails to succeed.
1731
1469
  # In any of these cases, any failure callback that matches the attempted
1732
1470
  # transition will be run.
1733
- #
1471
+ #
1734
1472
  # For example,
1735
- #
1473
+ #
1736
1474
  # class Vehicle
1737
1475
  # state_machine do
1738
1476
  # after_failure do |vehicle, transition|
1739
1477
  # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
1740
1478
  # end
1741
- #
1479
+ #
1742
1480
  # after_failure :on => :ignite, :do => :log_ignition_failure
1743
- #
1481
+ #
1744
1482
  # ...
1745
1483
  # end
1746
1484
  # end
1747
- def after_failure(*args, &block)
1748
- options = (args.last.is_a?(Hash) ? args.pop : {})
1749
- options[:do] = args if args.any?
1750
- options.assert_valid_keys(:on, :do, :if, :unless)
1485
+ def after_failure(*args, **options, &)
1486
+ # Extract legacy positional arguments and merge with keyword options
1487
+ parsed_options = parse_callback_arguments(args, options)
1488
+ StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless)
1751
1489
 
1752
- add_callback(:failure, options, &block)
1490
+ add_callback(:failure, parsed_options, &)
1753
1491
  end
1754
1492
 
1755
1493
  # Generates a list of the possible transition sequences that can be run on
1756
1494
  # the given object. These paths can reveal all of the possible states and
1757
1495
  # events that can be encountered in the object's state machine based on the
1758
1496
  # object's current state.
1759
- #
1497
+ #
1760
1498
  # Configuration options:
1761
1499
  # * +from+ - The initial state to start all paths from. By default, this
1762
1500
  # is the object's current state.
@@ -1767,31 +1505,31 @@ module StateMachines
1767
1505
  # state (if specified) is reached. If this is enabled, then paths can
1768
1506
  # continue even after reaching the target state; they will stop when
1769
1507
  # reaching the target state a second time.
1770
- #
1508
+ #
1771
1509
  # *Note* that the object is never modified when the list of paths is
1772
1510
  # generated.
1773
- #
1511
+ #
1774
1512
  # == Examples
1775
- #
1513
+ #
1776
1514
  # class Vehicle
1777
1515
  # state_machine :initial => :parked do
1778
1516
  # event :ignite do
1779
1517
  # transition :parked => :idling
1780
1518
  # end
1781
- #
1519
+ #
1782
1520
  # event :shift_up do
1783
1521
  # transition :idling => :first_gear, :first_gear => :second_gear
1784
1522
  # end
1785
- #
1523
+ #
1786
1524
  # event :shift_down do
1787
1525
  # transition :second_gear => :first_gear, :first_gear => :idling
1788
1526
  # end
1789
1527
  # end
1790
1528
  # end
1791
- #
1529
+ #
1792
1530
  # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
1793
1531
  # vehicle.state # => "parked"
1794
- #
1532
+ #
1795
1533
  # vehicle.state_paths
1796
1534
  # # => [
1797
1535
  # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
@@ -1799,37 +1537,33 @@ module StateMachines
1799
1537
  # # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
1800
1538
  # # #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
1801
1539
  # # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
1802
- # #
1540
+ # #
1803
1541
  # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1804
1542
  # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1805
1543
  # # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
1806
1544
  # # ]
1807
- #
1545
+ #
1808
1546
  # vehicle.state_paths(:from => :parked, :to => :second_gear)
1809
1547
  # # => [
1810
1548
  # # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
1811
1549
  # # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
1812
1550
  # # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
1813
1551
  # # ]
1814
- #
1552
+ #
1815
1553
  # In addition to getting the possible paths that can be accessed, you can
1816
1554
  # also get summary information about the states / events that can be
1817
1555
  # accessed at some point along one of the paths. For example:
1818
- #
1556
+ #
1819
1557
  # # Get the list of states that can be accessed from the current state
1820
1558
  # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
1821
- #
1559
+ #
1822
1560
  # # Get the list of events that can be accessed from the current state
1823
1561
  # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
1824
- def paths_for(object, requirements = {})
1825
- PathCollection.new(object, self, requirements)
1826
- end
1827
1562
 
1828
1563
  # Marks the given object as invalid with the given message.
1829
- #
1564
+ #
1830
1565
  # By default, this is a no-op.
1831
- def invalidate(_object, _attribute, _message, _values = [])
1832
- end
1566
+ def invalidate(_object, _attribute, _message, _values = []); end
1833
1567
 
1834
1568
  # Gets a description of the errors for the given object. This is used to
1835
1569
  # provide more detailed information when an InvalidTransition exception is
@@ -1839,328 +1573,63 @@ module StateMachines
1839
1573
  end
1840
1574
 
1841
1575
  # Resets any errors previously added when invalidating the given object.
1842
- #
1576
+ #
1843
1577
  # By default, this is a no-op.
1844
- def reset(_object)
1845
- end
1578
+ def reset(_object); end
1846
1579
 
1847
1580
  # Generates the message to use when invalidating the given object after
1848
1581
  # failing to transition on a specific event
1849
1582
  def generate_message(name, values = [])
1850
- message = (@messages[name] || self.class.default_messages[name])
1583
+ message = @messages[name] || self.class.default_messages[name]
1851
1584
 
1852
1585
  # Check whether there are actually any values to interpolate to avoid
1853
1586
  # any warnings
1854
1587
  if message.scan(/%./).any? { |match| match != '%%' }
1855
- message % values.map { |value| value.last }
1588
+ message % values.map(&:last)
1856
1589
  else
1857
1590
  message
1858
1591
  end
1859
1592
  end
1860
1593
 
1861
1594
  # Runs a transaction, rolling back any changes if the yielded block fails.
1862
- #
1595
+ #
1863
1596
  # This is only applicable to integrations that involve databases. By
1864
1597
  # default, this will not run any transactions since the changes aren't
1865
1598
  # taking place within the context of a database.
1866
- def within_transaction(object)
1599
+ def within_transaction(object, &)
1867
1600
  if use_transactions
1868
- transaction(object) { yield }
1601
+ transaction(object, &)
1869
1602
  else
1870
1603
  yield
1871
1604
  end
1872
1605
  end
1873
1606
 
1607
+ def renderer
1608
+ self.class.renderer
1609
+ end
1874
1610
 
1875
- def draw(*)
1876
- fail NotImplementedError
1611
+ def draw(**)
1612
+ renderer.draw_machine(self, **)
1877
1613
  end
1878
1614
 
1879
1615
  # Determines whether an action hook was defined for firing attribute-based
1880
1616
  # event transitions when the configured action gets called.
1881
1617
  def action_hook?(self_only = false)
1882
- @action_hook_defined || !self_only && owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self && machine.action_hook?(true) }
1618
+ @action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) })
1883
1619
  end
1884
1620
 
1885
1621
  protected
1886
- # Runs additional initialization hooks. By default, this is a no-op.
1887
- def after_initialize
1888
- end
1889
-
1890
- # Looks up other machines that have been defined in the owner class and
1891
- # are targeting the same attribute as this machine. When accessing
1892
- # sibling machines, they will be automatically copied for the current
1893
- # class if they haven't been already. This ensures that any configuration
1894
- # changes made to the sibling machines only affect this class and not any
1895
- # base class that may have originally defined the machine.
1896
- def sibling_machines
1897
- owner_class.state_machines.inject([]) do |machines, (name, machine)|
1898
- if machine.attribute == attribute && machine != self
1899
- machines << (owner_class.state_machine(name) {})
1900
- end
1901
- machines
1902
- end
1903
- end
1904
-
1905
- # Determines if the machine's attribute needs to be initialized. This
1906
- # will only be true if the machine's attribute is blank.
1907
- def initialize_state?(object)
1908
- value = read(object, :state)
1909
- (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
1910
- end
1911
-
1912
- # Adds helper methods for interacting with the state machine, including
1913
- # for states, events, and transitions
1914
- def define_helpers
1915
- define_state_accessor
1916
- define_state_predicate
1917
- define_event_helpers
1918
- define_path_helpers
1919
- define_action_helpers if define_action_helpers?
1920
- define_name_helpers
1921
- end
1922
-
1923
- # Defines the initial values for state machine attributes. Static values
1924
- # are set prior to the original initialize method and dynamic values are
1925
- # set *after* the initialize method in case it is dependent on it.
1926
- def define_state_initializer
1927
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1928
- def initialize(*)
1929
- self.class.state_machines.initialize_states(self) { super }
1930
- end
1931
- end_eval
1932
- end
1933
-
1934
- # Adds reader/writer methods for accessing the state attribute
1935
- def define_state_accessor
1936
- attribute = self.attribute
1937
-
1938
- @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
1939
- @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
1940
- end
1941
-
1942
- # Adds predicate method to the owner class for determining the name of the
1943
- # current state
1944
- def define_state_predicate
1945
- call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
1946
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1947
- def #{name}?(*args)
1948
- args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
1949
- end
1950
- end_eval
1951
- end
1952
-
1953
- # Adds helper methods for getting information about this state machine's
1954
- # events
1955
- def define_event_helpers
1956
- # Gets the events that are allowed to fire on the current object
1957
- define_helper(:instance, attribute(:events)) do |machine, object, *args|
1958
- machine.events.valid_for(object, *args).map { |event| event.name }
1959
- end
1960
-
1961
- # Gets the next possible transitions that can be run on the current
1962
- # object
1963
- define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
1964
- machine.events.transitions_for(object, *args)
1965
- end
1966
-
1967
- # Fire an arbitrary event for this machine
1968
- define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
1969
- machine.events.fetch(event).fire(object, *args)
1970
- end
1971
-
1972
- # Add helpers for tracking the event / transition to invoke when the
1973
- # action is called
1974
- if action
1975
- event_attribute = attribute(:event)
1976
- define_helper(:instance, event_attribute) do |machine, object|
1977
- # Interpret non-blank events as present
1978
- event = machine.read(object, :event, true)
1979
- event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
1980
- end
1981
1622
 
1982
- # A roundabout way of writing the attribute is used here so that
1983
- # integrations can hook into this modification
1984
- define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
1985
- machine.write(object, :event, value, true)
1986
- end
1987
-
1988
- event_transition_attribute = attribute(:event_transition)
1989
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
1990
- protected; attr_accessor #{event_transition_attribute.inspect}
1991
- end_eval
1992
- end
1993
- end
1994
-
1995
- # Adds helper methods for getting information about this state machine's
1996
- # available transition paths
1997
- def define_path_helpers
1998
- # Gets the paths of transitions available to the current object
1999
- define_helper(:instance, attribute(:paths)) do |machine, object, *args|
2000
- machine.paths_for(object, *args)
2001
- end
2002
- end
2003
-
2004
- # Determines whether action helpers should be defined for this machine.
2005
- # This is only true if there is an action configured and no other machines
2006
- # have process this same configuration already.
2007
- def define_action_helpers?
2008
- action && !owner_class.state_machines.any? { |name, machine| machine.action == action && machine != self }
2009
- end
2010
-
2011
- # Adds helper methods for automatically firing events when an action
2012
- # is invoked
2013
- def define_action_helpers
2014
- if action_hook
2015
- @action_hook_defined = true
2016
- define_action_hook
2017
- end
2018
- end
2019
-
2020
- # Hooks directly into actions by defining the same method in an included
2021
- # module. As a result, when the action gets invoked, any state events
2022
- # defined for the object will get run. Method visibility is preserved.
2023
- def define_action_hook
2024
- action_hook = self.action_hook
2025
- action = self.action
2026
- private_action_hook = owner_class.private_method_defined?(action_hook)
2027
-
2028
- # Only define helper if it hasn't
2029
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
2030
- def #{action_hook}(*)
2031
- self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
2032
- end
2033
-
2034
- private #{action_hook.inspect} if #{private_action_hook}
2035
- end_eval
2036
- end
2037
-
2038
- # The method to hook into for triggering transitions when invoked. By
2039
- # default, this is the action configured for the machine.
2040
- #
2041
- # Since the default hook technique relies on module inheritance, the
2042
- # action must be defined in an ancestor of the owner classs in order for
2043
- # it to be the action hook.
2044
- def action_hook
2045
- action && owner_class_ancestor_has_method?(:instance, action) ? action : nil
2046
- end
1623
+ # Runs additional initialization hooks. By default, this is a no-op.
1624
+ def after_initialize; end
2047
1625
 
2048
1626
  # Determines whether there's already a helper method defined within the
2049
1627
  # given scope. This is true only if one of the owner's ancestors defines
2050
1628
  # the method and is further along in the ancestor chain than this
2051
1629
  # machine's helper module.
2052
- def owner_class_ancestor_has_method?(scope, method)
2053
- return false unless owner_class_has_method?(scope, method)
2054
-
2055
- superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
2056
-
2057
- if scope == :class
2058
- current = owner_class.singleton_class
2059
- superclass = superclasses.first
2060
- else
2061
- current = owner_class
2062
- superclass = owner_class.superclass
2063
- end
2064
-
2065
- # Generate the list of modules that *only* occur in the owner class, but
2066
- # were included *prior* to the helper modules, in addition to the
2067
- # superclasses
2068
- ancestors = current.ancestors - superclass.ancestors + superclasses
2069
- ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
2070
-
2071
- # Search for for the first ancestor that defined this method
2072
- ancestors.detect do |ancestor|
2073
- ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
2074
- ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
2075
- end
2076
- end
2077
-
2078
- def owner_class_has_method?(scope, method)
2079
- target = scope == :class ? owner_class.singleton_class : owner_class
2080
- target.method_defined?(method) || target.private_method_defined?(method)
2081
- end
2082
-
2083
- # Adds helper methods for accessing naming information about states and
2084
- # events on the owner class
2085
- def define_name_helpers
2086
- # Gets the humanized version of a state
2087
- define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
2088
- machine.states.fetch(state).human_name(klass)
2089
- end
2090
-
2091
- # Gets the humanized version of an event
2092
- define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
2093
- machine.events.fetch(event).human_name(klass)
2094
- end
2095
-
2096
- # Gets the state name for the current value
2097
- define_helper(:instance, attribute(:name)) do |machine, object|
2098
- machine.states.match!(object).name
2099
- end
2100
-
2101
- # Gets the human state name for the current value
2102
- define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
2103
- machine.states.match!(object).human_name(object.class)
2104
- end
2105
- end
2106
-
2107
- # Defines the with/without scope helpers for this attribute. Both the
2108
- # singular and plural versions of the attribute are defined for each
2109
- # scope helper. A custom plural can be specified if it cannot be
2110
- # automatically determined by either calling +pluralize+ on the attribute
2111
- # name or adding an "s" to the end of the name.
2112
- def define_scopes(custom_plural = nil)
2113
- plural = custom_plural || pluralize(name)
2114
-
2115
- [:with, :without].each do |kind|
2116
- [name, plural].map { |s| s.to_s }.uniq.each do |suffix|
2117
- method = "#{kind}_#{suffix}"
2118
-
2119
- if scope = send("create_#{kind}_scope", method)
2120
- # Converts state names to their corresponding values so that they
2121
- # can be looked up properly
2122
- define_helper(:class, method) do |machine, klass, *states|
2123
- run_scope(scope, machine, klass, states)
2124
- end
2125
- end
2126
- end
2127
- end
2128
- end
2129
-
2130
- # Generates the results for the given scope based on one or more states to
2131
- # filter by
2132
- def run_scope(scope, machine, klass, states)
2133
- values = states.flatten.map { |state| machine.states.fetch(state).value }
2134
- scope.call(klass, values)
2135
- end
2136
-
2137
- # Pluralizes the given word using #pluralize (if available) or simply
2138
- # adding an "s" to the end of the word
2139
- def pluralize(word)
2140
- word = word.to_s
2141
- if word.respond_to?(:pluralize)
2142
- word.pluralize
2143
- else
2144
- "#{name}s"
2145
- end
2146
- end
2147
-
2148
- # Creates a scope for finding objects *with* a particular value or values
2149
- # for the attribute.
2150
- #
2151
- # By default, this is a no-op.
2152
- def create_with_scope(name)
2153
- end
2154
-
2155
- # Creates a scope for finding objects *without* a particular value or
2156
- # values for the attribute.
2157
- #
2158
- # By default, this is a no-op.
2159
- def create_without_scope(name)
2160
- end
2161
1630
 
2162
1631
  # Always yields
2163
- def transaction(object)
1632
+ def transaction(_object)
2164
1633
  yield
2165
1634
  end
2166
1635
 
@@ -2175,60 +1644,5 @@ module StateMachines
2175
1644
  def owner_class_attribute_default_matches?(state)
2176
1645
  state.matches?(owner_class_attribute_default)
2177
1646
  end
2178
-
2179
- # Updates this machine based on the configuration of other machines in the
2180
- # owner class that share the same target attribute.
2181
- def add_sibling_machine_configs
2182
- # Add existing states
2183
- sibling_machines.each do |machine|
2184
- machine.states.each { |state| states << state unless states[state.name] }
2185
- end
2186
- end
2187
-
2188
- # Adds a new transition callback of the given type.
2189
- def add_callback(type, options, &block)
2190
- callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
2191
- add_states(callback.known_states)
2192
- callback
2193
- end
2194
-
2195
- # Tracks the given set of states in the list of all known states for
2196
- # this machine
2197
- def add_states(new_states)
2198
- new_states.map do |new_state|
2199
- # Check for other states that use a different class type for their name.
2200
- # This typically prevents string / symbol misuse.
2201
- if new_state && conflict = states.detect { |state| state.name && state.name.class != new_state.class }
2202
- raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
2203
- end
2204
-
2205
- unless state = states[new_state]
2206
- states << state = State.new(self, new_state)
2207
-
2208
- # Copy states over to sibling machines
2209
- sibling_machines.each { |machine| machine.states << state }
2210
- end
2211
-
2212
- state
2213
- end
2214
- end
2215
-
2216
- # Tracks the given set of events in the list of all known events for
2217
- # this machine
2218
- def add_events(new_events)
2219
- new_events.map do |new_event|
2220
- # Check for other states that use a different class type for their name.
2221
- # This typically prevents string / symbol misuse.
2222
- if conflict = events.detect { |event| event.name.class != new_event.class }
2223
- raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
2224
- end
2225
-
2226
- unless event = events[new_event]
2227
- events << event = Event.new(self, new_event)
2228
- end
2229
-
2230
- event
2231
- end
2232
- end
2233
1647
  end
2234
1648
  end