state_machines 0.5.0 → 0.100.4
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.
- checksums.yaml +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +443 -22
- data/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
- data/lib/state_machines/async_mode/async_events.rb +282 -0
- data/lib/state_machines/async_mode/async_machine.rb +60 -0
- data/lib/state_machines/async_mode/async_transition_collection.rb +141 -0
- data/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
- data/lib/state_machines/async_mode.rb +64 -0
- data/lib/state_machines/branch.rb +148 -85
- data/lib/state_machines/callback.rb +35 -32
- data/lib/state_machines/core.rb +3 -3
- data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
- data/lib/state_machines/core_ext.rb +2 -0
- data/lib/state_machines/error.rb +7 -4
- data/lib/state_machines/eval_helpers.rb +195 -39
- data/lib/state_machines/event.rb +77 -58
- data/lib/state_machines/event_collection.rb +48 -38
- data/lib/state_machines/extensions.rb +6 -4
- data/lib/state_machines/helper_module.rb +4 -2
- data/lib/state_machines/integrations/base.rb +9 -1
- data/lib/state_machines/integrations.rb +19 -20
- data/lib/state_machines/machine/action_hooks.rb +53 -0
- data/lib/state_machines/machine/async_extensions.rb +88 -0
- data/lib/state_machines/machine/callbacks.rb +59 -0
- data/lib/state_machines/machine/class_methods.rb +95 -0
- data/lib/state_machines/machine/configuration.rb +136 -0
- data/lib/state_machines/machine/event_methods.rb +59 -0
- data/lib/state_machines/machine/helper_generators.rb +125 -0
- data/lib/state_machines/machine/integration.rb +70 -0
- data/lib/state_machines/machine/parsing.rb +77 -0
- data/lib/state_machines/machine/rendering.rb +17 -0
- data/lib/state_machines/machine/scoping.rb +44 -0
- data/lib/state_machines/machine/state_methods.rb +101 -0
- data/lib/state_machines/machine/utilities.rb +85 -0
- data/lib/state_machines/machine/validation.rb +39 -0
- data/lib/state_machines/machine.rb +423 -1011
- data/lib/state_machines/machine_collection.rb +28 -19
- data/lib/state_machines/macro_methods.rb +104 -102
- data/lib/state_machines/matcher.rb +31 -28
- data/lib/state_machines/matcher_helpers.rb +14 -12
- data/lib/state_machines/node_collection.rb +36 -29
- data/lib/state_machines/options_validator.rb +72 -0
- data/lib/state_machines/path.rb +60 -57
- data/lib/state_machines/path_collection.rb +39 -36
- data/lib/state_machines/state.rb +84 -47
- data/lib/state_machines/state_collection.rb +22 -19
- data/lib/state_machines/state_context.rb +40 -39
- data/lib/state_machines/stdio_renderer.rb +74 -0
- data/lib/state_machines/syntax_validator.rb +57 -0
- data/lib/state_machines/test_helper.rb +895 -0
- data/lib/state_machines/transition.rb +386 -204
- data/lib/state_machines/transition_collection.rb +208 -171
- data/lib/state_machines/version.rb +3 -1
- data/lib/state_machines.rb +4 -1
- metadata +39 -446
- data/.gitignore +0 -21
- data/.rspec +0 -3
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -16
- data/Changelog.md +0 -22
- data/Contributors.md +0 -39
- data/Gemfile +0 -8
- data/Rakefile +0 -12
- data/Testing.md +0 -0
- data/lib/state_machines/assertions.rb +0 -40
- data/state_machines.gemspec +0 -22
- data/test/files/integrations/event_on_failure_integration.rb +0 -10
- data/test/files/integrations/vehicle.rb +0 -7
- data/test/files/models/auto_shop.rb +0 -31
- data/test/files/models/car.rb +0 -21
- data/test/files/models/driver.rb +0 -13
- data/test/files/models/model_base.rb +0 -6
- data/test/files/models/motorcycle.rb +0 -16
- data/test/files/models/traffic_light.rb +0 -47
- data/test/files/models/vehicle.rb +0 -127
- data/test/files/node.rb +0 -5
- data/test/files/switch.rb +0 -15
- data/test/functional/auto_shop_available_test.rb +0 -20
- data/test/functional/auto_shop_busy_test.rb +0 -25
- data/test/functional/car_backing_up_test.rb +0 -45
- data/test/functional/car_test.rb +0 -49
- data/test/functional/driver_default_nonstandard_test.rb +0 -13
- data/test/functional/motorcycle_test.rb +0 -52
- data/test/functional/traffic_light_caution_test.rb +0 -17
- data/test/functional/traffic_light_proceed_test.rb +0 -17
- data/test/functional/traffic_light_stop_test.rb +0 -26
- data/test/functional/vehicle_first_gear_test.rb +0 -42
- data/test/functional/vehicle_idling_test.rb +0 -59
- data/test/functional/vehicle_locked_test.rb +0 -29
- data/test/functional/vehicle_parked_test.rb +0 -53
- data/test/functional/vehicle_repaired_test.rb +0 -20
- data/test/functional/vehicle_second_gear_test.rb +0 -42
- data/test/functional/vehicle_stalled_test.rb +0 -65
- data/test/functional/vehicle_test.rb +0 -20
- data/test/functional/vehicle_third_gear_test.rb +0 -42
- data/test/functional/vehicle_unsaved_test.rb +0 -181
- data/test/functional/vehicle_with_event_attributes_test.rb +0 -30
- data/test/functional/vehicle_with_parallel_events_test.rb +0 -36
- data/test/test_helper.rb +0 -15
- data/test/unit/assertions/assert_exclusive_keys_test.rb +0 -22
- data/test/unit/assertions/assert_valid_key_test.rb +0 -12
- data/test/unit/branch/branch_test.rb +0 -28
- data/test/unit/branch/branch_with_conflicting_conditionals_test.rb +0 -27
- data/test/unit/branch/branch_with_conflicting_from_requirements_test.rb +0 -8
- data/test/unit/branch/branch_with_conflicting_on_requirements_test.rb +0 -8
- data/test/unit/branch/branch_with_conflicting_to_requirements_test.rb +0 -8
- data/test/unit/branch/branch_with_different_requirements_test.rb +0 -41
- data/test/unit/branch/branch_with_except_from_matcher_requirement_test.rb +0 -8
- data/test/unit/branch/branch_with_except_from_requirement_test.rb +0 -36
- data/test/unit/branch/branch_with_except_on_matcher_requirement_test.rb +0 -8
- data/test/unit/branch/branch_with_except_on_requirement_test.rb +0 -36
- data/test/unit/branch/branch_with_except_to_matcher_requirement_test.rb +0 -8
- data/test/unit/branch/branch_with_except_to_requirement_test.rb +0 -36
- data/test/unit/branch/branch_with_from_matcher_requirement_test.rb +0 -20
- data/test/unit/branch/branch_with_from_requirement_test.rb +0 -45
- data/test/unit/branch/branch_with_if_conditional_test.rb +0 -27
- data/test/unit/branch/branch_with_implicit_and_explicit_requirements_test.rb +0 -23
- data/test/unit/branch/branch_with_implicit_from_requirement_matcher_test.rb +0 -20
- data/test/unit/branch/branch_with_implicit_requirement_test.rb +0 -20
- data/test/unit/branch/branch_with_implicit_to_requirement_matcher_test.rb +0 -16
- data/test/unit/branch/branch_with_multiple_except_from_requirements_test.rb +0 -20
- data/test/unit/branch/branch_with_multiple_except_on_requirements_test.rb +0 -16
- data/test/unit/branch/branch_with_multiple_except_to_requirements_test.rb +0 -20
- data/test/unit/branch/branch_with_multiple_from_requirements_test.rb +0 -16
- data/test/unit/branch/branch_with_multiple_if_conditionals_test.rb +0 -20
- data/test/unit/branch/branch_with_multiple_implicit_requirements_test.rb +0 -53
- data/test/unit/branch/branch_with_multiple_to_requirements_test.rb +0 -20
- data/test/unit/branch/branch_with_multiple_unless_conditionals_test.rb +0 -20
- data/test/unit/branch/branch_with_nil_requirements_test.rb +0 -28
- data/test/unit/branch/branch_with_no_requirements_test.rb +0 -36
- data/test/unit/branch/branch_with_on_matcher_requirement_test.rb +0 -16
- data/test/unit/branch/branch_with_on_requirement_test.rb +0 -45
- data/test/unit/branch/branch_with_to_matcher_requirement_test.rb +0 -20
- data/test/unit/branch/branch_with_to_requirement_test.rb +0 -45
- data/test/unit/branch/branch_with_unless_conditional_test.rb +0 -27
- data/test/unit/branch/branch_without_guards_test.rb +0 -27
- data/test/unit/callback/callback_by_default_test.rb +0 -25
- data/test/unit/callback/callback_test.rb +0 -53
- data/test/unit/callback/callback_with_application_bound_object_test.rb +0 -23
- data/test/unit/callback/callback_with_application_terminator_test.rb +0 -24
- data/test/unit/callback/callback_with_arguments_test.rb +0 -14
- data/test/unit/callback/callback_with_around_type_and_arguments_test.rb +0 -25
- data/test/unit/callback/callback_with_around_type_and_block_test.rb +0 -44
- data/test/unit/callback/callback_with_around_type_and_bound_method_test.rb +0 -23
- data/test/unit/callback/callback_with_around_type_and_multiple_methods_test.rb +0 -93
- data/test/unit/callback/callback_with_around_type_and_terminator_test.rb +0 -17
- data/test/unit/callback/callback_with_block_test.rb +0 -20
- data/test/unit/callback/callback_with_bound_method_and_arguments_test.rb +0 -28
- data/test/unit/callback/callback_with_bound_method_test.rb +0 -35
- data/test/unit/callback/callback_with_do_method_test.rb +0 -18
- data/test/unit/callback/callback_with_explicit_requirements_test.rb +0 -32
- data/test/unit/callback/callback_with_if_condition_test.rb +0 -17
- data/test/unit/callback/callback_with_implicit_requirements_test.rb +0 -32
- data/test/unit/callback/callback_with_method_argument_test.rb +0 -18
- data/test/unit/callback/callback_with_mixed_methods_test.rb +0 -31
- data/test/unit/callback/callback_with_multiple_bound_methods_test.rb +0 -21
- data/test/unit/callback/callback_with_multiple_do_methods_test.rb +0 -29
- data/test/unit/callback/callback_with_multiple_method_arguments_test.rb +0 -29
- data/test/unit/callback/callback_with_terminator_test.rb +0 -22
- data/test/unit/callback/callback_with_unbound_method_test.rb +0 -14
- data/test/unit/callback/callback_with_unless_condition_test.rb +0 -17
- data/test/unit/callback/callback_without_arguments_test.rb +0 -14
- data/test/unit/callback/callback_without_terminator_test.rb +0 -12
- data/test/unit/error/error_by_default_test.rb +0 -21
- data/test/unit/error/error_with_message_test.rb +0 -23
- data/test/unit/eval_helper/eval_helpers_base_test.rb +0 -8
- data/test/unit/eval_helper/eval_helpers_proc_block_and_explicit_arguments_test.rb +0 -14
- data/test/unit/eval_helper/eval_helpers_proc_block_and_implicit_arguments_test.rb +0 -14
- data/test/unit/eval_helper/eval_helpers_proc_test.rb +0 -13
- data/test/unit/eval_helper/eval_helpers_proc_with_arguments_test.rb +0 -13
- data/test/unit/eval_helper/eval_helpers_proc_with_block_test.rb +0 -13
- data/test/unit/eval_helper/eval_helpers_proc_with_block_without_arguments_test.rb +0 -18
- data/test/unit/eval_helper/eval_helpers_proc_with_block_without_object_test.rb +0 -14
- data/test/unit/eval_helper/eval_helpers_proc_without_arguments_test.rb +0 -19
- data/test/unit/eval_helper/eval_helpers_string_test.rb +0 -25
- data/test/unit/eval_helper/eval_helpers_string_with_block_test.rb +0 -12
- data/test/unit/eval_helper/eval_helpers_symbol_method_missing_test.rb +0 -20
- data/test/unit/eval_helper/eval_helpers_symbol_private_test.rb +0 -17
- data/test/unit/eval_helper/eval_helpers_symbol_protected_test.rb +0 -17
- data/test/unit/eval_helper/eval_helpers_symbol_tainted_method_test.rb +0 -18
- data/test/unit/eval_helper/eval_helpers_symbol_test.rb +0 -16
- data/test/unit/eval_helper/eval_helpers_symbol_with_arguments_and_block_test.rb +0 -16
- data/test/unit/eval_helper/eval_helpers_symbol_with_arguments_test.rb +0 -16
- data/test/unit/eval_helper/eval_helpers_symbol_with_block_test.rb +0 -16
- data/test/unit/eval_helper/eval_helpers_test.rb +0 -13
- data/test/unit/event/event_after_being_copied_test.rb +0 -17
- data/test/unit/event/event_by_default_test.rb +0 -60
- data/test/unit/event/event_context_test.rb +0 -16
- data/test/unit/event/event_on_failure_test.rb +0 -44
- data/test/unit/event/event_test.rb +0 -34
- data/test/unit/event/event_transitions_test.rb +0 -62
- data/test/unit/event/event_with_conflicting_helpers_after_definition_test.rb +0 -79
- data/test/unit/event/event_with_conflicting_helpers_before_definition_test.rb +0 -58
- data/test/unit/event/event_with_conflicting_machine_test.rb +0 -48
- data/test/unit/event/event_with_dynamic_human_name_test.rb +0 -26
- data/test/unit/event/event_with_human_name_test.rb +0 -13
- data/test/unit/event/event_with_invalid_current_state_test.rb +0 -30
- data/test/unit/event/event_with_machine_action_test.rb +0 -33
- data/test/unit/event/event_with_marshalling_test.rb +0 -47
- data/test/unit/event/event_with_matching_disabled_transitions_test.rb +0 -115
- data/test/unit/event/event_with_matching_enabled_transitions_test.rb +0 -75
- data/test/unit/event/event_with_multiple_transitions_test.rb +0 -61
- data/test/unit/event/event_with_namespace_test.rb +0 -34
- data/test/unit/event/event_with_transition_with_blacklisted_to_state_test.rb +0 -60
- data/test/unit/event/event_with_transition_with_loopback_state_test.rb +0 -36
- data/test/unit/event/event_with_transition_with_nil_to_state_test.rb +0 -36
- data/test/unit/event/event_with_transition_with_whitelisted_to_state_test.rb +0 -51
- data/test/unit/event/event_with_transition_without_to_state_test.rb +0 -36
- data/test/unit/event/event_with_transitions_test.rb +0 -32
- data/test/unit/event/event_without_matching_transitions_test.rb +0 -41
- data/test/unit/event/event_without_transitions_test.rb +0 -28
- data/test/unit/event/invalid_event_test.rb +0 -20
- data/test/unit/event_collection/event_collection_attribute_with_machine_action_test.rb +0 -62
- data/test/unit/event_collection/event_collection_attribute_with_namespaced_machine_test.rb +0 -36
- data/test/unit/event_collection/event_collection_by_default_test.rb +0 -26
- data/test/unit/event_collection/event_collection_test.rb +0 -39
- data/test/unit/event_collection/event_collection_with_custom_machine_attribute_test.rb +0 -31
- data/test/unit/event_collection/event_collection_with_events_with_transitions_test.rb +0 -76
- data/test/unit/event_collection/event_collection_with_multiple_events_test.rb +0 -27
- data/test/unit/event_collection/event_collection_with_validations_test.rb +0 -74
- data/test/unit/event_collection/event_collection_without_machine_action_test.rb +0 -18
- data/test/unit/event_collection/event_string_collection_test.rb +0 -31
- data/test/unit/helper_module_test.rb +0 -17
- data/test/unit/integrations/integration_finder_test.rb +0 -16
- data/test/unit/integrations/integration_matcher_test.rb +0 -29
- data/test/unit/invalid_transition/invalid_parallel_transition_test.rb +0 -18
- data/test/unit/invalid_transition/invalid_transition_test.rb +0 -47
- data/test/unit/invalid_transition/invalid_transition_with_integration_test.rb +0 -45
- data/test/unit/invalid_transition/invalid_transition_with_namespace_test.rb +0 -32
- data/test/unit/machine/machine_after_being_copied_test.rb +0 -62
- data/test/unit/machine/machine_after_changing_initial_state.rb +0 -28
- data/test/unit/machine/machine_after_changing_owner_class_test.rb +0 -31
- data/test/unit/machine/machine_by_default_test.rb +0 -160
- data/test/unit/machine/machine_finder_custom_options_test.rb +0 -17
- data/test/unit/machine/machine_finder_with_existing_machine_on_superclass_test.rb +0 -85
- data/test/unit/machine/machine_finder_with_existing_on_same_class_test.rb +0 -23
- data/test/unit/machine/machine_finder_without_existing_machine_test.rb +0 -25
- data/test/unit/machine/machine_persistence_test.rb +0 -52
- data/test/unit/machine/machine_state_initialization_test.rb +0 -56
- data/test/unit/machine/machine_test.rb +0 -30
- data/test/unit/machine/machine_with_action_already_overridden_test.rb +0 -23
- data/test/unit/machine/machine_with_action_defined_in_class_test.rb +0 -37
- data/test/unit/machine/machine_with_action_defined_in_included_module_test.rb +0 -46
- data/test/unit/machine/machine_with_action_defined_in_superclass_test.rb +0 -43
- data/test/unit/machine/machine_with_action_undefined_test.rb +0 -33
- data/test/unit/machine/machine_with_cached_state_test.rb +0 -20
- data/test/unit/machine/machine_with_class_helpers_test.rb +0 -179
- data/test/unit/machine/machine_with_conflicting_helpers_after_definition_test.rb +0 -244
- data/test/unit/machine/machine_with_conflicting_helpers_before_definition_test.rb +0 -175
- data/test/unit/machine/machine_with_custom_action_test.rb +0 -11
- data/test/unit/machine/machine_with_custom_attribute_test.rb +0 -103
- data/test/unit/machine/machine_with_custom_initialize_test.rb +0 -24
- data/test/unit/machine/machine_with_custom_integration_test.rb +0 -72
- data/test/unit/machine/machine_with_custom_invalidation_test.rb +0 -39
- data/test/unit/machine/machine_with_custom_name_test.rb +0 -57
- data/test/unit/machine/machine_with_custom_plural_test.rb +0 -52
- data/test/unit/machine/machine_with_dynamic_initial_state_test.rb +0 -65
- data/test/unit/machine/machine_with_event_matchers_test.rb +0 -41
- data/test/unit/machine/machine_with_events_test.rb +0 -52
- data/test/unit/machine/machine_with_events_with_custom_human_names_test.rb +0 -18
- data/test/unit/machine/machine_with_events_with_transitions_test.rb +0 -37
- data/test/unit/machine/machine_with_existing_event_test.rb +0 -17
- data/test/unit/machine/machine_with_existing_machines_on_owner_class_test.rb +0 -20
- data/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_class_test.rb +0 -71
- data/test/unit/machine/machine_with_existing_machines_with_same_attributes_on_owner_subclass_test.rb +0 -31
- data/test/unit/machine/machine_with_existing_state_test.rb +0 -27
- data/test/unit/machine/machine_with_failure_callbacks_test.rb +0 -48
- data/test/unit/machine/machine_with_helpers_test.rb +0 -14
- data/test/unit/machine/machine_with_initial_state_with_value_and_owner_default.rb +0 -25
- data/test/unit/machine/machine_with_initialize_and_super_test.rb +0 -17
- data/test/unit/machine/machine_with_initialize_arguments_and_block_test.rb +0 -31
- data/test/unit/machine/machine_with_initialize_without_super_test.rb +0 -17
- data/test/unit/machine/machine_with_instance_helpers_test.rb +0 -179
- data/test/unit/machine/machine_with_integration_test.rb +0 -72
- data/test/unit/machine/machine_with_multiple_events_test.rb +0 -32
- data/test/unit/machine/machine_with_namespace_test.rb +0 -48
- data/test/unit/machine/machine_with_nil_action_test.rb +0 -27
- data/test/unit/machine/machine_with_other_states.rb +0 -22
- data/test/unit/machine/machine_with_owner_subclass_test.rb +0 -18
- data/test/unit/machine/machine_with_paths_test.rb +0 -25
- data/test/unit/machine/machine_with_private_action_test.rb +0 -43
- data/test/unit/machine/machine_with_state_matchers_test.rb +0 -41
- data/test/unit/machine/machine_with_state_with_matchers_test.rb +0 -19
- data/test/unit/machine/machine_with_states_test.rb +0 -55
- data/test/unit/machine/machine_with_states_with_behaviors_test.rb +0 -23
- data/test/unit/machine/machine_with_states_with_custom_human_names_test.rb +0 -18
- data/test/unit/machine/machine_with_states_with_custom_values_test.rb +0 -21
- data/test/unit/machine/machine_with_states_with_runtime_dependencies_test.rb +0 -19
- data/test/unit/machine/machine_with_static_initial_state_test.rb +0 -49
- data/test/unit/machine/machine_with_superclass_conflicting_helpers_after_definition_test.rb +0 -36
- data/test/unit/machine/machine_with_transition_callbacks_test.rb +0 -144
- data/test/unit/machine/machine_with_transitions_test.rb +0 -87
- data/test/unit/machine/machine_without_initialization_test.rb +0 -31
- data/test/unit/machine/machine_without_initialize_test.rb +0 -14
- data/test/unit/machine/machine_without_integration_test.rb +0 -31
- data/test/unit/machine_collection/machine_collection_by_default_test.rb +0 -11
- data/test/unit/machine_collection/machine_collection_fire_test.rb +0 -80
- data/test/unit/machine_collection/machine_collection_fire_with_transactions_test.rb +0 -54
- data/test/unit/machine_collection/machine_collection_fire_with_validations_test.rb +0 -76
- data/test/unit/machine_collection/machine_collection_state_initialization_test.rb +0 -111
- data/test/unit/machine_collection/machine_collection_transitions_with_blank_events_test.rb +0 -25
- data/test/unit/machine_collection/machine_collection_transitions_with_custom_options_test.rb +0 -20
- data/test/unit/machine_collection/machine_collection_transitions_with_different_actions_test.rb +0 -26
- data/test/unit/machine_collection/machine_collection_transitions_with_exisiting_transitions_test.rb +0 -25
- data/test/unit/machine_collection/machine_collection_transitions_with_invalid_events_test.rb +0 -25
- data/test/unit/machine_collection/machine_collection_transitions_with_same_actions_test.rb +0 -31
- data/test/unit/machine_collection/machine_collection_transitions_with_transition_test.rb +0 -26
- data/test/unit/machine_collection/machine_collection_transitions_without_events_test.rb +0 -25
- data/test/unit/machine_collection/machine_collection_transitions_without_transition_test.rb +0 -27
- data/test/unit/matcher/all_matcher_test.rb +0 -29
- data/test/unit/matcher/blacklist_matcher_test.rb +0 -30
- data/test/unit/matcher/loopback_matcher_test.rb +0 -27
- data/test/unit/matcher/matcher_by_default_test.rb +0 -15
- data/test/unit/matcher/matcher_with_multiple_values_test.rb +0 -15
- data/test/unit/matcher/matcher_with_value_test.rb +0 -15
- data/test/unit/matcher/whitelist_matcher_test.rb +0 -30
- data/test/unit/matcher_helpers/matcher_helpers_all_test.rb +0 -14
- data/test/unit/matcher_helpers/matcher_helpers_any_test.rb +0 -14
- data/test/unit/matcher_helpers/matcher_helpers_same_test.rb +0 -13
- data/test/unit/node_collection/node_collection_after_being_copied_test.rb +0 -46
- data/test/unit/node_collection/node_collection_after_update_test.rb +0 -36
- data/test/unit/node_collection/node_collection_by_default_test.rb +0 -22
- data/test/unit/node_collection/node_collection_test.rb +0 -23
- data/test/unit/node_collection/node_collection_with_indices_test.rb +0 -42
- data/test/unit/node_collection/node_collection_with_matcher_contexts_test.rb +0 -25
- data/test/unit/node_collection/node_collection_with_nodes_test.rb +0 -46
- data/test/unit/node_collection/node_collection_with_numeric_index_test.rb +0 -24
- data/test/unit/node_collection/node_collection_with_postdefined_contexts_test.rb +0 -22
- data/test/unit/node_collection/node_collection_with_predefined_contexts_test.rb +0 -23
- data/test/unit/node_collection/node_collection_with_string_index_test.rb +0 -20
- data/test/unit/node_collection/node_collection_with_symbol_index_test.rb +0 -20
- data/test/unit/node_collection/node_collection_without_indices_test.rb +0 -30
- data/test/unit/path/path_by_default_test.rb +0 -54
- data/test/unit/path/path_test.rb +0 -14
- data/test/unit/path/path_with_available_transitions_after_reaching_target_test.rb +0 -40
- data/test/unit/path/path_with_available_transitions_test.rb +0 -54
- data/test/unit/path/path_with_deep_target_reached_test.rb +0 -50
- data/test/unit/path/path_with_deep_target_test.rb +0 -40
- data/test/unit/path/path_with_duplicates_test.rb +0 -32
- data/test/unit/path/path_with_encountered_transitions_test.rb +0 -34
- data/test/unit/path/path_with_guarded_transitions_test.rb +0 -42
- data/test/unit/path/path_with_reached_target_test.rb +0 -35
- data/test/unit/path/path_with_transitions_test.rb +0 -54
- data/test/unit/path/path_with_unreached_target_test.rb +0 -31
- data/test/unit/path/path_without_transitions_test.rb +0 -24
- data/test/unit/path_collection/path_collection_by_default_test.rb +0 -46
- data/test/unit/path_collection/path_collection_test.rb +0 -24
- data/test/unit/path_collection/path_collection_with_deep_paths_test.rb +0 -43
- data/test/unit/path_collection/path_collection_with_duplicate_nodes_test.rb +0 -31
- data/test/unit/path_collection/path_collection_with_from_state_test.rb +0 -27
- data/test/unit/path_collection/path_collection_with_paths_test.rb +0 -47
- data/test/unit/path_collection/path_collection_with_to_state_test.rb +0 -29
- data/test/unit/path_collection/path_with_guarded_paths_test.rb +0 -25
- data/test/unit/state/state_after_being_copied_test.rb +0 -19
- data/test/unit/state/state_by_default_test.rb +0 -41
- data/test/unit/state/state_final_test.rb +0 -28
- data/test/unit/state/state_initial_test.rb +0 -13
- data/test/unit/state/state_not_final_test.rb +0 -32
- data/test/unit/state/state_not_initial_test.rb +0 -13
- data/test/unit/state/state_test.rb +0 -44
- data/test/unit/state/state_with_cached_lambda_value_test.rb +0 -29
- data/test/unit/state/state_with_conflicting_helpers_after_definition_test.rb +0 -38
- data/test/unit/state/state_with_conflicting_helpers_before_definition_test.rb +0 -29
- data/test/unit/state/state_with_conflicting_machine_name_test.rb +0 -20
- data/test/unit/state/state_with_conflicting_machine_test.rb +0 -37
- data/test/unit/state/state_with_context_test.rb +0 -60
- data/test/unit/state/state_with_dynamic_human_name_test.rb +0 -25
- data/test/unit/state/state_with_existing_context_method_test.rb +0 -24
- data/test/unit/state/state_with_human_name_test.rb +0 -13
- data/test/unit/state/state_with_integer_value_test.rb +0 -32
- data/test/unit/state/state_with_invalid_method_call_test.rb +0 -21
- data/test/unit/state/state_with_lambda_value_test.rb +0 -37
- data/test/unit/state/state_with_matcher_test.rb +0 -18
- data/test/unit/state/state_with_multiple_contexts_test.rb +0 -57
- data/test/unit/state/state_with_name_test.rb +0 -43
- data/test/unit/state/state_with_namespace_test.rb +0 -22
- data/test/unit/state/state_with_nil_value_test.rb +0 -35
- data/test/unit/state/state_with_redefined_context_method_test.rb +0 -45
- data/test/unit/state/state_with_symbolic_value_test.rb +0 -32
- data/test/unit/state/state_with_valid_inherited_method_call_for_current_state_test.rb +0 -40
- data/test/unit/state/state_with_valid_method_call_for_current_state_test.rb +0 -33
- data/test/unit/state/state_with_valid_method_call_for_different_state_test.rb +0 -41
- data/test/unit/state/state_without_cached_lambda_value_test.rb +0 -25
- data/test/unit/state/state_without_name_test.rb +0 -39
- data/test/unit/state_collection/state_collection_by_default_test.rb +0 -21
- data/test/unit/state_collection/state_collection_string_test.rb +0 -35
- data/test/unit/state_collection/state_collection_test.rb +0 -74
- data/test/unit/state_collection/state_collection_with_custom_state_values_test.rb +0 -29
- data/test/unit/state_collection/state_collection_with_event_transitions_test.rb +0 -39
- data/test/unit/state_collection/state_collection_with_initial_state_test.rb +0 -40
- data/test/unit/state_collection/state_collection_with_namespace_test.rb +0 -21
- data/test/unit/state_collection/state_collection_with_state_behaviors_test.rb +0 -40
- data/test/unit/state_collection/state_collection_with_state_matchers_test.rb +0 -29
- data/test/unit/state_collection/state_collection_with_transition_callbacks_test.rb +0 -40
- data/test/unit/state_context/state_context_proxy_test.rb +0 -26
- data/test/unit/state_context/state_context_proxy_with_if_and_unless_conditions_test.rb +0 -42
- data/test/unit/state_context/state_context_proxy_with_if_condition_test.rb +0 -64
- data/test/unit/state_context/state_context_proxy_with_multiple_if_conditions_test.rb +0 -32
- data/test/unit/state_context/state_context_proxy_with_multiple_unless_conditions_test.rb +0 -32
- data/test/unit/state_context/state_context_proxy_with_unless_condition_test.rb +0 -64
- data/test/unit/state_context/state_context_proxy_without_conditions_test.rb +0 -31
- data/test/unit/state_context/state_context_test.rb +0 -28
- data/test/unit/state_context/state_context_transition_test.rb +0 -104
- data/test/unit/state_context/state_context_with_matching_transition_test.rb +0 -27
- data/test/unit/state_machine/state_machine_by_default_test.rb +0 -12
- data/test/unit/state_machine/state_machine_test.rb +0 -20
- data/test/unit/transition/transition_after_being_performed_test.rb +0 -48
- data/test/unit/transition/transition_after_being_persisted_test.rb +0 -46
- data/test/unit/transition/transition_after_being_rolled_back_test.rb +0 -35
- data/test/unit/transition/transition_equality_test.rb +0 -52
- data/test/unit/transition/transition_loopback_test.rb +0 -18
- data/test/unit/transition/transition_test.rb +0 -96
- data/test/unit/transition/transition_transient_test.rb +0 -20
- data/test/unit/transition/transition_with_action_test.rb +0 -27
- data/test/unit/transition/transition_with_after_callbacks_skipped_test.rb +0 -127
- data/test/unit/transition/transition_with_after_callbacks_test.rb +0 -93
- data/test/unit/transition/transition_with_around_callbacks_test.rb +0 -141
- data/test/unit/transition/transition_with_before_callbacks_skipped_test.rb +0 -30
- data/test/unit/transition/transition_with_before_callbacks_test.rb +0 -104
- data/test/unit/transition/transition_with_custom_machine_attribute_test.rb +0 -28
- data/test/unit/transition/transition_with_different_states_test.rb +0 -18
- data/test/unit/transition/transition_with_dynamic_to_value_test.rb +0 -19
- data/test/unit/transition/transition_with_failure_callbacks_test.rb +0 -84
- data/test/unit/transition/transition_with_invalid_nodes_test.rb +0 -29
- data/test/unit/transition/transition_with_mixed_callbacks_test.rb +0 -105
- data/test/unit/transition/transition_with_multiple_after_callbacks_test.rb +0 -40
- data/test/unit/transition/transition_with_multiple_around_callbacks_test.rb +0 -114
- data/test/unit/transition/transition_with_multiple_before_callbacks_test.rb +0 -40
- data/test/unit/transition/transition_with_multiple_failure_callbacks_test.rb +0 -40
- data/test/unit/transition/transition_with_namespace_test.rb +0 -47
- data/test/unit/transition/transition_with_perform_arguments_test.rb +0 -35
- data/test/unit/transition/transition_with_transactions_test.rb +0 -42
- data/test/unit/transition/transition_without_callbacks_test.rb +0 -33
- data/test/unit/transition/transition_without_reading_state_test.rb +0 -22
- data/test/unit/transition/transition_without_running_action_test.rb +0 -47
- data/test/unit/transition_collection/attribute_transition_collection_by_default_test.rb +0 -23
- data/test/unit/transition_collection/attribute_transition_collection_marshalling_test.rb +0 -64
- data/test/unit/transition_collection/attribute_transition_collection_with_action_error_test.rb +0 -44
- data/test/unit/transition_collection/attribute_transition_collection_with_action_failed_test.rb +0 -44
- data/test/unit/transition_collection/attribute_transition_collection_with_after_callback_error_test.rb +0 -32
- data/test/unit/transition_collection/attribute_transition_collection_with_after_callback_halt_test.rb +0 -33
- data/test/unit/transition_collection/attribute_transition_collection_with_around_after_yield_callback_error_test.rb +0 -32
- data/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_error_test.rb +0 -32
- data/test/unit/transition_collection/attribute_transition_collection_with_around_callback_after_yield_halt_test.rb +0 -33
- data/test/unit/transition_collection/attribute_transition_collection_with_around_callback_before_yield_halt_test.rb +0 -33
- data/test/unit/transition_collection/attribute_transition_collection_with_before_callback_error_test.rb +0 -32
- data/test/unit/transition_collection/attribute_transition_collection_with_before_callback_halt_test.rb +0 -33
- data/test/unit/transition_collection/attribute_transition_collection_with_callbacks_test.rb +0 -68
- data/test/unit/transition_collection/attribute_transition_collection_with_event_transitions_test.rb +0 -41
- data/test/unit/transition_collection/attribute_transition_collection_with_events_test.rb +0 -44
- data/test/unit/transition_collection/attribute_transition_collection_with_skipped_after_callbacks_test.rb +0 -42
- data/test/unit/transition_collection/transition_collection_by_default_test.rb +0 -23
- data/test/unit/transition_collection/transition_collection_empty_with_block_test.rb +0 -23
- data/test/unit/transition_collection/transition_collection_empty_without_block_test.rb +0 -12
- data/test/unit/transition_collection/transition_collection_invalid_test.rb +0 -21
- data/test/unit/transition_collection/transition_collection_partial_invalid_test.rb +0 -69
- data/test/unit/transition_collection/transition_collection_test.rb +0 -26
- data/test/unit/transition_collection/transition_collection_valid_test.rb +0 -57
- data/test/unit/transition_collection/transition_collection_with_action_error_test.rb +0 -66
- data/test/unit/transition_collection/transition_collection_with_action_failed_test.rb +0 -60
- data/test/unit/transition_collection/transition_collection_with_action_hook_and_block_test.rb +0 -17
- data/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_action_test.rb +0 -17
- data/test/unit/transition_collection/transition_collection_with_action_hook_and_skipped_after_callbacks_test.rb +0 -37
- data/test/unit/transition_collection/transition_collection_with_action_hook_base_test.rb +0 -34
- data/test/unit/transition_collection/transition_collection_with_action_hook_error_test.rb +0 -29
- data/test/unit/transition_collection/transition_collection_with_action_hook_invalid_test.rb +0 -17
- data/test/unit/transition_collection/transition_collection_with_action_hook_multiple_test.rb +0 -79
- data/test/unit/transition_collection/transition_collection_with_action_hook_test.rb +0 -45
- data/test/unit/transition_collection/transition_collection_with_action_hook_with_different_actions_test.rb +0 -48
- data/test/unit/transition_collection/transition_collection_with_action_hook_with_nil_action_test.rb +0 -42
- data/test/unit/transition_collection/transition_collection_with_after_callback_halt_test.rb +0 -47
- data/test/unit/transition_collection/transition_collection_with_before_callback_halt_test.rb +0 -51
- data/test/unit/transition_collection/transition_collection_with_block_test.rb +0 -46
- data/test/unit/transition_collection/transition_collection_with_callbacks_test.rb +0 -135
- data/test/unit/transition_collection/transition_collection_with_different_actions_test.rb +0 -189
- data/test/unit/transition_collection/transition_collection_with_duplicate_actions_test.rb +0 -48
- data/test/unit/transition_collection/transition_collection_with_empty_actions_test.rb +0 -41
- data/test/unit/transition_collection/transition_collection_with_mixed_actions_test.rb +0 -41
- data/test/unit/transition_collection/transition_collection_with_skipped_actions_and_block_test.rb +0 -34
- data/test/unit/transition_collection/transition_collection_with_skipped_actions_test.rb +0 -69
- data/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_and_around_callbacks_test.rb +0 -53
- data/test/unit/transition_collection/transition_collection_with_skipped_after_callbacks_test.rb +0 -34
- data/test/unit/transition_collection/transition_collection_with_transactions_test.rb +0 -65
- 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 :
|
|
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 :
|
|
146
|
-
# after_transition :
|
|
147
|
-
#
|
|
164
|
+
# state_machine initial: :pending
|
|
165
|
+
# after_transition pending: :parked, do: :track_initial_transition
|
|
166
|
+
#
|
|
148
167
|
# event :park do
|
|
149
|
-
# transition :
|
|
168
|
+
# transition pending: :parked
|
|
150
169
|
# end
|
|
151
|
-
#
|
|
152
|
-
# state :pending, :
|
|
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 :
|
|
165
|
-
# after_transition :
|
|
166
|
-
#
|
|
183
|
+
# state_machine initial: :pending
|
|
184
|
+
# after_transition pending: :parked, do: :track_initial_transition
|
|
185
|
+
#
|
|
167
186
|
# event :park do
|
|
168
|
-
# transition :
|
|
187
|
+
# transition pending: :parked
|
|
169
188
|
# end
|
|
170
|
-
#
|
|
171
|
-
# state :pending, :
|
|
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 :
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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,100 @@ 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,
|
|
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 |*
|
|
729
|
-
block.call((scope == :instance ? self.class : self).state_machine(name), self, *
|
|
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,
|
|
568
|
+
helper_module.class_eval(method, __FILE__, __LINE__)
|
|
735
569
|
end
|
|
736
570
|
end
|
|
737
571
|
|
|
738
572
|
# Customizes the definition of one or more states in the machine.
|
|
739
|
-
#
|
|
573
|
+
#
|
|
740
574
|
# Configuration options:
|
|
741
575
|
# * <tt>:value</tt> - The actual value to store when an object transitions
|
|
742
576
|
# to the state. Default is the name (stringified).
|
|
@@ -748,13 +582,13 @@ module StateMachines
|
|
|
748
582
|
# * <tt>:human_name</tt> - The human-readable version of this state's name.
|
|
749
583
|
# By default, this is either defined by the integration or stringifies the
|
|
750
584
|
# name and converts underscores to spaces.
|
|
751
|
-
#
|
|
585
|
+
#
|
|
752
586
|
# == Customizing the stored value
|
|
753
|
-
#
|
|
587
|
+
#
|
|
754
588
|
# Whenever a state is automatically discovered in the state machine, its
|
|
755
589
|
# default value is assumed to be the stringified version of the name. For
|
|
756
590
|
# example,
|
|
757
|
-
#
|
|
591
|
+
#
|
|
758
592
|
# class Vehicle
|
|
759
593
|
# state_machine :initial => :parked do
|
|
760
594
|
# event :ignite do
|
|
@@ -762,124 +596,124 @@ module StateMachines
|
|
|
762
596
|
# end
|
|
763
597
|
# end
|
|
764
598
|
# end
|
|
765
|
-
#
|
|
599
|
+
#
|
|
766
600
|
# In the above state machine, there are two states automatically discovered:
|
|
767
601
|
# :parked and :idling. These states, by default, will store their stringified
|
|
768
602
|
# equivalents when an object moves into that state (e.g. "parked" / "idling").
|
|
769
|
-
#
|
|
603
|
+
#
|
|
770
604
|
# For legacy systems or when tying state machines into existing frameworks,
|
|
771
605
|
# it's oftentimes necessary to need to store a different value for a state
|
|
772
606
|
# than the default. In order to continue taking advantage of an expressive
|
|
773
607
|
# state machine and helper methods, every defined state can be re-configured
|
|
774
608
|
# with a custom stored value. For example,
|
|
775
|
-
#
|
|
609
|
+
#
|
|
776
610
|
# class Vehicle
|
|
777
611
|
# state_machine :initial => :parked do
|
|
778
612
|
# event :ignite do
|
|
779
613
|
# transition :parked => :idling
|
|
780
614
|
# end
|
|
781
|
-
#
|
|
615
|
+
#
|
|
782
616
|
# state :idling, :value => 'IDLING'
|
|
783
617
|
# state :parked, :value => 'PARKED
|
|
784
618
|
# end
|
|
785
619
|
# end
|
|
786
|
-
#
|
|
620
|
+
#
|
|
787
621
|
# This is also useful if being used in association with a database and,
|
|
788
622
|
# instead of storing the state name in a column, you want to store the
|
|
789
623
|
# state's foreign key:
|
|
790
|
-
#
|
|
624
|
+
#
|
|
791
625
|
# class VehicleState < ActiveRecord::Base
|
|
792
626
|
# end
|
|
793
|
-
#
|
|
627
|
+
#
|
|
794
628
|
# class Vehicle < ActiveRecord::Base
|
|
795
629
|
# state_machine :attribute => :state_id, :initial => :parked do
|
|
796
630
|
# event :ignite do
|
|
797
631
|
# transition :parked => :idling
|
|
798
632
|
# end
|
|
799
|
-
#
|
|
633
|
+
#
|
|
800
634
|
# states.each do |state|
|
|
801
635
|
# self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
|
|
802
636
|
# end
|
|
803
637
|
# end
|
|
804
638
|
# end
|
|
805
|
-
#
|
|
639
|
+
#
|
|
806
640
|
# In the above example, each known state is configured to store it's
|
|
807
641
|
# associated database id in the +state_id+ attribute. Also, notice that a
|
|
808
642
|
# lambda block is used to define the state's value. This is required in
|
|
809
643
|
# situations (like testing) where the model is loaded without any existing
|
|
810
644
|
# data (i.e. no VehicleState records available).
|
|
811
|
-
#
|
|
645
|
+
#
|
|
812
646
|
# One caveat to the above example is to keep performance in mind. To avoid
|
|
813
647
|
# constant db hits for looking up the VehicleState ids, the value is cached
|
|
814
648
|
# by specifying the <tt>:cache</tt> option. Alternatively, a custom
|
|
815
649
|
# caching strategy can be used like so:
|
|
816
|
-
#
|
|
650
|
+
#
|
|
817
651
|
# class VehicleState < ActiveRecord::Base
|
|
818
652
|
# cattr_accessor :cache_store
|
|
819
653
|
# self.cache_store = ActiveSupport::Cache::MemoryStore.new
|
|
820
|
-
#
|
|
654
|
+
#
|
|
821
655
|
# def self.find_by_name(name)
|
|
822
656
|
# cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
|
|
823
657
|
# end
|
|
824
658
|
# end
|
|
825
|
-
#
|
|
659
|
+
#
|
|
826
660
|
# === Dynamic values
|
|
827
|
-
#
|
|
661
|
+
#
|
|
828
662
|
# In addition to customizing states with other value types, lambda blocks
|
|
829
663
|
# can also be specified to allow for a state's value to be determined
|
|
830
664
|
# dynamically at runtime. For example,
|
|
831
|
-
#
|
|
665
|
+
#
|
|
832
666
|
# class Vehicle
|
|
833
667
|
# state_machine :purchased_at, :initial => :available do
|
|
834
668
|
# event :purchase do
|
|
835
669
|
# transition all => :purchased
|
|
836
670
|
# end
|
|
837
|
-
#
|
|
671
|
+
#
|
|
838
672
|
# event :restock do
|
|
839
673
|
# transition all => :available
|
|
840
674
|
# end
|
|
841
|
-
#
|
|
675
|
+
#
|
|
842
676
|
# state :available, :value => nil
|
|
843
677
|
# state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
|
|
844
678
|
# end
|
|
845
679
|
# end
|
|
846
|
-
#
|
|
680
|
+
#
|
|
847
681
|
# In the above definition, the <tt>:purchased</tt> state is customized with
|
|
848
682
|
# both a dynamic value *and* a value matcher.
|
|
849
|
-
#
|
|
683
|
+
#
|
|
850
684
|
# When an object transitions to the purchased state, the value's lambda
|
|
851
685
|
# block will be called. This will get the current time and store it in the
|
|
852
686
|
# object's +purchased_at+ attribute.
|
|
853
|
-
#
|
|
687
|
+
#
|
|
854
688
|
# *Note* that the custom matcher is very important here. Since there's no
|
|
855
689
|
# way for the state machine to figure out an object's state when it's set to
|
|
856
690
|
# a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
|
|
857
691
|
# were not configured for the state, then an ArgumentError exception would
|
|
858
692
|
# be raised at runtime, indicating that the state machine could not figure
|
|
859
693
|
# out what the current state of the object was.
|
|
860
|
-
#
|
|
694
|
+
#
|
|
861
695
|
# == Behaviors
|
|
862
|
-
#
|
|
696
|
+
#
|
|
863
697
|
# Behaviors define a series of methods to mixin with objects when the current
|
|
864
698
|
# state matches the given one(s). This allows instance methods to behave
|
|
865
699
|
# a specific way depending on what the value of the object's state is.
|
|
866
|
-
#
|
|
700
|
+
#
|
|
867
701
|
# For example,
|
|
868
|
-
#
|
|
702
|
+
#
|
|
869
703
|
# class Vehicle
|
|
870
704
|
# attr_accessor :driver
|
|
871
705
|
# attr_accessor :passenger
|
|
872
|
-
#
|
|
706
|
+
#
|
|
873
707
|
# state_machine :initial => :parked do
|
|
874
708
|
# event :ignite do
|
|
875
709
|
# transition :parked => :idling
|
|
876
710
|
# end
|
|
877
|
-
#
|
|
711
|
+
#
|
|
878
712
|
# state :parked do
|
|
879
713
|
# def speed
|
|
880
714
|
# 0
|
|
881
715
|
# end
|
|
882
|
-
#
|
|
716
|
+
#
|
|
883
717
|
# def rotate_driver
|
|
884
718
|
# driver = self.driver
|
|
885
719
|
# self.driver = passenger
|
|
@@ -887,97 +721,97 @@ module StateMachines
|
|
|
887
721
|
# true
|
|
888
722
|
# end
|
|
889
723
|
# end
|
|
890
|
-
#
|
|
724
|
+
#
|
|
891
725
|
# state :idling, :first_gear do
|
|
892
726
|
# def speed
|
|
893
727
|
# 20
|
|
894
728
|
# end
|
|
895
|
-
#
|
|
729
|
+
#
|
|
896
730
|
# def rotate_driver
|
|
897
731
|
# self.state = 'parked'
|
|
898
732
|
# rotate_driver
|
|
899
733
|
# end
|
|
900
734
|
# end
|
|
901
|
-
#
|
|
735
|
+
#
|
|
902
736
|
# other_states :backing_up
|
|
903
737
|
# end
|
|
904
738
|
# end
|
|
905
|
-
#
|
|
739
|
+
#
|
|
906
740
|
# In the above example, there are two dynamic behaviors defined for the
|
|
907
741
|
# class:
|
|
908
742
|
# * +speed+
|
|
909
743
|
# * +rotate_driver+
|
|
910
|
-
#
|
|
744
|
+
#
|
|
911
745
|
# Each of these behaviors are instance methods on the Vehicle class. However,
|
|
912
746
|
# which method actually gets invoked is based on the current state of the
|
|
913
747
|
# object. Using the above class as the example:
|
|
914
|
-
#
|
|
748
|
+
#
|
|
915
749
|
# vehicle = Vehicle.new
|
|
916
750
|
# vehicle.driver = 'John'
|
|
917
751
|
# vehicle.passenger = 'Jane'
|
|
918
|
-
#
|
|
752
|
+
#
|
|
919
753
|
# # Behaviors in the "parked" state
|
|
920
754
|
# vehicle.state # => "parked"
|
|
921
755
|
# vehicle.speed # => 0
|
|
922
756
|
# vehicle.rotate_driver # => true
|
|
923
757
|
# vehicle.driver # => "Jane"
|
|
924
758
|
# vehicle.passenger # => "John"
|
|
925
|
-
#
|
|
759
|
+
#
|
|
926
760
|
# vehicle.ignite # => true
|
|
927
|
-
#
|
|
761
|
+
#
|
|
928
762
|
# # Behaviors in the "idling" state
|
|
929
763
|
# vehicle.state # => "idling"
|
|
930
764
|
# vehicle.speed # => 20
|
|
931
765
|
# vehicle.rotate_driver # => true
|
|
932
766
|
# vehicle.driver # => "John"
|
|
933
767
|
# vehicle.passenger # => "Jane"
|
|
934
|
-
#
|
|
768
|
+
#
|
|
935
769
|
# As can be seen, both the +speed+ and +rotate_driver+ instance method
|
|
936
770
|
# implementations changed how they behave based on what the current state
|
|
937
771
|
# of the vehicle was.
|
|
938
|
-
#
|
|
772
|
+
#
|
|
939
773
|
# === Invalid behaviors
|
|
940
|
-
#
|
|
774
|
+
#
|
|
941
775
|
# If a specific behavior has not been defined for a state, then a
|
|
942
776
|
# NoMethodError exception will be raised, indicating that that method would
|
|
943
777
|
# not normally exist for an object with that state.
|
|
944
|
-
#
|
|
778
|
+
#
|
|
945
779
|
# Using the example from before:
|
|
946
|
-
#
|
|
780
|
+
#
|
|
947
781
|
# vehicle = Vehicle.new
|
|
948
782
|
# vehicle.state = 'backing_up'
|
|
949
783
|
# vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
|
|
950
|
-
#
|
|
784
|
+
#
|
|
951
785
|
# === Using matchers
|
|
952
|
-
#
|
|
786
|
+
#
|
|
953
787
|
# The +all+ / +any+ matchers can be used to easily define behaviors for a
|
|
954
788
|
# group of states. Note, however, that you cannot use these matchers to
|
|
955
789
|
# set configurations for states. Behaviors using these matchers can be
|
|
956
790
|
# defined at any point in the state machine and will always get applied to
|
|
957
791
|
# the proper states.
|
|
958
|
-
#
|
|
792
|
+
#
|
|
959
793
|
# For example:
|
|
960
|
-
#
|
|
794
|
+
#
|
|
961
795
|
# state_machine :initial => :parked do
|
|
962
796
|
# ...
|
|
963
|
-
#
|
|
797
|
+
#
|
|
964
798
|
# state all - [:parked, :idling, :stalled] do
|
|
965
799
|
# validates_presence_of :speed
|
|
966
|
-
#
|
|
800
|
+
#
|
|
967
801
|
# def speed
|
|
968
802
|
# gear * 10
|
|
969
803
|
# end
|
|
970
804
|
# end
|
|
971
805
|
# end
|
|
972
|
-
#
|
|
806
|
+
#
|
|
973
807
|
# == State-aware class methods
|
|
974
|
-
#
|
|
808
|
+
#
|
|
975
809
|
# In addition to defining scopes for instance methods that are state-aware,
|
|
976
810
|
# the same can be done for certain types of class methods.
|
|
977
|
-
#
|
|
811
|
+
#
|
|
978
812
|
# Some libraries have support for class-level methods that only run certain
|
|
979
813
|
# behaviors based on a conditions hash passed in. For example:
|
|
980
|
-
#
|
|
814
|
+
#
|
|
981
815
|
# class Vehicle < ActiveRecord::Base
|
|
982
816
|
# state_machine do
|
|
983
817
|
# ...
|
|
@@ -987,110 +821,35 @@ module StateMachines
|
|
|
987
821
|
# end
|
|
988
822
|
# end
|
|
989
823
|
# end
|
|
990
|
-
#
|
|
824
|
+
#
|
|
991
825
|
# In the above ActiveRecord model, two validations have been defined which
|
|
992
826
|
# will *only* run when the Vehicle object is in one of the three states:
|
|
993
827
|
# +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
|
|
994
828
|
# conditions can continue to be used.
|
|
995
|
-
#
|
|
829
|
+
#
|
|
996
830
|
# This functionality is not library-specific and can work for any class-level
|
|
997
831
|
# method that is defined like so:
|
|
998
|
-
#
|
|
832
|
+
#
|
|
999
833
|
# def validates_presence_of(attribute, options = {})
|
|
1000
834
|
# ...
|
|
1001
835
|
# end
|
|
1002
|
-
#
|
|
836
|
+
#
|
|
1003
837
|
# The minimum requirement is that the last argument in the method be an
|
|
1004
838
|
# 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
839
|
|
|
1081
840
|
# Defines one or more events for the machine and the transitions that can
|
|
1082
841
|
# be performed when those events are run.
|
|
1083
|
-
#
|
|
842
|
+
#
|
|
1084
843
|
# This method is also aliased as +on+ for improved compatibility with
|
|
1085
844
|
# using a domain-specific language.
|
|
1086
|
-
#
|
|
845
|
+
#
|
|
1087
846
|
# Configuration options:
|
|
1088
847
|
# * <tt>:human_name</tt> - The human-readable version of this event's name.
|
|
1089
848
|
# By default, this is either defined by the integration or stringifies the
|
|
1090
849
|
# name and converts underscores to spaces.
|
|
1091
|
-
#
|
|
850
|
+
#
|
|
1092
851
|
# == Instance methods
|
|
1093
|
-
#
|
|
852
|
+
#
|
|
1094
853
|
# The following instance methods are generated when a new event is defined
|
|
1095
854
|
# (the "park" event is used as an example):
|
|
1096
855
|
# * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
|
|
@@ -1114,13 +873,13 @@ module StateMachines
|
|
|
1114
873
|
# object or nil if no transitions can be performed. Like <tt>can_park?</tt>
|
|
1115
874
|
# this will also *not* run validations or callbacks. It will only
|
|
1116
875
|
# determine if the state machine defines a valid transition for the event.
|
|
1117
|
-
#
|
|
876
|
+
#
|
|
1118
877
|
# With a namespace of "car", the above names map to the following methods:
|
|
1119
878
|
# * <tt>can_park_car?</tt>
|
|
1120
879
|
# * <tt>park_car_transition</tt>
|
|
1121
880
|
# * <tt>park_car</tt>
|
|
1122
881
|
# * <tt>park_car!</tt>
|
|
1123
|
-
#
|
|
882
|
+
#
|
|
1124
883
|
# The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
|
|
1125
884
|
# optional set of requirements for determining what transitions are available
|
|
1126
885
|
# for the current object. These requirements include:
|
|
@@ -1130,58 +889,58 @@ module StateMachines
|
|
|
1130
889
|
# specified, then this will match any to state.
|
|
1131
890
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
|
1132
891
|
# conditionals defined for each one. Default is true.
|
|
1133
|
-
#
|
|
892
|
+
#
|
|
1134
893
|
# == Defining transitions
|
|
1135
|
-
#
|
|
894
|
+
#
|
|
1136
895
|
# +event+ requires a block which allows you to define the possible
|
|
1137
896
|
# transitions that can happen as a result of that event. For example,
|
|
1138
|
-
#
|
|
897
|
+
#
|
|
1139
898
|
# event :park, :stop do
|
|
1140
899
|
# transition :idling => :parked
|
|
1141
900
|
# end
|
|
1142
|
-
#
|
|
901
|
+
#
|
|
1143
902
|
# event :first_gear do
|
|
1144
903
|
# transition :parked => :first_gear, :if => :seatbelt_on?
|
|
1145
904
|
# transition :parked => same # Allow to loopback if seatbelt is off
|
|
1146
905
|
# end
|
|
1147
|
-
#
|
|
906
|
+
#
|
|
1148
907
|
# See StateMachines::Event#transition for more information on
|
|
1149
908
|
# the possible options that can be passed in.
|
|
1150
|
-
#
|
|
909
|
+
#
|
|
1151
910
|
# *Note* that this block is executed within the context of the actual event
|
|
1152
911
|
# object. As a result, you will not be able to reference any class methods
|
|
1153
912
|
# on the model without referencing the class itself. For example,
|
|
1154
|
-
#
|
|
913
|
+
#
|
|
1155
914
|
# class Vehicle
|
|
1156
915
|
# def self.safe_states
|
|
1157
916
|
# [:parked, :idling, :stalled]
|
|
1158
917
|
# end
|
|
1159
|
-
#
|
|
918
|
+
#
|
|
1160
919
|
# state_machine do
|
|
1161
920
|
# event :park do
|
|
1162
921
|
# transition Vehicle.safe_states => :parked
|
|
1163
922
|
# end
|
|
1164
923
|
# end
|
|
1165
|
-
# end
|
|
1166
|
-
#
|
|
924
|
+
# end
|
|
925
|
+
#
|
|
1167
926
|
# == Overriding the event method
|
|
1168
|
-
#
|
|
927
|
+
#
|
|
1169
928
|
# By default, this will define an instance method (with the same name as the
|
|
1170
929
|
# event) that will fire the next possible transition for that. Although the
|
|
1171
930
|
# +before_transition+, +after_transition+, and +around_transition+ hooks
|
|
1172
931
|
# allow you to define behavior that gets executed as a result of the event's
|
|
1173
932
|
# transition, you can also override the event method in order to have a
|
|
1174
933
|
# little more fine-grained control.
|
|
1175
|
-
#
|
|
934
|
+
#
|
|
1176
935
|
# For example:
|
|
1177
|
-
#
|
|
936
|
+
#
|
|
1178
937
|
# class Vehicle
|
|
1179
938
|
# state_machine do
|
|
1180
939
|
# event :park do
|
|
1181
940
|
# ...
|
|
1182
941
|
# end
|
|
1183
942
|
# end
|
|
1184
|
-
#
|
|
943
|
+
#
|
|
1185
944
|
# def park(*)
|
|
1186
945
|
# take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
|
|
1187
946
|
# if result = super # Runs the transition and all before/after/around hooks
|
|
@@ -1190,33 +949,33 @@ module StateMachines
|
|
|
1190
949
|
# result
|
|
1191
950
|
# end
|
|
1192
951
|
# end
|
|
1193
|
-
#
|
|
952
|
+
#
|
|
1194
953
|
# There are a few important things to note here. First, the method
|
|
1195
954
|
# signature is defined with an unlimited argument list in order to allow
|
|
1196
955
|
# callers to continue passing arguments that are expected by state_machine.
|
|
1197
956
|
# For example, it will still allow calls to +park+ with a single parameter
|
|
1198
957
|
# for skipping the configured action.
|
|
1199
|
-
#
|
|
958
|
+
#
|
|
1200
959
|
# Second, the overridden event method must call +super+ in order to run the
|
|
1201
960
|
# logic for running the next possible transition. In order to remain
|
|
1202
961
|
# consistent with other events, the result of +super+ is returned.
|
|
1203
|
-
#
|
|
962
|
+
#
|
|
1204
963
|
# Third, any behavior defined in this method will *not* get executed if
|
|
1205
964
|
# you're taking advantage of attribute-based event transitions. For example:
|
|
1206
|
-
#
|
|
965
|
+
#
|
|
1207
966
|
# vehicle = Vehicle.new
|
|
1208
967
|
# vehicle.state_event = 'park'
|
|
1209
968
|
# vehicle.save
|
|
1210
|
-
#
|
|
969
|
+
#
|
|
1211
970
|
# In this case, the +park+ event will run the before/after/around transition
|
|
1212
971
|
# hooks and transition the state, but the behavior defined in the overriden
|
|
1213
972
|
# +park+ method will *not* be executed.
|
|
1214
|
-
#
|
|
973
|
+
#
|
|
1215
974
|
# == Defining additional arguments
|
|
1216
|
-
#
|
|
975
|
+
#
|
|
1217
976
|
# Additional arguments can be passed into events and accessed by transition
|
|
1218
977
|
# hooks like so:
|
|
1219
|
-
#
|
|
978
|
+
#
|
|
1220
979
|
# class Vehicle
|
|
1221
980
|
# state_machine do
|
|
1222
981
|
# after_transition :on => :park do |vehicle, transition|
|
|
@@ -1224,157 +983,128 @@ module StateMachines
|
|
|
1224
983
|
# ...
|
|
1225
984
|
# end
|
|
1226
985
|
# after_transition :on => :park, :do => :take_deep_breath
|
|
1227
|
-
#
|
|
986
|
+
#
|
|
1228
987
|
# event :park do
|
|
1229
988
|
# ...
|
|
1230
989
|
# end
|
|
1231
|
-
#
|
|
990
|
+
#
|
|
1232
991
|
# def take_deep_breath(transition)
|
|
1233
992
|
# kind = *transition.args # :parallel
|
|
1234
993
|
# ...
|
|
1235
994
|
# end
|
|
1236
995
|
# end
|
|
1237
996
|
# end
|
|
1238
|
-
#
|
|
997
|
+
#
|
|
1239
998
|
# vehicle = Vehicle.new
|
|
1240
999
|
# vehicle.park(:parallel)
|
|
1241
|
-
#
|
|
1000
|
+
#
|
|
1242
1001
|
# *Remember* that if the last argument is a boolean, it will be used as the
|
|
1243
1002
|
# +run_action+ parameter to the event action. Using the +park+ action
|
|
1244
1003
|
# example from above, you can might call it like so:
|
|
1245
|
-
#
|
|
1004
|
+
#
|
|
1246
1005
|
# vehicle.park # => Uses default args and runs machine action
|
|
1247
1006
|
# vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
|
|
1248
1007
|
# vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
|
|
1249
|
-
#
|
|
1008
|
+
#
|
|
1250
1009
|
# If you decide to override the +park+ event method *and* define additional
|
|
1251
1010
|
# arguments, you can do so as shown below:
|
|
1252
|
-
#
|
|
1011
|
+
#
|
|
1253
1012
|
# class Vehicle
|
|
1254
1013
|
# state_machine do
|
|
1255
1014
|
# event :park do
|
|
1256
1015
|
# ...
|
|
1257
1016
|
# end
|
|
1258
1017
|
# end
|
|
1259
|
-
#
|
|
1018
|
+
#
|
|
1260
1019
|
# def park(kind = :parallel, *args)
|
|
1261
1020
|
# take_deep_breath if kind == :parallel
|
|
1262
1021
|
# super
|
|
1263
1022
|
# end
|
|
1264
1023
|
# end
|
|
1265
|
-
#
|
|
1024
|
+
#
|
|
1266
1025
|
# Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
|
|
1267
1026
|
# the entire arguments list to be accessed by transition callbacks through
|
|
1268
1027
|
# StateMachines::Transition#args.
|
|
1269
|
-
#
|
|
1028
|
+
#
|
|
1270
1029
|
# === Using matchers
|
|
1271
|
-
#
|
|
1030
|
+
#
|
|
1272
1031
|
# The +all+ / +any+ matchers can be used to easily execute blocks for a
|
|
1273
1032
|
# group of events. Note, however, that you cannot use these matchers to
|
|
1274
1033
|
# set configurations for events. Blocks using these matchers can be
|
|
1275
1034
|
# defined at any point in the state machine and will always get applied to
|
|
1276
1035
|
# the proper events.
|
|
1277
|
-
#
|
|
1036
|
+
#
|
|
1278
1037
|
# For example:
|
|
1279
|
-
#
|
|
1038
|
+
#
|
|
1280
1039
|
# state_machine :initial => :parked do
|
|
1281
1040
|
# ...
|
|
1282
|
-
#
|
|
1041
|
+
#
|
|
1283
1042
|
# event all - [:crash] do
|
|
1284
1043
|
# transition :stalled => :parked
|
|
1285
1044
|
# end
|
|
1286
1045
|
# end
|
|
1287
|
-
#
|
|
1046
|
+
#
|
|
1288
1047
|
# == Example
|
|
1289
|
-
#
|
|
1048
|
+
#
|
|
1290
1049
|
# class Vehicle
|
|
1291
1050
|
# state_machine do
|
|
1292
1051
|
# # The park, stop, and halt events will all share the given transitions
|
|
1293
1052
|
# event :park, :stop, :halt do
|
|
1294
1053
|
# transition [:idling, :backing_up] => :parked
|
|
1295
1054
|
# end
|
|
1296
|
-
#
|
|
1055
|
+
#
|
|
1297
1056
|
# event :stop do
|
|
1298
1057
|
# transition :first_gear => :idling
|
|
1299
1058
|
# end
|
|
1300
|
-
#
|
|
1059
|
+
#
|
|
1301
1060
|
# event :ignite do
|
|
1302
1061
|
# transition :parked => :idling
|
|
1303
1062
|
# transition :idling => same # Allow ignite while still idling
|
|
1304
1063
|
# end
|
|
1305
1064
|
# end
|
|
1306
1065
|
# 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
1066
|
|
|
1337
1067
|
# Creates a new transition that determines what to change the current state
|
|
1338
1068
|
# to when an event fires.
|
|
1339
|
-
#
|
|
1069
|
+
#
|
|
1340
1070
|
# == Defining transitions
|
|
1341
|
-
#
|
|
1071
|
+
#
|
|
1342
1072
|
# The options for a new transition uses the Hash syntax to map beginning
|
|
1343
1073
|
# states to ending states. For example,
|
|
1344
|
-
#
|
|
1074
|
+
#
|
|
1345
1075
|
# transition :parked => :idling, :idling => :first_gear, :on => :ignite
|
|
1346
|
-
#
|
|
1076
|
+
#
|
|
1347
1077
|
# In this case, when the +ignite+ event is fired, this transition will cause
|
|
1348
1078
|
# the state to be +idling+ if it's current state is +parked+ or +first_gear+
|
|
1349
1079
|
# if it's current state is +idling+.
|
|
1350
|
-
#
|
|
1080
|
+
#
|
|
1351
1081
|
# To help define these implicit transitions, a set of helpers are available
|
|
1352
1082
|
# for slightly more complex matching:
|
|
1353
1083
|
# * <tt>all</tt> - Matches every state in the machine
|
|
1354
1084
|
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
|
|
1355
1085
|
# * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
|
|
1356
1086
|
# * <tt>same</tt> - Matches the same state being transitioned from
|
|
1357
|
-
#
|
|
1087
|
+
#
|
|
1358
1088
|
# See StateMachines::MatcherHelpers for more information.
|
|
1359
|
-
#
|
|
1089
|
+
#
|
|
1360
1090
|
# Examples:
|
|
1361
|
-
#
|
|
1091
|
+
#
|
|
1362
1092
|
# transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
|
|
1363
1093
|
# transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
|
|
1364
1094
|
# transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
|
|
1365
1095
|
# transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
|
|
1366
1096
|
# transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
|
|
1367
1097
|
# transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
|
|
1368
|
-
#
|
|
1098
|
+
#
|
|
1369
1099
|
# transition :parked => same, :on => :park # Loops :parked back to :parked
|
|
1370
1100
|
# transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
|
|
1371
1101
|
# transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
|
|
1372
|
-
#
|
|
1102
|
+
#
|
|
1373
1103
|
# # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
|
|
1374
1104
|
# transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
|
|
1375
|
-
#
|
|
1105
|
+
#
|
|
1376
1106
|
# == Verbose transitions
|
|
1377
|
-
#
|
|
1107
|
+
#
|
|
1378
1108
|
# Transitions can also be defined use an explicit set of configuration
|
|
1379
1109
|
# options:
|
|
1380
1110
|
# * <tt>:from</tt> - A state or array of states that can be transitioned from.
|
|
@@ -1383,24 +1113,24 @@ module StateMachines
|
|
|
1383
1113
|
# then the transition will simply loop back (i.e. the state will not change).
|
|
1384
1114
|
# * <tt>:except_from</tt> - A state or array of states that *cannot* be
|
|
1385
1115
|
# transitioned from.
|
|
1386
|
-
#
|
|
1116
|
+
#
|
|
1387
1117
|
# These options must be used when defining transitions within the context
|
|
1388
1118
|
# of a state.
|
|
1389
|
-
#
|
|
1119
|
+
#
|
|
1390
1120
|
# Examples:
|
|
1391
|
-
#
|
|
1121
|
+
#
|
|
1392
1122
|
# transition :to => nil, :on => :park
|
|
1393
1123
|
# transition :to => :idling, :on => :ignite
|
|
1394
1124
|
# transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
|
|
1395
1125
|
# transition :from => nil, :to => :idling, :on => :ignite
|
|
1396
1126
|
# transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
|
|
1397
|
-
#
|
|
1127
|
+
#
|
|
1398
1128
|
# == Conditions
|
|
1399
|
-
#
|
|
1129
|
+
#
|
|
1400
1130
|
# In addition to the state requirements for each transition, a condition
|
|
1401
1131
|
# can also be defined to help determine whether that transition is
|
|
1402
1132
|
# available. These options will work on both the normal and verbose syntax.
|
|
1403
|
-
#
|
|
1133
|
+
#
|
|
1404
1134
|
# Configuration options:
|
|
1405
1135
|
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
|
1406
1136
|
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
|
|
@@ -1408,39 +1138,30 @@ module StateMachines
|
|
|
1408
1138
|
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
|
1409
1139
|
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
|
|
1410
1140
|
# The condition should return or evaluate to true or false.
|
|
1411
|
-
#
|
|
1141
|
+
#
|
|
1412
1142
|
# Examples:
|
|
1413
|
-
#
|
|
1143
|
+
#
|
|
1414
1144
|
# transition :parked => :idling, :on => :ignite, :if => :moving?
|
|
1415
1145
|
# transition :parked => :idling, :on => :ignite, :unless => :stopped?
|
|
1416
1146
|
# transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
|
|
1417
|
-
#
|
|
1147
|
+
#
|
|
1418
1148
|
# transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
|
|
1419
1149
|
# transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
|
|
1420
|
-
#
|
|
1150
|
+
#
|
|
1421
1151
|
# == Order of operations
|
|
1422
|
-
#
|
|
1152
|
+
#
|
|
1423
1153
|
# Transitions are evaluated in the order in which they're defined. As a
|
|
1424
1154
|
# result, if more than one transition applies to a given object, then the
|
|
1425
1155
|
# 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
1156
|
|
|
1436
1157
|
# Creates a callback that will be invoked *before* a transition is
|
|
1437
1158
|
# performed so long as the given requirements match the transition.
|
|
1438
|
-
#
|
|
1159
|
+
#
|
|
1439
1160
|
# == The callback
|
|
1440
|
-
#
|
|
1161
|
+
#
|
|
1441
1162
|
# Callbacks must be defined as either an argument, in the :do option, or
|
|
1442
1163
|
# as a block. For example,
|
|
1443
|
-
#
|
|
1164
|
+
#
|
|
1444
1165
|
# class Vehicle
|
|
1445
1166
|
# state_machine do
|
|
1446
1167
|
# before_transition :set_alarm
|
|
@@ -1452,13 +1173,13 @@ module StateMachines
|
|
|
1452
1173
|
# ...
|
|
1453
1174
|
# end
|
|
1454
1175
|
# end
|
|
1455
|
-
#
|
|
1176
|
+
#
|
|
1456
1177
|
# Notice that the first three callbacks are the same in terms of how the
|
|
1457
1178
|
# methods to invoke are defined. However, using the <tt>:do</tt> can
|
|
1458
1179
|
# provide for a more fluid DSL.
|
|
1459
|
-
#
|
|
1180
|
+
#
|
|
1460
1181
|
# In addition, multiple callbacks can be defined like so:
|
|
1461
|
-
#
|
|
1182
|
+
#
|
|
1462
1183
|
# class Vehicle
|
|
1463
1184
|
# state_machine do
|
|
1464
1185
|
# before_transition :set_alarm, :lock_doors, all => :parked
|
|
@@ -1468,54 +1189,54 @@ module StateMachines
|
|
|
1468
1189
|
# end
|
|
1469
1190
|
# end
|
|
1470
1191
|
# end
|
|
1471
|
-
#
|
|
1192
|
+
#
|
|
1472
1193
|
# Notice that the different ways of configuring methods can be mixed.
|
|
1473
|
-
#
|
|
1194
|
+
#
|
|
1474
1195
|
# == State requirements
|
|
1475
|
-
#
|
|
1196
|
+
#
|
|
1476
1197
|
# Callbacks can require that the machine be transitioning from and to
|
|
1477
1198
|
# specific states. These requirements use a Hash syntax to map beginning
|
|
1478
1199
|
# states to ending states. For example,
|
|
1479
|
-
#
|
|
1200
|
+
#
|
|
1480
1201
|
# before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
|
|
1481
|
-
#
|
|
1202
|
+
#
|
|
1482
1203
|
# In this case, the +set_alarm+ callback will only be called if the machine
|
|
1483
1204
|
# is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
|
|
1484
|
-
#
|
|
1205
|
+
#
|
|
1485
1206
|
# To help define state requirements, a set of helpers are available for
|
|
1486
1207
|
# slightly more complex matching:
|
|
1487
1208
|
# * <tt>all</tt> - Matches every state/event in the machine
|
|
1488
1209
|
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
|
|
1489
1210
|
# * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
|
|
1490
1211
|
# * <tt>same</tt> - Matches the same state being transitioned from
|
|
1491
|
-
#
|
|
1212
|
+
#
|
|
1492
1213
|
# See StateMachines::MatcherHelpers for more information.
|
|
1493
|
-
#
|
|
1214
|
+
#
|
|
1494
1215
|
# Examples:
|
|
1495
|
-
#
|
|
1216
|
+
#
|
|
1496
1217
|
# before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
|
|
1497
1218
|
# before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
|
|
1498
1219
|
# before_transition all => :parked, :do => ... # Matches all states to parked
|
|
1499
1220
|
# before_transition any => same, :do => ... # Matches every loopback
|
|
1500
|
-
#
|
|
1221
|
+
#
|
|
1501
1222
|
# == Event requirements
|
|
1502
|
-
#
|
|
1223
|
+
#
|
|
1503
1224
|
# In addition to state requirements, an event requirement can be defined so
|
|
1504
1225
|
# that the callback is only invoked on specific events using the +on+
|
|
1505
1226
|
# option. This can also use the same matcher helpers as the state
|
|
1506
1227
|
# requirements.
|
|
1507
|
-
#
|
|
1228
|
+
#
|
|
1508
1229
|
# Examples:
|
|
1509
|
-
#
|
|
1230
|
+
#
|
|
1510
1231
|
# before_transition :on => :ignite, :do => ... # Matches only on ignite
|
|
1511
1232
|
# before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
|
|
1512
1233
|
# before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
|
|
1513
|
-
#
|
|
1234
|
+
#
|
|
1514
1235
|
# == Verbose Requirements
|
|
1515
|
-
#
|
|
1236
|
+
#
|
|
1516
1237
|
# Requirements can also be defined using verbose options rather than the
|
|
1517
1238
|
# implicit Hash syntax and helper methods described above.
|
|
1518
|
-
#
|
|
1239
|
+
#
|
|
1519
1240
|
# Configuration options:
|
|
1520
1241
|
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
|
1521
1242
|
# are specified, then all states will match.
|
|
@@ -1526,163 +1247,173 @@ module StateMachines
|
|
|
1526
1247
|
# * <tt>:except_from</tt> - One or more states *not* being transitioned from
|
|
1527
1248
|
# * <tt>:except_to</tt> - One more states *not* being transitioned to
|
|
1528
1249
|
# * <tt>:except_on</tt> - One or more events that *did not* fire the transition
|
|
1529
|
-
#
|
|
1250
|
+
#
|
|
1530
1251
|
# Examples:
|
|
1531
|
-
#
|
|
1252
|
+
#
|
|
1532
1253
|
# before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
|
|
1533
1254
|
# before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
|
|
1534
|
-
#
|
|
1255
|
+
#
|
|
1535
1256
|
# == Conditions
|
|
1536
|
-
#
|
|
1257
|
+
#
|
|
1537
1258
|
# In addition to the state/event requirements, a condition can also be
|
|
1538
1259
|
# defined to help determine whether the callback should be invoked.
|
|
1539
|
-
#
|
|
1260
|
+
#
|
|
1540
1261
|
# Configuration options:
|
|
1541
1262
|
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
|
1542
1263
|
# callback should occur (e.g. :if => :allow_callbacks, or
|
|
1543
1264
|
# :if => lambda {|user| user.signup_step > 2}). The method, proc or string
|
|
1544
|
-
# should return or evaluate to a true or false value.
|
|
1265
|
+
# should return or evaluate to a true or false value.
|
|
1545
1266
|
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
|
1546
1267
|
# callback should not occur (e.g. :unless => :skip_callbacks, or
|
|
1547
1268
|
# :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
|
|
1548
|
-
# string should return or evaluate to a true or false value.
|
|
1549
|
-
#
|
|
1269
|
+
# string should return or evaluate to a true or false value.
|
|
1270
|
+
#
|
|
1550
1271
|
# Examples:
|
|
1551
|
-
#
|
|
1272
|
+
#
|
|
1552
1273
|
# before_transition :parked => :idling, :if => :moving?, :do => ...
|
|
1553
1274
|
# before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
|
|
1554
|
-
#
|
|
1275
|
+
#
|
|
1555
1276
|
# == Accessing the transition
|
|
1556
|
-
#
|
|
1277
|
+
#
|
|
1557
1278
|
# In addition to passing the object being transitioned, the actual
|
|
1558
1279
|
# transition describing the context (e.g. event, from, to) can be accessed
|
|
1559
1280
|
# as well. This additional argument is only passed if the callback allows
|
|
1560
1281
|
# for it.
|
|
1561
|
-
#
|
|
1282
|
+
#
|
|
1562
1283
|
# For example,
|
|
1563
|
-
#
|
|
1284
|
+
#
|
|
1564
1285
|
# class Vehicle
|
|
1565
1286
|
# # Only specifies one parameter (the object being transitioned)
|
|
1566
1287
|
# before_transition all => :parked do |vehicle|
|
|
1567
1288
|
# vehicle.set_alarm
|
|
1568
1289
|
# end
|
|
1569
|
-
#
|
|
1290
|
+
#
|
|
1570
1291
|
# # Specifies 2 parameters (object being transitioned and actual transition)
|
|
1571
1292
|
# before_transition all => :parked do |vehicle, transition|
|
|
1572
1293
|
# vehicle.set_alarm(transition)
|
|
1573
1294
|
# end
|
|
1574
1295
|
# end
|
|
1575
|
-
#
|
|
1296
|
+
#
|
|
1576
1297
|
# *Note* that the object in the callback will only be passed in as an
|
|
1577
1298
|
# argument if callbacks are configured to *not* be bound to the object
|
|
1578
1299
|
# involved. This is the default and may change on a per-integration basis.
|
|
1579
|
-
#
|
|
1300
|
+
#
|
|
1580
1301
|
# See StateMachines::Transition for more information about the
|
|
1581
1302
|
# attributes available on the transition.
|
|
1582
|
-
#
|
|
1303
|
+
#
|
|
1583
1304
|
# == Usage with delegates
|
|
1584
|
-
#
|
|
1305
|
+
#
|
|
1585
1306
|
# As noted above, state_machine uses the callback method's argument list
|
|
1586
1307
|
# arity to determine whether to include the transition in the method call.
|
|
1587
1308
|
# If you're using delegates, such as those defined in ActiveSupport or
|
|
1588
1309
|
# Forwardable, the actual arity of the delegated method gets masked. This
|
|
1589
1310
|
# means that callbacks which reference delegates will always get passed the
|
|
1590
1311
|
# transition as an argument. For example:
|
|
1591
|
-
#
|
|
1312
|
+
#
|
|
1592
1313
|
# class Vehicle
|
|
1593
1314
|
# extend Forwardable
|
|
1594
1315
|
# delegate :refresh => :dashboard
|
|
1595
|
-
#
|
|
1316
|
+
#
|
|
1596
1317
|
# state_machine do
|
|
1597
1318
|
# before_transition :refresh
|
|
1598
1319
|
# ...
|
|
1599
1320
|
# end
|
|
1600
|
-
#
|
|
1321
|
+
#
|
|
1601
1322
|
# def dashboard
|
|
1602
1323
|
# @dashboard ||= Dashboard.new
|
|
1603
1324
|
# end
|
|
1604
1325
|
# end
|
|
1605
|
-
#
|
|
1326
|
+
#
|
|
1606
1327
|
# class Dashboard
|
|
1607
1328
|
# def refresh(transition)
|
|
1608
1329
|
# # ...
|
|
1609
1330
|
# end
|
|
1610
1331
|
# end
|
|
1611
|
-
#
|
|
1332
|
+
#
|
|
1612
1333
|
# In the above example, <tt>Dashboard#refresh</tt> *must* defined a
|
|
1613
1334
|
# +transition+ argument. Otherwise, an +ArgumentError+ exception will get
|
|
1614
1335
|
# raised. The only way around this is to avoid the use of delegates and
|
|
1615
1336
|
# manually define the delegate method so that the correct arity is used.
|
|
1616
|
-
#
|
|
1337
|
+
#
|
|
1617
1338
|
# == Examples
|
|
1618
|
-
#
|
|
1339
|
+
#
|
|
1619
1340
|
# Below is an example of a class with one state machine and various types
|
|
1620
1341
|
# of +before+ transitions defined for it:
|
|
1621
|
-
#
|
|
1342
|
+
#
|
|
1622
1343
|
# class Vehicle
|
|
1623
1344
|
# state_machine do
|
|
1624
1345
|
# # Before all transitions
|
|
1625
1346
|
# before_transition :update_dashboard
|
|
1626
|
-
#
|
|
1347
|
+
#
|
|
1627
1348
|
# # Before specific transition:
|
|
1628
1349
|
# before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
|
|
1629
|
-
#
|
|
1350
|
+
#
|
|
1630
1351
|
# # With conditional callback:
|
|
1631
1352
|
# before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
|
|
1632
|
-
#
|
|
1353
|
+
#
|
|
1633
1354
|
# # Using helpers:
|
|
1634
1355
|
# before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
|
|
1635
1356
|
# ...
|
|
1636
1357
|
# end
|
|
1637
1358
|
# end
|
|
1638
|
-
#
|
|
1359
|
+
#
|
|
1639
1360
|
# As can be seen, any number of transitions can be created using various
|
|
1640
1361
|
# combinations of configuration options.
|
|
1641
|
-
def before_transition(*args, &
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1362
|
+
def before_transition(*args, **options, &)
|
|
1363
|
+
# Extract legacy positional arguments and merge with keyword options
|
|
1364
|
+
parsed_options = parse_callback_arguments(args, options)
|
|
1365
|
+
|
|
1366
|
+
# Only validate callback-specific options, not state transition requirements
|
|
1367
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
|
1368
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
|
1369
|
+
|
|
1370
|
+
add_callback(:before, parsed_options, &)
|
|
1645
1371
|
end
|
|
1646
1372
|
|
|
1647
1373
|
# Creates a callback that will be invoked *after* a transition is
|
|
1648
1374
|
# performed so long as the given requirements match the transition.
|
|
1649
|
-
#
|
|
1375
|
+
#
|
|
1650
1376
|
# See +before_transition+ for a description of the possible configurations
|
|
1651
1377
|
# for defining callbacks.
|
|
1652
|
-
def after_transition(*args, &
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1378
|
+
def after_transition(*args, **options, &)
|
|
1379
|
+
# Extract legacy positional arguments and merge with keyword options
|
|
1380
|
+
parsed_options = parse_callback_arguments(args, options)
|
|
1381
|
+
|
|
1382
|
+
# Only validate callback-specific options, not state transition requirements
|
|
1383
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
|
1384
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
|
1385
|
+
|
|
1386
|
+
add_callback(:after, parsed_options, &)
|
|
1656
1387
|
end
|
|
1657
1388
|
|
|
1658
1389
|
# Creates a callback that will be invoked *around* a transition so long as
|
|
1659
1390
|
# the given requirements match the transition.
|
|
1660
|
-
#
|
|
1391
|
+
#
|
|
1661
1392
|
# == The callback
|
|
1662
|
-
#
|
|
1393
|
+
#
|
|
1663
1394
|
# Around callbacks wrap transitions, executing code both before and after.
|
|
1664
1395
|
# These callbacks are defined in the exact same manner as before / after
|
|
1665
1396
|
# callbacks with the exception that the transition must be yielded to in
|
|
1666
1397
|
# order to finish running it.
|
|
1667
|
-
#
|
|
1398
|
+
#
|
|
1668
1399
|
# If defining +around+ callbacks using blocks, you must yield within the
|
|
1669
1400
|
# transition by directly calling the block (since yielding is not allowed
|
|
1670
1401
|
# within blocks).
|
|
1671
|
-
#
|
|
1402
|
+
#
|
|
1672
1403
|
# For example,
|
|
1673
|
-
#
|
|
1404
|
+
#
|
|
1674
1405
|
# class Vehicle
|
|
1675
1406
|
# state_machine do
|
|
1676
1407
|
# around_transition do |block|
|
|
1677
1408
|
# Benchmark.measure { block.call }
|
|
1678
1409
|
# end
|
|
1679
|
-
#
|
|
1410
|
+
#
|
|
1680
1411
|
# around_transition do |vehicle, block|
|
|
1681
1412
|
# logger.info "vehicle was #{state}..."
|
|
1682
1413
|
# block.call
|
|
1683
1414
|
# logger.info "...and is now #{state}"
|
|
1684
1415
|
# end
|
|
1685
|
-
#
|
|
1416
|
+
#
|
|
1686
1417
|
# around_transition do |vehicle, transition, block|
|
|
1687
1418
|
# logger.info "before #{transition.event}: #{vehicle.state}"
|
|
1688
1419
|
# block.call
|
|
@@ -1690,73 +1421,78 @@ module StateMachines
|
|
|
1690
1421
|
# end
|
|
1691
1422
|
# end
|
|
1692
1423
|
# end
|
|
1693
|
-
#
|
|
1424
|
+
#
|
|
1694
1425
|
# Notice that referencing the block is similar to doing so within an
|
|
1695
1426
|
# actual method definition in that it is always the last argument.
|
|
1696
|
-
#
|
|
1427
|
+
#
|
|
1697
1428
|
# On the other hand, if you're defining +around+ callbacks using method
|
|
1698
1429
|
# references, you can yield like normal:
|
|
1699
|
-
#
|
|
1430
|
+
#
|
|
1700
1431
|
# class Vehicle
|
|
1701
1432
|
# state_machine do
|
|
1702
1433
|
# around_transition :benchmark
|
|
1703
1434
|
# ...
|
|
1704
1435
|
# end
|
|
1705
|
-
#
|
|
1436
|
+
#
|
|
1706
1437
|
# def benchmark
|
|
1707
1438
|
# Benchmark.measure { yield }
|
|
1708
1439
|
# end
|
|
1709
1440
|
# end
|
|
1710
|
-
#
|
|
1441
|
+
#
|
|
1711
1442
|
# See +before_transition+ for a description of the possible configurations
|
|
1712
1443
|
# for defining callbacks.
|
|
1713
|
-
def around_transition(*args, &
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1444
|
+
def around_transition(*args, **options, &)
|
|
1445
|
+
# Extract legacy positional arguments and merge with keyword options
|
|
1446
|
+
parsed_options = parse_callback_arguments(args, options)
|
|
1447
|
+
|
|
1448
|
+
# Only validate callback-specific options, not state transition requirements
|
|
1449
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
|
1450
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
|
1451
|
+
|
|
1452
|
+
add_callback(:around, parsed_options, &)
|
|
1717
1453
|
end
|
|
1718
1454
|
|
|
1719
1455
|
# Creates a callback that will be invoked *after* a transition failures to
|
|
1720
1456
|
# be performed so long as the given requirements match the transition.
|
|
1721
|
-
#
|
|
1457
|
+
#
|
|
1722
1458
|
# See +before_transition+ for a description of the possible configurations
|
|
1723
1459
|
# for defining callbacks. *Note* however that you cannot define the state
|
|
1724
1460
|
# requirements in these callbacks. You may only define event requirements.
|
|
1725
|
-
#
|
|
1461
|
+
#
|
|
1726
1462
|
# = The callback
|
|
1727
|
-
#
|
|
1463
|
+
#
|
|
1728
1464
|
# Failure callbacks get invoked whenever an event fails to execute. This
|
|
1729
1465
|
# can happen when no transition is available, a +before+ callback halts
|
|
1730
1466
|
# execution, or the action associated with this machine fails to succeed.
|
|
1731
1467
|
# In any of these cases, any failure callback that matches the attempted
|
|
1732
1468
|
# transition will be run.
|
|
1733
|
-
#
|
|
1469
|
+
#
|
|
1734
1470
|
# For example,
|
|
1735
|
-
#
|
|
1471
|
+
#
|
|
1736
1472
|
# class Vehicle
|
|
1737
1473
|
# state_machine do
|
|
1738
1474
|
# after_failure do |vehicle, transition|
|
|
1739
1475
|
# logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
|
|
1740
1476
|
# end
|
|
1741
|
-
#
|
|
1477
|
+
#
|
|
1742
1478
|
# after_failure :on => :ignite, :do => :log_ignition_failure
|
|
1743
|
-
#
|
|
1479
|
+
#
|
|
1744
1480
|
# ...
|
|
1745
1481
|
# end
|
|
1746
1482
|
# end
|
|
1747
|
-
def after_failure(*args, &
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1483
|
+
def after_failure(*args, **options, &)
|
|
1484
|
+
# Extract legacy positional arguments and merge with keyword options
|
|
1485
|
+
parsed_options = parse_callback_arguments(args, options)
|
|
1486
|
+
StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless)
|
|
1751
1487
|
|
|
1752
|
-
add_callback(:failure,
|
|
1488
|
+
add_callback(:failure, parsed_options, &)
|
|
1753
1489
|
end
|
|
1754
1490
|
|
|
1755
1491
|
# Generates a list of the possible transition sequences that can be run on
|
|
1756
1492
|
# the given object. These paths can reveal all of the possible states and
|
|
1757
1493
|
# events that can be encountered in the object's state machine based on the
|
|
1758
1494
|
# object's current state.
|
|
1759
|
-
#
|
|
1495
|
+
#
|
|
1760
1496
|
# Configuration options:
|
|
1761
1497
|
# * +from+ - The initial state to start all paths from. By default, this
|
|
1762
1498
|
# is the object's current state.
|
|
@@ -1767,31 +1503,31 @@ module StateMachines
|
|
|
1767
1503
|
# state (if specified) is reached. If this is enabled, then paths can
|
|
1768
1504
|
# continue even after reaching the target state; they will stop when
|
|
1769
1505
|
# reaching the target state a second time.
|
|
1770
|
-
#
|
|
1506
|
+
#
|
|
1771
1507
|
# *Note* that the object is never modified when the list of paths is
|
|
1772
1508
|
# generated.
|
|
1773
|
-
#
|
|
1509
|
+
#
|
|
1774
1510
|
# == Examples
|
|
1775
|
-
#
|
|
1511
|
+
#
|
|
1776
1512
|
# class Vehicle
|
|
1777
1513
|
# state_machine :initial => :parked do
|
|
1778
1514
|
# event :ignite do
|
|
1779
1515
|
# transition :parked => :idling
|
|
1780
1516
|
# end
|
|
1781
|
-
#
|
|
1517
|
+
#
|
|
1782
1518
|
# event :shift_up do
|
|
1783
1519
|
# transition :idling => :first_gear, :first_gear => :second_gear
|
|
1784
1520
|
# end
|
|
1785
|
-
#
|
|
1521
|
+
#
|
|
1786
1522
|
# event :shift_down do
|
|
1787
1523
|
# transition :second_gear => :first_gear, :first_gear => :idling
|
|
1788
1524
|
# end
|
|
1789
1525
|
# end
|
|
1790
1526
|
# end
|
|
1791
|
-
#
|
|
1527
|
+
#
|
|
1792
1528
|
# vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
|
|
1793
1529
|
# vehicle.state # => "parked"
|
|
1794
|
-
#
|
|
1530
|
+
#
|
|
1795
1531
|
# vehicle.state_paths
|
|
1796
1532
|
# # => [
|
|
1797
1533
|
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
|
@@ -1799,37 +1535,33 @@ module StateMachines
|
|
|
1799
1535
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
|
|
1800
1536
|
# # #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
|
|
1801
1537
|
# # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
|
|
1802
|
-
# #
|
|
1538
|
+
# #
|
|
1803
1539
|
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
|
1804
1540
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
|
1805
1541
|
# # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
|
|
1806
1542
|
# # ]
|
|
1807
|
-
#
|
|
1543
|
+
#
|
|
1808
1544
|
# vehicle.state_paths(:from => :parked, :to => :second_gear)
|
|
1809
1545
|
# # => [
|
|
1810
1546
|
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
|
1811
1547
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
|
1812
1548
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
|
|
1813
1549
|
# # ]
|
|
1814
|
-
#
|
|
1550
|
+
#
|
|
1815
1551
|
# In addition to getting the possible paths that can be accessed, you can
|
|
1816
1552
|
# also get summary information about the states / events that can be
|
|
1817
1553
|
# accessed at some point along one of the paths. For example:
|
|
1818
|
-
#
|
|
1554
|
+
#
|
|
1819
1555
|
# # Get the list of states that can be accessed from the current state
|
|
1820
1556
|
# vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
|
|
1821
|
-
#
|
|
1557
|
+
#
|
|
1822
1558
|
# # Get the list of events that can be accessed from the current state
|
|
1823
1559
|
# vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
|
|
1824
|
-
def paths_for(object, requirements = {})
|
|
1825
|
-
PathCollection.new(object, self, requirements)
|
|
1826
|
-
end
|
|
1827
1560
|
|
|
1828
1561
|
# Marks the given object as invalid with the given message.
|
|
1829
|
-
#
|
|
1562
|
+
#
|
|
1830
1563
|
# By default, this is a no-op.
|
|
1831
|
-
def invalidate(_object, _attribute, _message, _values = [])
|
|
1832
|
-
end
|
|
1564
|
+
def invalidate(_object, _attribute, _message, _values = []); end
|
|
1833
1565
|
|
|
1834
1566
|
# Gets a description of the errors for the given object. This is used to
|
|
1835
1567
|
# provide more detailed information when an InvalidTransition exception is
|
|
@@ -1839,328 +1571,63 @@ module StateMachines
|
|
|
1839
1571
|
end
|
|
1840
1572
|
|
|
1841
1573
|
# Resets any errors previously added when invalidating the given object.
|
|
1842
|
-
#
|
|
1574
|
+
#
|
|
1843
1575
|
# By default, this is a no-op.
|
|
1844
|
-
def reset(_object)
|
|
1845
|
-
end
|
|
1576
|
+
def reset(_object); end
|
|
1846
1577
|
|
|
1847
1578
|
# Generates the message to use when invalidating the given object after
|
|
1848
1579
|
# failing to transition on a specific event
|
|
1849
1580
|
def generate_message(name, values = [])
|
|
1850
|
-
message =
|
|
1581
|
+
message = @messages[name] || self.class.default_messages[name]
|
|
1851
1582
|
|
|
1852
1583
|
# Check whether there are actually any values to interpolate to avoid
|
|
1853
1584
|
# any warnings
|
|
1854
1585
|
if message.scan(/%./).any? { |match| match != '%%' }
|
|
1855
|
-
message % values.map
|
|
1586
|
+
message % values.map(&:last)
|
|
1856
1587
|
else
|
|
1857
1588
|
message
|
|
1858
1589
|
end
|
|
1859
1590
|
end
|
|
1860
1591
|
|
|
1861
1592
|
# Runs a transaction, rolling back any changes if the yielded block fails.
|
|
1862
|
-
#
|
|
1593
|
+
#
|
|
1863
1594
|
# This is only applicable to integrations that involve databases. By
|
|
1864
1595
|
# default, this will not run any transactions since the changes aren't
|
|
1865
1596
|
# taking place within the context of a database.
|
|
1866
|
-
def within_transaction(object)
|
|
1597
|
+
def within_transaction(object, &)
|
|
1867
1598
|
if use_transactions
|
|
1868
|
-
transaction(object)
|
|
1599
|
+
transaction(object, &)
|
|
1869
1600
|
else
|
|
1870
1601
|
yield
|
|
1871
1602
|
end
|
|
1872
1603
|
end
|
|
1873
1604
|
|
|
1605
|
+
def renderer
|
|
1606
|
+
self.class.renderer
|
|
1607
|
+
end
|
|
1874
1608
|
|
|
1875
|
-
def draw(
|
|
1876
|
-
|
|
1609
|
+
def draw(**)
|
|
1610
|
+
renderer.draw_machine(self, **)
|
|
1877
1611
|
end
|
|
1878
1612
|
|
|
1879
1613
|
# Determines whether an action hook was defined for firing attribute-based
|
|
1880
1614
|
# event transitions when the configured action gets called.
|
|
1881
1615
|
def action_hook?(self_only = false)
|
|
1882
|
-
@action_hook_defined || !self_only && owner_class.state_machines.any? { |
|
|
1616
|
+
@action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) })
|
|
1883
1617
|
end
|
|
1884
1618
|
|
|
1885
1619
|
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
1620
|
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
|
1621
|
+
# Runs additional initialization hooks. By default, this is a no-op.
|
|
1622
|
+
def after_initialize; end
|
|
2047
1623
|
|
|
2048
1624
|
# Determines whether there's already a helper method defined within the
|
|
2049
1625
|
# given scope. This is true only if one of the owner's ancestors defines
|
|
2050
1626
|
# the method and is further along in the ancestor chain than this
|
|
2051
1627
|
# 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
1628
|
|
|
2162
1629
|
# Always yields
|
|
2163
|
-
def transaction(
|
|
1630
|
+
def transaction(_object)
|
|
2164
1631
|
yield
|
|
2165
1632
|
end
|
|
2166
1633
|
|
|
@@ -2175,60 +1642,5 @@ module StateMachines
|
|
|
2175
1642
|
def owner_class_attribute_default_matches?(state)
|
|
2176
1643
|
state.matches?(owner_class_attribute_default)
|
|
2177
1644
|
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
1645
|
end
|
|
2234
1646
|
end
|