state_machines 0.5.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +146 -86
- 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 +197 -39
- data/lib/state_machines/event.rb +77 -58
- data/lib/state_machines/event_collection.rb +49 -39
- data/lib/state_machines/extensions.rb +6 -4
- data/lib/state_machines/helper_module.rb +4 -2
- data/lib/state_machines/integrations/base.rb +3 -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 +97 -0
- data/lib/state_machines/machine/configuration.rb +134 -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 +425 -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 +896 -0
- data/lib/state_machines/transition.rb +215 -199
- data/lib/state_machines/transition_collection.rb +187 -170
- 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,102 @@ module StateMachines
|
|
500
477
|
attr_reader :use_transactions
|
501
478
|
|
502
479
|
# Creates a new state machine for the given attribute
|
503
|
-
def initialize(owner_class, *args, &block)
|
504
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
505
|
-
options.assert_valid_keys(:attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
506
|
-
|
507
|
-
# Find an integration that matches this machine's owner class
|
508
|
-
if options.include?(:integration)
|
509
|
-
@integration = options[:integration] && StateMachines::Integrations.find_by_name(options[:integration])
|
510
|
-
else
|
511
|
-
@integration = StateMachines::Integrations.match(owner_class)
|
512
|
-
end
|
513
|
-
|
514
|
-
if @integration
|
515
|
-
extend @integration
|
516
|
-
options = (@integration.defaults || {}).merge(options)
|
517
|
-
end
|
518
|
-
|
519
|
-
# Add machine-wide defaults
|
520
|
-
options = {:use_transactions => true, :initialize => true}.merge(options)
|
521
|
-
|
522
|
-
# Set machine configuration
|
523
|
-
@name = args.first || :state
|
524
|
-
@attribute = options[:attribute] || @name
|
525
|
-
@events = EventCollection.new(self)
|
526
|
-
@states = StateCollection.new(self)
|
527
|
-
@callbacks = {:before => [], :after => [], :failure => []}
|
528
|
-
@namespace = options[:namespace]
|
529
|
-
@messages = options[:messages] || {}
|
530
|
-
@action = options[:action]
|
531
|
-
@use_transactions = options[:use_transactions]
|
532
|
-
@initialize_state = options[:initialize]
|
533
|
-
@action_hook_defined = false
|
534
|
-
self.owner_class = owner_class
|
535
|
-
|
536
|
-
# Merge with sibling machine configurations
|
537
|
-
add_sibling_machine_configs
|
538
|
-
|
539
|
-
# Define class integration
|
540
|
-
define_helpers
|
541
|
-
define_scopes(options[:plural])
|
542
|
-
after_initialize
|
543
|
-
|
544
|
-
# Evaluate DSL
|
545
|
-
instance_eval(&block) if block_given?
|
546
|
-
self.initial_state = options[:initial] unless sibling_machines.any?
|
547
|
-
end
|
548
|
-
|
549
|
-
# Creates a copy of this machine in addition to copies of each associated
|
550
|
-
# event/states/callback, so that the modifications to those collections do
|
551
|
-
# not affect the original machine.
|
552
|
-
def initialize_copy(orig) #:nodoc:
|
553
|
-
super
|
554
|
-
|
555
|
-
@events = @events.dup
|
556
|
-
@events.machine = self
|
557
|
-
@states = @states.dup
|
558
|
-
@states.machine = self
|
559
|
-
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
|
560
|
-
end
|
561
|
-
|
562
|
-
# Sets the class which is the owner of this state machine. Any methods
|
563
|
-
# generated by states, events, or other parts of the machine will be defined
|
564
|
-
# on the given owner class.
|
565
|
-
def owner_class=(klass)
|
566
|
-
@owner_class = klass
|
567
|
-
|
568
|
-
# Create modules for extending the class with state/event-specific methods
|
569
|
-
@helper_modules = helper_modules = {:instance => HelperModule.new(self, :instance), :class => HelperModule.new(self, :class)}
|
570
|
-
owner_class.class_eval do
|
571
|
-
extend helper_modules[:class]
|
572
|
-
include helper_modules[:instance]
|
573
|
-
end
|
574
|
-
|
575
|
-
# Add class-/instance-level methods to the owner class for state initialization
|
576
|
-
unless owner_class < StateMachines::InstanceMethods
|
577
|
-
owner_class.class_eval do
|
578
|
-
extend StateMachines::ClassMethods
|
579
|
-
include StateMachines::InstanceMethods
|
580
|
-
end
|
581
|
-
|
582
|
-
define_state_initializer if @initialize_state
|
583
|
-
end
|
584
|
-
|
585
|
-
# Record this machine as matched to the name in the current owner class.
|
586
|
-
# This will override any machines mapped to the same name in any superclasses.
|
587
|
-
owner_class.state_machines[name] = self
|
588
|
-
end
|
589
|
-
|
590
|
-
# Sets the initial state of the machine. This can be either the static name
|
591
|
-
# of a state or a lambda block which determines the initial state at
|
592
|
-
# creation time.
|
593
|
-
def initial_state=(new_initial_state)
|
594
|
-
@initial_state = new_initial_state
|
595
|
-
add_states([@initial_state]) unless dynamic_initial_state?
|
596
|
-
|
597
|
-
# Update all states to reflect the new initial state
|
598
|
-
states.each { |state| state.initial = (state.name == @initial_state) }
|
599
|
-
|
600
|
-
# Output a warning if there are conflicting initial states for the machine's
|
601
|
-
# attribute
|
602
|
-
initial_state = states.detect { |state| state.initial }
|
603
|
-
if !owner_class_attribute_default.nil? && (dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state))
|
604
|
-
warn(
|
605
|
-
"Both #{owner_class.name} and its #{name.inspect} machine have defined "\
|
606
|
-
"a different default for \"#{attribute}\". Use only one or the other for "\
|
607
|
-
"defining defaults to avoid unexpected behaviors."
|
608
|
-
)
|
609
|
-
end
|
610
|
-
end
|
611
480
|
|
612
481
|
# Gets the initial state of the machine for the given object. If a dynamic
|
613
482
|
# initial state was configured for this machine, then the object will be
|
614
483
|
# passed into the lambda block to help determine the actual state.
|
615
|
-
#
|
484
|
+
#
|
616
485
|
# == Examples
|
617
|
-
#
|
486
|
+
#
|
618
487
|
# With a static initial state:
|
619
|
-
#
|
488
|
+
#
|
620
489
|
# class Vehicle
|
621
490
|
# state_machine :initial => :parked do
|
622
491
|
# ...
|
623
492
|
# end
|
624
493
|
# end
|
625
|
-
#
|
494
|
+
#
|
626
495
|
# vehicle = Vehicle.new
|
627
496
|
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=true>
|
628
|
-
#
|
497
|
+
#
|
629
498
|
# With a dynamic initial state:
|
630
|
-
#
|
499
|
+
#
|
631
500
|
# class Vehicle
|
632
501
|
# attr_accessor :force_idle
|
633
|
-
#
|
502
|
+
#
|
634
503
|
# state_machine :initial => lambda {|vehicle| vehicle.force_idle ? :idling : :parked} do
|
635
504
|
# ...
|
636
505
|
# end
|
637
506
|
# end
|
638
|
-
#
|
507
|
+
#
|
639
508
|
# vehicle = Vehicle.new
|
640
|
-
#
|
509
|
+
#
|
641
510
|
# vehicle.force_idle = true
|
642
511
|
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:idling value="idling" initial=false>
|
643
|
-
#
|
512
|
+
#
|
644
513
|
# vehicle.force_idle = false
|
645
514
|
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachines::State name=:parked value="parked" initial=false>
|
646
|
-
def initial_state(object)
|
647
|
-
states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
|
648
|
-
end
|
649
|
-
|
650
|
-
# Whether a dynamic initial state is being used in the machine
|
651
|
-
def dynamic_initial_state?
|
652
|
-
instance_variable_defined?('@initial_state') && @initial_state.is_a?(Proc)
|
653
|
-
end
|
654
|
-
|
655
|
-
# Initializes the state on the given object. Initial values are only set if
|
656
|
-
# the machine's attribute hasn't been previously initialized.
|
657
|
-
#
|
658
|
-
# Configuration options:
|
659
|
-
# * <tt>:force</tt> - Whether to initialize the state regardless of its
|
660
|
-
# current value
|
661
|
-
# * <tt>:to</tt> - A hash to set the initial value in instead of writing
|
662
|
-
# directly to the object
|
663
|
-
def initialize_state(object, options = {})
|
664
|
-
state = initial_state(object)
|
665
|
-
if state && (options[:force] || initialize_state?(object))
|
666
|
-
value = state.value
|
667
|
-
|
668
|
-
if hash = options[:to]
|
669
|
-
hash[attribute.to_s] = value
|
670
|
-
else
|
671
|
-
write(object, :state, value)
|
672
|
-
end
|
673
|
-
end
|
674
|
-
end
|
675
|
-
|
676
|
-
# Gets the actual name of the attribute on the machine's owner class that
|
677
|
-
# stores data with the given name.
|
678
|
-
def attribute(name = :state)
|
679
|
-
name == :state ? @attribute : :"#{self.name}_#{name}"
|
680
|
-
end
|
681
515
|
|
682
516
|
# Defines a new helper method in an instance or class scope with the given
|
683
517
|
# name. If the method is already defined in the scope, then this will not
|
684
518
|
# override it.
|
685
|
-
#
|
519
|
+
#
|
686
520
|
# If passing in a block, there are two side effects to be aware of
|
687
521
|
# 1. The method cannot be chained, meaning that the block cannot call +super+
|
688
522
|
# 2. If the method is already defined in an ancestor, then it will not get
|
689
523
|
# overridden and a warning will be output.
|
690
|
-
#
|
524
|
+
#
|
691
525
|
# Example:
|
692
|
-
#
|
526
|
+
#
|
693
527
|
# # Instance helper
|
694
528
|
# machine.define_helper(:instance, :state_name) do |machine, object|
|
695
529
|
# machine.states.match(object).name
|
696
530
|
# end
|
697
|
-
#
|
531
|
+
#
|
698
532
|
# # Class helper
|
699
533
|
# machine.define_helper(:class, :state_machine_name) do |machine, klass|
|
700
534
|
# "State"
|
701
535
|
# end
|
702
|
-
#
|
536
|
+
#
|
703
537
|
# You can also define helpers using string evaluation like so:
|
704
|
-
#
|
538
|
+
#
|
705
539
|
# # Instance helper
|
706
540
|
# machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
707
541
|
# def state_name
|
708
542
|
# self.class.state_machine(:state).states.match(self).name
|
709
543
|
# end
|
710
544
|
# end_eval
|
711
|
-
#
|
545
|
+
#
|
712
546
|
# # Class helper
|
713
547
|
# machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
|
714
548
|
# def state_machine_name
|
715
549
|
# "State"
|
716
550
|
# end
|
717
551
|
# end_eval
|
718
|
-
def define_helper(scope, method,
|
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
|
-
|
568
|
+
# Validate string input before eval if method is a string
|
569
|
+
validate_eval_string(method) if method.is_a?(String)
|
570
|
+
helper_module.class_eval(method, __FILE__, __LINE__)
|
735
571
|
end
|
736
572
|
end
|
737
573
|
|
738
574
|
# Customizes the definition of one or more states in the machine.
|
739
|
-
#
|
575
|
+
#
|
740
576
|
# Configuration options:
|
741
577
|
# * <tt>:value</tt> - The actual value to store when an object transitions
|
742
578
|
# to the state. Default is the name (stringified).
|
@@ -748,13 +584,13 @@ module StateMachines
|
|
748
584
|
# * <tt>:human_name</tt> - The human-readable version of this state's name.
|
749
585
|
# By default, this is either defined by the integration or stringifies the
|
750
586
|
# name and converts underscores to spaces.
|
751
|
-
#
|
587
|
+
#
|
752
588
|
# == Customizing the stored value
|
753
|
-
#
|
589
|
+
#
|
754
590
|
# Whenever a state is automatically discovered in the state machine, its
|
755
591
|
# default value is assumed to be the stringified version of the name. For
|
756
592
|
# example,
|
757
|
-
#
|
593
|
+
#
|
758
594
|
# class Vehicle
|
759
595
|
# state_machine :initial => :parked do
|
760
596
|
# event :ignite do
|
@@ -762,124 +598,124 @@ module StateMachines
|
|
762
598
|
# end
|
763
599
|
# end
|
764
600
|
# end
|
765
|
-
#
|
601
|
+
#
|
766
602
|
# In the above state machine, there are two states automatically discovered:
|
767
603
|
# :parked and :idling. These states, by default, will store their stringified
|
768
604
|
# equivalents when an object moves into that state (e.g. "parked" / "idling").
|
769
|
-
#
|
605
|
+
#
|
770
606
|
# For legacy systems or when tying state machines into existing frameworks,
|
771
607
|
# it's oftentimes necessary to need to store a different value for a state
|
772
608
|
# than the default. In order to continue taking advantage of an expressive
|
773
609
|
# state machine and helper methods, every defined state can be re-configured
|
774
610
|
# with a custom stored value. For example,
|
775
|
-
#
|
611
|
+
#
|
776
612
|
# class Vehicle
|
777
613
|
# state_machine :initial => :parked do
|
778
614
|
# event :ignite do
|
779
615
|
# transition :parked => :idling
|
780
616
|
# end
|
781
|
-
#
|
617
|
+
#
|
782
618
|
# state :idling, :value => 'IDLING'
|
783
619
|
# state :parked, :value => 'PARKED
|
784
620
|
# end
|
785
621
|
# end
|
786
|
-
#
|
622
|
+
#
|
787
623
|
# This is also useful if being used in association with a database and,
|
788
624
|
# instead of storing the state name in a column, you want to store the
|
789
625
|
# state's foreign key:
|
790
|
-
#
|
626
|
+
#
|
791
627
|
# class VehicleState < ActiveRecord::Base
|
792
628
|
# end
|
793
|
-
#
|
629
|
+
#
|
794
630
|
# class Vehicle < ActiveRecord::Base
|
795
631
|
# state_machine :attribute => :state_id, :initial => :parked do
|
796
632
|
# event :ignite do
|
797
633
|
# transition :parked => :idling
|
798
634
|
# end
|
799
|
-
#
|
635
|
+
#
|
800
636
|
# states.each do |state|
|
801
637
|
# self.state(state.name, :value => lambda { VehicleState.find_by_name(state.name.to_s).id }, :cache => true)
|
802
638
|
# end
|
803
639
|
# end
|
804
640
|
# end
|
805
|
-
#
|
641
|
+
#
|
806
642
|
# In the above example, each known state is configured to store it's
|
807
643
|
# associated database id in the +state_id+ attribute. Also, notice that a
|
808
644
|
# lambda block is used to define the state's value. This is required in
|
809
645
|
# situations (like testing) where the model is loaded without any existing
|
810
646
|
# data (i.e. no VehicleState records available).
|
811
|
-
#
|
647
|
+
#
|
812
648
|
# One caveat to the above example is to keep performance in mind. To avoid
|
813
649
|
# constant db hits for looking up the VehicleState ids, the value is cached
|
814
650
|
# by specifying the <tt>:cache</tt> option. Alternatively, a custom
|
815
651
|
# caching strategy can be used like so:
|
816
|
-
#
|
652
|
+
#
|
817
653
|
# class VehicleState < ActiveRecord::Base
|
818
654
|
# cattr_accessor :cache_store
|
819
655
|
# self.cache_store = ActiveSupport::Cache::MemoryStore.new
|
820
|
-
#
|
656
|
+
#
|
821
657
|
# def self.find_by_name(name)
|
822
658
|
# cache_store.fetch(name) { find(:first, :conditions => {:name => name}) }
|
823
659
|
# end
|
824
660
|
# end
|
825
|
-
#
|
661
|
+
#
|
826
662
|
# === Dynamic values
|
827
|
-
#
|
663
|
+
#
|
828
664
|
# In addition to customizing states with other value types, lambda blocks
|
829
665
|
# can also be specified to allow for a state's value to be determined
|
830
666
|
# dynamically at runtime. For example,
|
831
|
-
#
|
667
|
+
#
|
832
668
|
# class Vehicle
|
833
669
|
# state_machine :purchased_at, :initial => :available do
|
834
670
|
# event :purchase do
|
835
671
|
# transition all => :purchased
|
836
672
|
# end
|
837
|
-
#
|
673
|
+
#
|
838
674
|
# event :restock do
|
839
675
|
# transition all => :available
|
840
676
|
# end
|
841
|
-
#
|
677
|
+
#
|
842
678
|
# state :available, :value => nil
|
843
679
|
# state :purchased, :if => lambda {|value| !value.nil?}, :value => lambda {Time.now}
|
844
680
|
# end
|
845
681
|
# end
|
846
|
-
#
|
682
|
+
#
|
847
683
|
# In the above definition, the <tt>:purchased</tt> state is customized with
|
848
684
|
# both a dynamic value *and* a value matcher.
|
849
|
-
#
|
685
|
+
#
|
850
686
|
# When an object transitions to the purchased state, the value's lambda
|
851
687
|
# block will be called. This will get the current time and store it in the
|
852
688
|
# object's +purchased_at+ attribute.
|
853
|
-
#
|
689
|
+
#
|
854
690
|
# *Note* that the custom matcher is very important here. Since there's no
|
855
691
|
# way for the state machine to figure out an object's state when it's set to
|
856
692
|
# a runtime value, it must be explicitly defined. If the <tt>:if</tt> option
|
857
693
|
# were not configured for the state, then an ArgumentError exception would
|
858
694
|
# be raised at runtime, indicating that the state machine could not figure
|
859
695
|
# out what the current state of the object was.
|
860
|
-
#
|
696
|
+
#
|
861
697
|
# == Behaviors
|
862
|
-
#
|
698
|
+
#
|
863
699
|
# Behaviors define a series of methods to mixin with objects when the current
|
864
700
|
# state matches the given one(s). This allows instance methods to behave
|
865
701
|
# a specific way depending on what the value of the object's state is.
|
866
|
-
#
|
702
|
+
#
|
867
703
|
# For example,
|
868
|
-
#
|
704
|
+
#
|
869
705
|
# class Vehicle
|
870
706
|
# attr_accessor :driver
|
871
707
|
# attr_accessor :passenger
|
872
|
-
#
|
708
|
+
#
|
873
709
|
# state_machine :initial => :parked do
|
874
710
|
# event :ignite do
|
875
711
|
# transition :parked => :idling
|
876
712
|
# end
|
877
|
-
#
|
713
|
+
#
|
878
714
|
# state :parked do
|
879
715
|
# def speed
|
880
716
|
# 0
|
881
717
|
# end
|
882
|
-
#
|
718
|
+
#
|
883
719
|
# def rotate_driver
|
884
720
|
# driver = self.driver
|
885
721
|
# self.driver = passenger
|
@@ -887,97 +723,97 @@ module StateMachines
|
|
887
723
|
# true
|
888
724
|
# end
|
889
725
|
# end
|
890
|
-
#
|
726
|
+
#
|
891
727
|
# state :idling, :first_gear do
|
892
728
|
# def speed
|
893
729
|
# 20
|
894
730
|
# end
|
895
|
-
#
|
731
|
+
#
|
896
732
|
# def rotate_driver
|
897
733
|
# self.state = 'parked'
|
898
734
|
# rotate_driver
|
899
735
|
# end
|
900
736
|
# end
|
901
|
-
#
|
737
|
+
#
|
902
738
|
# other_states :backing_up
|
903
739
|
# end
|
904
740
|
# end
|
905
|
-
#
|
741
|
+
#
|
906
742
|
# In the above example, there are two dynamic behaviors defined for the
|
907
743
|
# class:
|
908
744
|
# * +speed+
|
909
745
|
# * +rotate_driver+
|
910
|
-
#
|
746
|
+
#
|
911
747
|
# Each of these behaviors are instance methods on the Vehicle class. However,
|
912
748
|
# which method actually gets invoked is based on the current state of the
|
913
749
|
# object. Using the above class as the example:
|
914
|
-
#
|
750
|
+
#
|
915
751
|
# vehicle = Vehicle.new
|
916
752
|
# vehicle.driver = 'John'
|
917
753
|
# vehicle.passenger = 'Jane'
|
918
|
-
#
|
754
|
+
#
|
919
755
|
# # Behaviors in the "parked" state
|
920
756
|
# vehicle.state # => "parked"
|
921
757
|
# vehicle.speed # => 0
|
922
758
|
# vehicle.rotate_driver # => true
|
923
759
|
# vehicle.driver # => "Jane"
|
924
760
|
# vehicle.passenger # => "John"
|
925
|
-
#
|
761
|
+
#
|
926
762
|
# vehicle.ignite # => true
|
927
|
-
#
|
763
|
+
#
|
928
764
|
# # Behaviors in the "idling" state
|
929
765
|
# vehicle.state # => "idling"
|
930
766
|
# vehicle.speed # => 20
|
931
767
|
# vehicle.rotate_driver # => true
|
932
768
|
# vehicle.driver # => "John"
|
933
769
|
# vehicle.passenger # => "Jane"
|
934
|
-
#
|
770
|
+
#
|
935
771
|
# As can be seen, both the +speed+ and +rotate_driver+ instance method
|
936
772
|
# implementations changed how they behave based on what the current state
|
937
773
|
# of the vehicle was.
|
938
|
-
#
|
774
|
+
#
|
939
775
|
# === Invalid behaviors
|
940
|
-
#
|
776
|
+
#
|
941
777
|
# If a specific behavior has not been defined for a state, then a
|
942
778
|
# NoMethodError exception will be raised, indicating that that method would
|
943
779
|
# not normally exist for an object with that state.
|
944
|
-
#
|
780
|
+
#
|
945
781
|
# Using the example from before:
|
946
|
-
#
|
782
|
+
#
|
947
783
|
# vehicle = Vehicle.new
|
948
784
|
# vehicle.state = 'backing_up'
|
949
785
|
# vehicle.speed # => NoMethodError: undefined method 'speed' for #<Vehicle:0xb7d296ac> in state "backing_up"
|
950
|
-
#
|
786
|
+
#
|
951
787
|
# === Using matchers
|
952
|
-
#
|
788
|
+
#
|
953
789
|
# The +all+ / +any+ matchers can be used to easily define behaviors for a
|
954
790
|
# group of states. Note, however, that you cannot use these matchers to
|
955
791
|
# set configurations for states. Behaviors using these matchers can be
|
956
792
|
# defined at any point in the state machine and will always get applied to
|
957
793
|
# the proper states.
|
958
|
-
#
|
794
|
+
#
|
959
795
|
# For example:
|
960
|
-
#
|
796
|
+
#
|
961
797
|
# state_machine :initial => :parked do
|
962
798
|
# ...
|
963
|
-
#
|
799
|
+
#
|
964
800
|
# state all - [:parked, :idling, :stalled] do
|
965
801
|
# validates_presence_of :speed
|
966
|
-
#
|
802
|
+
#
|
967
803
|
# def speed
|
968
804
|
# gear * 10
|
969
805
|
# end
|
970
806
|
# end
|
971
807
|
# end
|
972
|
-
#
|
808
|
+
#
|
973
809
|
# == State-aware class methods
|
974
|
-
#
|
810
|
+
#
|
975
811
|
# In addition to defining scopes for instance methods that are state-aware,
|
976
812
|
# the same can be done for certain types of class methods.
|
977
|
-
#
|
813
|
+
#
|
978
814
|
# Some libraries have support for class-level methods that only run certain
|
979
815
|
# behaviors based on a conditions hash passed in. For example:
|
980
|
-
#
|
816
|
+
#
|
981
817
|
# class Vehicle < ActiveRecord::Base
|
982
818
|
# state_machine do
|
983
819
|
# ...
|
@@ -987,110 +823,35 @@ module StateMachines
|
|
987
823
|
# end
|
988
824
|
# end
|
989
825
|
# end
|
990
|
-
#
|
826
|
+
#
|
991
827
|
# In the above ActiveRecord model, two validations have been defined which
|
992
828
|
# will *only* run when the Vehicle object is in one of the three states:
|
993
829
|
# +first_gear+, +second_gear+, or +third_gear. Notice, also, that if/unless
|
994
830
|
# conditions can continue to be used.
|
995
|
-
#
|
831
|
+
#
|
996
832
|
# This functionality is not library-specific and can work for any class-level
|
997
833
|
# method that is defined like so:
|
998
|
-
#
|
834
|
+
#
|
999
835
|
# def validates_presence_of(attribute, options = {})
|
1000
836
|
# ...
|
1001
837
|
# end
|
1002
|
-
#
|
838
|
+
#
|
1003
839
|
# The minimum requirement is that the last argument in the method be an
|
1004
840
|
# options hash which contains at least <tt>:if</tt> condition support.
|
1005
|
-
def state(*names, &block)
|
1006
|
-
options = names.last.is_a?(Hash) ? names.pop : {}
|
1007
|
-
options.assert_valid_keys(:value, :cache, :if, :human_name)
|
1008
|
-
|
1009
|
-
# Store the context so that it can be used for / matched against any state
|
1010
|
-
# that gets added
|
1011
|
-
@states.context(names, &block) if block_given?
|
1012
|
-
|
1013
|
-
if names.first.is_a?(Matcher)
|
1014
|
-
# Add any states referenced in the matcher. When matchers are used,
|
1015
|
-
# states are not allowed to be configured.
|
1016
|
-
raise ArgumentError, "Cannot configure states when using matchers (using #{options.inspect})" if options.any?
|
1017
|
-
states = add_states(names.first.values)
|
1018
|
-
else
|
1019
|
-
states = add_states(names)
|
1020
|
-
|
1021
|
-
# Update the configuration for the state(s)
|
1022
|
-
states.each do |state|
|
1023
|
-
if options.include?(:value)
|
1024
|
-
state.value = options[:value]
|
1025
|
-
self.states.update(state)
|
1026
|
-
end
|
1027
|
-
|
1028
|
-
state.human_name = options[:human_name] if options.include?(:human_name)
|
1029
|
-
state.cache = options[:cache] if options.include?(:cache)
|
1030
|
-
state.matcher = options[:if] if options.include?(:if)
|
1031
|
-
end
|
1032
|
-
end
|
1033
|
-
|
1034
|
-
states.length == 1 ? states.first : states
|
1035
|
-
end
|
1036
|
-
|
1037
|
-
alias_method :other_states, :state
|
1038
|
-
|
1039
|
-
# Gets the current value stored in the given object's attribute.
|
1040
|
-
#
|
1041
|
-
# For example,
|
1042
|
-
#
|
1043
|
-
# class Vehicle
|
1044
|
-
# state_machine :initial => :parked do
|
1045
|
-
# ...
|
1046
|
-
# end
|
1047
|
-
# end
|
1048
|
-
#
|
1049
|
-
# vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
|
1050
|
-
# Vehicle.state_machine.read(vehicle, :state) # => "parked" # Equivalent to vehicle.state
|
1051
|
-
# Vehicle.state_machine.read(vehicle, :event) # => nil # Equivalent to vehicle.state_event
|
1052
|
-
def read(object, attribute, ivar = false)
|
1053
|
-
attribute = self.attribute(attribute)
|
1054
|
-
if ivar
|
1055
|
-
object.instance_variable_defined?("@#{attribute}") ? object.instance_variable_get("@#{attribute}") : nil
|
1056
|
-
else
|
1057
|
-
object.send(attribute)
|
1058
|
-
end
|
1059
|
-
end
|
1060
|
-
|
1061
|
-
# Sets a new value in the given object's attribute.
|
1062
|
-
#
|
1063
|
-
# For example,
|
1064
|
-
#
|
1065
|
-
# class Vehicle
|
1066
|
-
# state_machine :initial => :parked do
|
1067
|
-
# ...
|
1068
|
-
# end
|
1069
|
-
# end
|
1070
|
-
#
|
1071
|
-
# vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
|
1072
|
-
# Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
|
1073
|
-
# Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
|
1074
|
-
# vehicle.state # => "idling"
|
1075
|
-
# vehicle.event # => "park"
|
1076
|
-
def write(object, attribute, value, ivar = false)
|
1077
|
-
attribute = self.attribute(attribute)
|
1078
|
-
ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
|
1079
|
-
end
|
1080
841
|
|
1081
842
|
# Defines one or more events for the machine and the transitions that can
|
1082
843
|
# be performed when those events are run.
|
1083
|
-
#
|
844
|
+
#
|
1084
845
|
# This method is also aliased as +on+ for improved compatibility with
|
1085
846
|
# using a domain-specific language.
|
1086
|
-
#
|
847
|
+
#
|
1087
848
|
# Configuration options:
|
1088
849
|
# * <tt>:human_name</tt> - The human-readable version of this event's name.
|
1089
850
|
# By default, this is either defined by the integration or stringifies the
|
1090
851
|
# name and converts underscores to spaces.
|
1091
|
-
#
|
852
|
+
#
|
1092
853
|
# == Instance methods
|
1093
|
-
#
|
854
|
+
#
|
1094
855
|
# The following instance methods are generated when a new event is defined
|
1095
856
|
# (the "park" event is used as an example):
|
1096
857
|
# * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
|
@@ -1114,13 +875,13 @@ module StateMachines
|
|
1114
875
|
# object or nil if no transitions can be performed. Like <tt>can_park?</tt>
|
1115
876
|
# this will also *not* run validations or callbacks. It will only
|
1116
877
|
# determine if the state machine defines a valid transition for the event.
|
1117
|
-
#
|
878
|
+
#
|
1118
879
|
# With a namespace of "car", the above names map to the following methods:
|
1119
880
|
# * <tt>can_park_car?</tt>
|
1120
881
|
# * <tt>park_car_transition</tt>
|
1121
882
|
# * <tt>park_car</tt>
|
1122
883
|
# * <tt>park_car!</tt>
|
1123
|
-
#
|
884
|
+
#
|
1124
885
|
# The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
|
1125
886
|
# optional set of requirements for determining what transitions are available
|
1126
887
|
# for the current object. These requirements include:
|
@@ -1130,58 +891,58 @@ module StateMachines
|
|
1130
891
|
# specified, then this will match any to state.
|
1131
892
|
# * <tt>:guard</tt> - Whether to guard transitions with the if/unless
|
1132
893
|
# conditionals defined for each one. Default is true.
|
1133
|
-
#
|
894
|
+
#
|
1134
895
|
# == Defining transitions
|
1135
|
-
#
|
896
|
+
#
|
1136
897
|
# +event+ requires a block which allows you to define the possible
|
1137
898
|
# transitions that can happen as a result of that event. For example,
|
1138
|
-
#
|
899
|
+
#
|
1139
900
|
# event :park, :stop do
|
1140
901
|
# transition :idling => :parked
|
1141
902
|
# end
|
1142
|
-
#
|
903
|
+
#
|
1143
904
|
# event :first_gear do
|
1144
905
|
# transition :parked => :first_gear, :if => :seatbelt_on?
|
1145
906
|
# transition :parked => same # Allow to loopback if seatbelt is off
|
1146
907
|
# end
|
1147
|
-
#
|
908
|
+
#
|
1148
909
|
# See StateMachines::Event#transition for more information on
|
1149
910
|
# the possible options that can be passed in.
|
1150
|
-
#
|
911
|
+
#
|
1151
912
|
# *Note* that this block is executed within the context of the actual event
|
1152
913
|
# object. As a result, you will not be able to reference any class methods
|
1153
914
|
# on the model without referencing the class itself. For example,
|
1154
|
-
#
|
915
|
+
#
|
1155
916
|
# class Vehicle
|
1156
917
|
# def self.safe_states
|
1157
918
|
# [:parked, :idling, :stalled]
|
1158
919
|
# end
|
1159
|
-
#
|
920
|
+
#
|
1160
921
|
# state_machine do
|
1161
922
|
# event :park do
|
1162
923
|
# transition Vehicle.safe_states => :parked
|
1163
924
|
# end
|
1164
925
|
# end
|
1165
|
-
# end
|
1166
|
-
#
|
926
|
+
# end
|
927
|
+
#
|
1167
928
|
# == Overriding the event method
|
1168
|
-
#
|
929
|
+
#
|
1169
930
|
# By default, this will define an instance method (with the same name as the
|
1170
931
|
# event) that will fire the next possible transition for that. Although the
|
1171
932
|
# +before_transition+, +after_transition+, and +around_transition+ hooks
|
1172
933
|
# allow you to define behavior that gets executed as a result of the event's
|
1173
934
|
# transition, you can also override the event method in order to have a
|
1174
935
|
# little more fine-grained control.
|
1175
|
-
#
|
936
|
+
#
|
1176
937
|
# For example:
|
1177
|
-
#
|
938
|
+
#
|
1178
939
|
# class Vehicle
|
1179
940
|
# state_machine do
|
1180
941
|
# event :park do
|
1181
942
|
# ...
|
1182
943
|
# end
|
1183
944
|
# end
|
1184
|
-
#
|
945
|
+
#
|
1185
946
|
# def park(*)
|
1186
947
|
# take_deep_breath # Executes before the transition (and before_transition hooks) even if no transition is possible
|
1187
948
|
# if result = super # Runs the transition and all before/after/around hooks
|
@@ -1190,33 +951,33 @@ module StateMachines
|
|
1190
951
|
# result
|
1191
952
|
# end
|
1192
953
|
# end
|
1193
|
-
#
|
954
|
+
#
|
1194
955
|
# There are a few important things to note here. First, the method
|
1195
956
|
# signature is defined with an unlimited argument list in order to allow
|
1196
957
|
# callers to continue passing arguments that are expected by state_machine.
|
1197
958
|
# For example, it will still allow calls to +park+ with a single parameter
|
1198
959
|
# for skipping the configured action.
|
1199
|
-
#
|
960
|
+
#
|
1200
961
|
# Second, the overridden event method must call +super+ in order to run the
|
1201
962
|
# logic for running the next possible transition. In order to remain
|
1202
963
|
# consistent with other events, the result of +super+ is returned.
|
1203
|
-
#
|
964
|
+
#
|
1204
965
|
# Third, any behavior defined in this method will *not* get executed if
|
1205
966
|
# you're taking advantage of attribute-based event transitions. For example:
|
1206
|
-
#
|
967
|
+
#
|
1207
968
|
# vehicle = Vehicle.new
|
1208
969
|
# vehicle.state_event = 'park'
|
1209
970
|
# vehicle.save
|
1210
|
-
#
|
971
|
+
#
|
1211
972
|
# In this case, the +park+ event will run the before/after/around transition
|
1212
973
|
# hooks and transition the state, but the behavior defined in the overriden
|
1213
974
|
# +park+ method will *not* be executed.
|
1214
|
-
#
|
975
|
+
#
|
1215
976
|
# == Defining additional arguments
|
1216
|
-
#
|
977
|
+
#
|
1217
978
|
# Additional arguments can be passed into events and accessed by transition
|
1218
979
|
# hooks like so:
|
1219
|
-
#
|
980
|
+
#
|
1220
981
|
# class Vehicle
|
1221
982
|
# state_machine do
|
1222
983
|
# after_transition :on => :park do |vehicle, transition|
|
@@ -1224,157 +985,128 @@ module StateMachines
|
|
1224
985
|
# ...
|
1225
986
|
# end
|
1226
987
|
# after_transition :on => :park, :do => :take_deep_breath
|
1227
|
-
#
|
988
|
+
#
|
1228
989
|
# event :park do
|
1229
990
|
# ...
|
1230
991
|
# end
|
1231
|
-
#
|
992
|
+
#
|
1232
993
|
# def take_deep_breath(transition)
|
1233
994
|
# kind = *transition.args # :parallel
|
1234
995
|
# ...
|
1235
996
|
# end
|
1236
997
|
# end
|
1237
998
|
# end
|
1238
|
-
#
|
999
|
+
#
|
1239
1000
|
# vehicle = Vehicle.new
|
1240
1001
|
# vehicle.park(:parallel)
|
1241
|
-
#
|
1002
|
+
#
|
1242
1003
|
# *Remember* that if the last argument is a boolean, it will be used as the
|
1243
1004
|
# +run_action+ parameter to the event action. Using the +park+ action
|
1244
1005
|
# example from above, you can might call it like so:
|
1245
|
-
#
|
1006
|
+
#
|
1246
1007
|
# vehicle.park # => Uses default args and runs machine action
|
1247
1008
|
# vehicle.park(:parallel) # => Specifies the +kind+ argument and runs the machine action
|
1248
1009
|
# vehicle.park(:parallel, false) # => Specifies the +kind+ argument and *skips* the machine action
|
1249
|
-
#
|
1010
|
+
#
|
1250
1011
|
# If you decide to override the +park+ event method *and* define additional
|
1251
1012
|
# arguments, you can do so as shown below:
|
1252
|
-
#
|
1013
|
+
#
|
1253
1014
|
# class Vehicle
|
1254
1015
|
# state_machine do
|
1255
1016
|
# event :park do
|
1256
1017
|
# ...
|
1257
1018
|
# end
|
1258
1019
|
# end
|
1259
|
-
#
|
1020
|
+
#
|
1260
1021
|
# def park(kind = :parallel, *args)
|
1261
1022
|
# take_deep_breath if kind == :parallel
|
1262
1023
|
# super
|
1263
1024
|
# end
|
1264
1025
|
# end
|
1265
|
-
#
|
1026
|
+
#
|
1266
1027
|
# Note that +super+ is called instead of <tt>super(*args)</tt>. This allow
|
1267
1028
|
# the entire arguments list to be accessed by transition callbacks through
|
1268
1029
|
# StateMachines::Transition#args.
|
1269
|
-
#
|
1030
|
+
#
|
1270
1031
|
# === Using matchers
|
1271
|
-
#
|
1032
|
+
#
|
1272
1033
|
# The +all+ / +any+ matchers can be used to easily execute blocks for a
|
1273
1034
|
# group of events. Note, however, that you cannot use these matchers to
|
1274
1035
|
# set configurations for events. Blocks using these matchers can be
|
1275
1036
|
# defined at any point in the state machine and will always get applied to
|
1276
1037
|
# the proper events.
|
1277
|
-
#
|
1038
|
+
#
|
1278
1039
|
# For example:
|
1279
|
-
#
|
1040
|
+
#
|
1280
1041
|
# state_machine :initial => :parked do
|
1281
1042
|
# ...
|
1282
|
-
#
|
1043
|
+
#
|
1283
1044
|
# event all - [:crash] do
|
1284
1045
|
# transition :stalled => :parked
|
1285
1046
|
# end
|
1286
1047
|
# end
|
1287
|
-
#
|
1048
|
+
#
|
1288
1049
|
# == Example
|
1289
|
-
#
|
1050
|
+
#
|
1290
1051
|
# class Vehicle
|
1291
1052
|
# state_machine do
|
1292
1053
|
# # The park, stop, and halt events will all share the given transitions
|
1293
1054
|
# event :park, :stop, :halt do
|
1294
1055
|
# transition [:idling, :backing_up] => :parked
|
1295
1056
|
# end
|
1296
|
-
#
|
1057
|
+
#
|
1297
1058
|
# event :stop do
|
1298
1059
|
# transition :first_gear => :idling
|
1299
1060
|
# end
|
1300
|
-
#
|
1061
|
+
#
|
1301
1062
|
# event :ignite do
|
1302
1063
|
# transition :parked => :idling
|
1303
1064
|
# transition :idling => same # Allow ignite while still idling
|
1304
1065
|
# end
|
1305
1066
|
# end
|
1306
1067
|
# end
|
1307
|
-
def event(*names, &block)
|
1308
|
-
options = names.last.is_a?(Hash) ? names.pop : {}
|
1309
|
-
options.assert_valid_keys(:human_name)
|
1310
|
-
|
1311
|
-
# Store the context so that it can be used for / matched against any event
|
1312
|
-
# that gets added
|
1313
|
-
@events.context(names, &block) if block_given?
|
1314
|
-
|
1315
|
-
if names.first.is_a?(Matcher)
|
1316
|
-
# Add any events referenced in the matcher. When matchers are used,
|
1317
|
-
# events are not allowed to be configured.
|
1318
|
-
raise ArgumentError, "Cannot configure events when using matchers (using #{options.inspect})" if options.any?
|
1319
|
-
events = add_events(names.first.values)
|
1320
|
-
else
|
1321
|
-
events = add_events(names)
|
1322
|
-
|
1323
|
-
# Update the configuration for the event(s)
|
1324
|
-
events.each do |event|
|
1325
|
-
event.human_name = options[:human_name] if options.include?(:human_name)
|
1326
|
-
|
1327
|
-
# Add any states that may have been referenced within the event
|
1328
|
-
add_states(event.known_states)
|
1329
|
-
end
|
1330
|
-
end
|
1331
|
-
|
1332
|
-
events.length == 1 ? events.first : events
|
1333
|
-
end
|
1334
|
-
|
1335
|
-
alias_method :on, :event
|
1336
1068
|
|
1337
1069
|
# Creates a new transition that determines what to change the current state
|
1338
1070
|
# to when an event fires.
|
1339
|
-
#
|
1071
|
+
#
|
1340
1072
|
# == Defining transitions
|
1341
|
-
#
|
1073
|
+
#
|
1342
1074
|
# The options for a new transition uses the Hash syntax to map beginning
|
1343
1075
|
# states to ending states. For example,
|
1344
|
-
#
|
1076
|
+
#
|
1345
1077
|
# transition :parked => :idling, :idling => :first_gear, :on => :ignite
|
1346
|
-
#
|
1078
|
+
#
|
1347
1079
|
# In this case, when the +ignite+ event is fired, this transition will cause
|
1348
1080
|
# the state to be +idling+ if it's current state is +parked+ or +first_gear+
|
1349
1081
|
# if it's current state is +idling+.
|
1350
|
-
#
|
1082
|
+
#
|
1351
1083
|
# To help define these implicit transitions, a set of helpers are available
|
1352
1084
|
# for slightly more complex matching:
|
1353
1085
|
# * <tt>all</tt> - Matches every state in the machine
|
1354
1086
|
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state except those specified
|
1355
1087
|
# * <tt>any</tt> - An alias for +all+ (matches every state in the machine)
|
1356
1088
|
# * <tt>same</tt> - Matches the same state being transitioned from
|
1357
|
-
#
|
1089
|
+
#
|
1358
1090
|
# See StateMachines::MatcherHelpers for more information.
|
1359
|
-
#
|
1091
|
+
#
|
1360
1092
|
# Examples:
|
1361
|
-
#
|
1093
|
+
#
|
1362
1094
|
# transition all => nil, :on => :ignite # Transitions to nil regardless of the current state
|
1363
1095
|
# transition all => :idling, :on => :ignite # Transitions to :idling regardless of the current state
|
1364
1096
|
# transition all - [:idling, :first_gear] => :idling, :on => :ignite # Transitions every state but :idling and :first_gear to :idling
|
1365
1097
|
# transition nil => :idling, :on => :ignite # Transitions to :idling from the nil state
|
1366
1098
|
# transition :parked => :idling, :on => :ignite # Transitions to :idling if :parked
|
1367
1099
|
# transition [:parked, :stalled] => :idling, :on => :ignite # Transitions to :idling if :parked or :stalled
|
1368
|
-
#
|
1100
|
+
#
|
1369
1101
|
# transition :parked => same, :on => :park # Loops :parked back to :parked
|
1370
1102
|
# transition [:parked, :stalled] => same, :on => [:park, :stall] # Loops either :parked or :stalled back to the same state on the park and stall events
|
1371
1103
|
# transition all - :parked => same, :on => :noop # Loops every state but :parked back to the same state
|
1372
|
-
#
|
1104
|
+
#
|
1373
1105
|
# # Transitions to :idling if :parked, :first_gear if :idling, or :second_gear if :first_gear
|
1374
1106
|
# transition :parked => :idling, :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up
|
1375
|
-
#
|
1107
|
+
#
|
1376
1108
|
# == Verbose transitions
|
1377
|
-
#
|
1109
|
+
#
|
1378
1110
|
# Transitions can also be defined use an explicit set of configuration
|
1379
1111
|
# options:
|
1380
1112
|
# * <tt>:from</tt> - A state or array of states that can be transitioned from.
|
@@ -1383,24 +1115,24 @@ module StateMachines
|
|
1383
1115
|
# then the transition will simply loop back (i.e. the state will not change).
|
1384
1116
|
# * <tt>:except_from</tt> - A state or array of states that *cannot* be
|
1385
1117
|
# transitioned from.
|
1386
|
-
#
|
1118
|
+
#
|
1387
1119
|
# These options must be used when defining transitions within the context
|
1388
1120
|
# of a state.
|
1389
|
-
#
|
1121
|
+
#
|
1390
1122
|
# Examples:
|
1391
|
-
#
|
1123
|
+
#
|
1392
1124
|
# transition :to => nil, :on => :park
|
1393
1125
|
# transition :to => :idling, :on => :ignite
|
1394
1126
|
# transition :except_from => [:idling, :first_gear], :to => :idling, :on => :ignite
|
1395
1127
|
# transition :from => nil, :to => :idling, :on => :ignite
|
1396
1128
|
# transition :from => [:parked, :stalled], :to => :idling, :on => :ignite
|
1397
|
-
#
|
1129
|
+
#
|
1398
1130
|
# == Conditions
|
1399
|
-
#
|
1131
|
+
#
|
1400
1132
|
# In addition to the state requirements for each transition, a condition
|
1401
1133
|
# can also be defined to help determine whether that transition is
|
1402
1134
|
# available. These options will work on both the normal and verbose syntax.
|
1403
|
-
#
|
1135
|
+
#
|
1404
1136
|
# Configuration options:
|
1405
1137
|
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
1406
1138
|
# transition should occur (e.g. :if => :moving?, or :if => lambda {|vehicle| vehicle.speed > 60}).
|
@@ -1408,39 +1140,30 @@ module StateMachines
|
|
1408
1140
|
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
1409
1141
|
# transition should not occur (e.g. :unless => :stopped?, or :unless => lambda {|vehicle| vehicle.speed <= 60}).
|
1410
1142
|
# The condition should return or evaluate to true or false.
|
1411
|
-
#
|
1143
|
+
#
|
1412
1144
|
# Examples:
|
1413
|
-
#
|
1145
|
+
#
|
1414
1146
|
# transition :parked => :idling, :on => :ignite, :if => :moving?
|
1415
1147
|
# transition :parked => :idling, :on => :ignite, :unless => :stopped?
|
1416
1148
|
# transition :idling => :first_gear, :first_gear => :second_gear, :on => :shift_up, :if => :seatbelt_on?
|
1417
|
-
#
|
1149
|
+
#
|
1418
1150
|
# transition :from => :parked, :to => :idling, :on => ignite, :if => :moving?
|
1419
1151
|
# transition :from => :parked, :to => :idling, :on => ignite, :unless => :stopped?
|
1420
|
-
#
|
1152
|
+
#
|
1421
1153
|
# == Order of operations
|
1422
|
-
#
|
1154
|
+
#
|
1423
1155
|
# Transitions are evaluated in the order in which they're defined. As a
|
1424
1156
|
# result, if more than one transition applies to a given object, then the
|
1425
1157
|
# first transition that matches will be performed.
|
1426
|
-
def transition(options)
|
1427
|
-
raise ArgumentError, 'Must specify :on event' unless options[:on]
|
1428
|
-
|
1429
|
-
branches = []
|
1430
|
-
options = options.dup
|
1431
|
-
event(*Array(options.delete(:on))) { branches << transition(options) }
|
1432
|
-
|
1433
|
-
branches.length == 1 ? branches.first : branches
|
1434
|
-
end
|
1435
1158
|
|
1436
1159
|
# Creates a callback that will be invoked *before* a transition is
|
1437
1160
|
# performed so long as the given requirements match the transition.
|
1438
|
-
#
|
1161
|
+
#
|
1439
1162
|
# == The callback
|
1440
|
-
#
|
1163
|
+
#
|
1441
1164
|
# Callbacks must be defined as either an argument, in the :do option, or
|
1442
1165
|
# as a block. For example,
|
1443
|
-
#
|
1166
|
+
#
|
1444
1167
|
# class Vehicle
|
1445
1168
|
# state_machine do
|
1446
1169
|
# before_transition :set_alarm
|
@@ -1452,13 +1175,13 @@ module StateMachines
|
|
1452
1175
|
# ...
|
1453
1176
|
# end
|
1454
1177
|
# end
|
1455
|
-
#
|
1178
|
+
#
|
1456
1179
|
# Notice that the first three callbacks are the same in terms of how the
|
1457
1180
|
# methods to invoke are defined. However, using the <tt>:do</tt> can
|
1458
1181
|
# provide for a more fluid DSL.
|
1459
|
-
#
|
1182
|
+
#
|
1460
1183
|
# In addition, multiple callbacks can be defined like so:
|
1461
|
-
#
|
1184
|
+
#
|
1462
1185
|
# class Vehicle
|
1463
1186
|
# state_machine do
|
1464
1187
|
# before_transition :set_alarm, :lock_doors, all => :parked
|
@@ -1468,54 +1191,54 @@ module StateMachines
|
|
1468
1191
|
# end
|
1469
1192
|
# end
|
1470
1193
|
# end
|
1471
|
-
#
|
1194
|
+
#
|
1472
1195
|
# Notice that the different ways of configuring methods can be mixed.
|
1473
|
-
#
|
1196
|
+
#
|
1474
1197
|
# == State requirements
|
1475
|
-
#
|
1198
|
+
#
|
1476
1199
|
# Callbacks can require that the machine be transitioning from and to
|
1477
1200
|
# specific states. These requirements use a Hash syntax to map beginning
|
1478
1201
|
# states to ending states. For example,
|
1479
|
-
#
|
1202
|
+
#
|
1480
1203
|
# before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm
|
1481
|
-
#
|
1204
|
+
#
|
1482
1205
|
# In this case, the +set_alarm+ callback will only be called if the machine
|
1483
1206
|
# is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.
|
1484
|
-
#
|
1207
|
+
#
|
1485
1208
|
# To help define state requirements, a set of helpers are available for
|
1486
1209
|
# slightly more complex matching:
|
1487
1210
|
# * <tt>all</tt> - Matches every state/event in the machine
|
1488
1211
|
# * <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
|
1489
1212
|
# * <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
|
1490
1213
|
# * <tt>same</tt> - Matches the same state being transitioned from
|
1491
|
-
#
|
1214
|
+
#
|
1492
1215
|
# See StateMachines::MatcherHelpers for more information.
|
1493
|
-
#
|
1216
|
+
#
|
1494
1217
|
# Examples:
|
1495
|
-
#
|
1218
|
+
#
|
1496
1219
|
# before_transition :parked => [:idling, :first_gear], :do => ... # Matches from parked to idling or first_gear
|
1497
1220
|
# before_transition all - [:parked, :idling] => :idling, :do => ... # Matches from every state except parked and idling to idling
|
1498
1221
|
# before_transition all => :parked, :do => ... # Matches all states to parked
|
1499
1222
|
# before_transition any => same, :do => ... # Matches every loopback
|
1500
|
-
#
|
1223
|
+
#
|
1501
1224
|
# == Event requirements
|
1502
|
-
#
|
1225
|
+
#
|
1503
1226
|
# In addition to state requirements, an event requirement can be defined so
|
1504
1227
|
# that the callback is only invoked on specific events using the +on+
|
1505
1228
|
# option. This can also use the same matcher helpers as the state
|
1506
1229
|
# requirements.
|
1507
|
-
#
|
1230
|
+
#
|
1508
1231
|
# Examples:
|
1509
|
-
#
|
1232
|
+
#
|
1510
1233
|
# before_transition :on => :ignite, :do => ... # Matches only on ignite
|
1511
1234
|
# before_transition :on => all - :ignite, :do => ... # Matches on every event except ignite
|
1512
1235
|
# before_transition :parked => :idling, :on => :ignite, :do => ... # Matches from parked to idling on ignite
|
1513
|
-
#
|
1236
|
+
#
|
1514
1237
|
# == Verbose Requirements
|
1515
|
-
#
|
1238
|
+
#
|
1516
1239
|
# Requirements can also be defined using verbose options rather than the
|
1517
1240
|
# implicit Hash syntax and helper methods described above.
|
1518
|
-
#
|
1241
|
+
#
|
1519
1242
|
# Configuration options:
|
1520
1243
|
# * <tt>:from</tt> - One or more states being transitioned from. If none
|
1521
1244
|
# are specified, then all states will match.
|
@@ -1526,163 +1249,173 @@ module StateMachines
|
|
1526
1249
|
# * <tt>:except_from</tt> - One or more states *not* being transitioned from
|
1527
1250
|
# * <tt>:except_to</tt> - One more states *not* being transitioned to
|
1528
1251
|
# * <tt>:except_on</tt> - One or more events that *did not* fire the transition
|
1529
|
-
#
|
1252
|
+
#
|
1530
1253
|
# Examples:
|
1531
|
-
#
|
1254
|
+
#
|
1532
1255
|
# before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
|
1533
1256
|
# before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...
|
1534
|
-
#
|
1257
|
+
#
|
1535
1258
|
# == Conditions
|
1536
|
-
#
|
1259
|
+
#
|
1537
1260
|
# In addition to the state/event requirements, a condition can also be
|
1538
1261
|
# defined to help determine whether the callback should be invoked.
|
1539
|
-
#
|
1262
|
+
#
|
1540
1263
|
# Configuration options:
|
1541
1264
|
# * <tt>:if</tt> - A method, proc or string to call to determine if the
|
1542
1265
|
# callback should occur (e.g. :if => :allow_callbacks, or
|
1543
1266
|
# :if => lambda {|user| user.signup_step > 2}). The method, proc or string
|
1544
|
-
# should return or evaluate to a true or false value.
|
1267
|
+
# should return or evaluate to a true or false value.
|
1545
1268
|
# * <tt>:unless</tt> - A method, proc or string to call to determine if the
|
1546
1269
|
# callback should not occur (e.g. :unless => :skip_callbacks, or
|
1547
1270
|
# :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
|
1548
|
-
# string should return or evaluate to a true or false value.
|
1549
|
-
#
|
1271
|
+
# string should return or evaluate to a true or false value.
|
1272
|
+
#
|
1550
1273
|
# Examples:
|
1551
|
-
#
|
1274
|
+
#
|
1552
1275
|
# before_transition :parked => :idling, :if => :moving?, :do => ...
|
1553
1276
|
# before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...
|
1554
|
-
#
|
1277
|
+
#
|
1555
1278
|
# == Accessing the transition
|
1556
|
-
#
|
1279
|
+
#
|
1557
1280
|
# In addition to passing the object being transitioned, the actual
|
1558
1281
|
# transition describing the context (e.g. event, from, to) can be accessed
|
1559
1282
|
# as well. This additional argument is only passed if the callback allows
|
1560
1283
|
# for it.
|
1561
|
-
#
|
1284
|
+
#
|
1562
1285
|
# For example,
|
1563
|
-
#
|
1286
|
+
#
|
1564
1287
|
# class Vehicle
|
1565
1288
|
# # Only specifies one parameter (the object being transitioned)
|
1566
1289
|
# before_transition all => :parked do |vehicle|
|
1567
1290
|
# vehicle.set_alarm
|
1568
1291
|
# end
|
1569
|
-
#
|
1292
|
+
#
|
1570
1293
|
# # Specifies 2 parameters (object being transitioned and actual transition)
|
1571
1294
|
# before_transition all => :parked do |vehicle, transition|
|
1572
1295
|
# vehicle.set_alarm(transition)
|
1573
1296
|
# end
|
1574
1297
|
# end
|
1575
|
-
#
|
1298
|
+
#
|
1576
1299
|
# *Note* that the object in the callback will only be passed in as an
|
1577
1300
|
# argument if callbacks are configured to *not* be bound to the object
|
1578
1301
|
# involved. This is the default and may change on a per-integration basis.
|
1579
|
-
#
|
1302
|
+
#
|
1580
1303
|
# See StateMachines::Transition for more information about the
|
1581
1304
|
# attributes available on the transition.
|
1582
|
-
#
|
1305
|
+
#
|
1583
1306
|
# == Usage with delegates
|
1584
|
-
#
|
1307
|
+
#
|
1585
1308
|
# As noted above, state_machine uses the callback method's argument list
|
1586
1309
|
# arity to determine whether to include the transition in the method call.
|
1587
1310
|
# If you're using delegates, such as those defined in ActiveSupport or
|
1588
1311
|
# Forwardable, the actual arity of the delegated method gets masked. This
|
1589
1312
|
# means that callbacks which reference delegates will always get passed the
|
1590
1313
|
# transition as an argument. For example:
|
1591
|
-
#
|
1314
|
+
#
|
1592
1315
|
# class Vehicle
|
1593
1316
|
# extend Forwardable
|
1594
1317
|
# delegate :refresh => :dashboard
|
1595
|
-
#
|
1318
|
+
#
|
1596
1319
|
# state_machine do
|
1597
1320
|
# before_transition :refresh
|
1598
1321
|
# ...
|
1599
1322
|
# end
|
1600
|
-
#
|
1323
|
+
#
|
1601
1324
|
# def dashboard
|
1602
1325
|
# @dashboard ||= Dashboard.new
|
1603
1326
|
# end
|
1604
1327
|
# end
|
1605
|
-
#
|
1328
|
+
#
|
1606
1329
|
# class Dashboard
|
1607
1330
|
# def refresh(transition)
|
1608
1331
|
# # ...
|
1609
1332
|
# end
|
1610
1333
|
# end
|
1611
|
-
#
|
1334
|
+
#
|
1612
1335
|
# In the above example, <tt>Dashboard#refresh</tt> *must* defined a
|
1613
1336
|
# +transition+ argument. Otherwise, an +ArgumentError+ exception will get
|
1614
1337
|
# raised. The only way around this is to avoid the use of delegates and
|
1615
1338
|
# manually define the delegate method so that the correct arity is used.
|
1616
|
-
#
|
1339
|
+
#
|
1617
1340
|
# == Examples
|
1618
|
-
#
|
1341
|
+
#
|
1619
1342
|
# Below is an example of a class with one state machine and various types
|
1620
1343
|
# of +before+ transitions defined for it:
|
1621
|
-
#
|
1344
|
+
#
|
1622
1345
|
# class Vehicle
|
1623
1346
|
# state_machine do
|
1624
1347
|
# # Before all transitions
|
1625
1348
|
# before_transition :update_dashboard
|
1626
|
-
#
|
1349
|
+
#
|
1627
1350
|
# # Before specific transition:
|
1628
1351
|
# before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
|
1629
|
-
#
|
1352
|
+
#
|
1630
1353
|
# # With conditional callback:
|
1631
1354
|
# before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
|
1632
|
-
#
|
1355
|
+
#
|
1633
1356
|
# # Using helpers:
|
1634
1357
|
# before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
|
1635
1358
|
# ...
|
1636
1359
|
# end
|
1637
1360
|
# end
|
1638
|
-
#
|
1361
|
+
#
|
1639
1362
|
# As can be seen, any number of transitions can be created using various
|
1640
1363
|
# combinations of configuration options.
|
1641
|
-
def before_transition(*args, &
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1364
|
+
def before_transition(*args, **options, &)
|
1365
|
+
# Extract legacy positional arguments and merge with keyword options
|
1366
|
+
parsed_options = parse_callback_arguments(args, options)
|
1367
|
+
|
1368
|
+
# Only validate callback-specific options, not state transition requirements
|
1369
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
1370
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
1371
|
+
|
1372
|
+
add_callback(:before, parsed_options, &)
|
1645
1373
|
end
|
1646
1374
|
|
1647
1375
|
# Creates a callback that will be invoked *after* a transition is
|
1648
1376
|
# performed so long as the given requirements match the transition.
|
1649
|
-
#
|
1377
|
+
#
|
1650
1378
|
# See +before_transition+ for a description of the possible configurations
|
1651
1379
|
# for defining callbacks.
|
1652
|
-
def after_transition(*args, &
|
1653
|
-
|
1654
|
-
|
1655
|
-
|
1380
|
+
def after_transition(*args, **options, &)
|
1381
|
+
# Extract legacy positional arguments and merge with keyword options
|
1382
|
+
parsed_options = parse_callback_arguments(args, options)
|
1383
|
+
|
1384
|
+
# Only validate callback-specific options, not state transition requirements
|
1385
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
1386
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
1387
|
+
|
1388
|
+
add_callback(:after, parsed_options, &)
|
1656
1389
|
end
|
1657
1390
|
|
1658
1391
|
# Creates a callback that will be invoked *around* a transition so long as
|
1659
1392
|
# the given requirements match the transition.
|
1660
|
-
#
|
1393
|
+
#
|
1661
1394
|
# == The callback
|
1662
|
-
#
|
1395
|
+
#
|
1663
1396
|
# Around callbacks wrap transitions, executing code both before and after.
|
1664
1397
|
# These callbacks are defined in the exact same manner as before / after
|
1665
1398
|
# callbacks with the exception that the transition must be yielded to in
|
1666
1399
|
# order to finish running it.
|
1667
|
-
#
|
1400
|
+
#
|
1668
1401
|
# If defining +around+ callbacks using blocks, you must yield within the
|
1669
1402
|
# transition by directly calling the block (since yielding is not allowed
|
1670
1403
|
# within blocks).
|
1671
|
-
#
|
1404
|
+
#
|
1672
1405
|
# For example,
|
1673
|
-
#
|
1406
|
+
#
|
1674
1407
|
# class Vehicle
|
1675
1408
|
# state_machine do
|
1676
1409
|
# around_transition do |block|
|
1677
1410
|
# Benchmark.measure { block.call }
|
1678
1411
|
# end
|
1679
|
-
#
|
1412
|
+
#
|
1680
1413
|
# around_transition do |vehicle, block|
|
1681
1414
|
# logger.info "vehicle was #{state}..."
|
1682
1415
|
# block.call
|
1683
1416
|
# logger.info "...and is now #{state}"
|
1684
1417
|
# end
|
1685
|
-
#
|
1418
|
+
#
|
1686
1419
|
# around_transition do |vehicle, transition, block|
|
1687
1420
|
# logger.info "before #{transition.event}: #{vehicle.state}"
|
1688
1421
|
# block.call
|
@@ -1690,73 +1423,78 @@ module StateMachines
|
|
1690
1423
|
# end
|
1691
1424
|
# end
|
1692
1425
|
# end
|
1693
|
-
#
|
1426
|
+
#
|
1694
1427
|
# Notice that referencing the block is similar to doing so within an
|
1695
1428
|
# actual method definition in that it is always the last argument.
|
1696
|
-
#
|
1429
|
+
#
|
1697
1430
|
# On the other hand, if you're defining +around+ callbacks using method
|
1698
1431
|
# references, you can yield like normal:
|
1699
|
-
#
|
1432
|
+
#
|
1700
1433
|
# class Vehicle
|
1701
1434
|
# state_machine do
|
1702
1435
|
# around_transition :benchmark
|
1703
1436
|
# ...
|
1704
1437
|
# end
|
1705
|
-
#
|
1438
|
+
#
|
1706
1439
|
# def benchmark
|
1707
1440
|
# Benchmark.measure { yield }
|
1708
1441
|
# end
|
1709
1442
|
# end
|
1710
|
-
#
|
1443
|
+
#
|
1711
1444
|
# See +before_transition+ for a description of the possible configurations
|
1712
1445
|
# for defining callbacks.
|
1713
|
-
def around_transition(*args, &
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1446
|
+
def around_transition(*args, **options, &)
|
1447
|
+
# Extract legacy positional arguments and merge with keyword options
|
1448
|
+
parsed_options = parse_callback_arguments(args, options)
|
1449
|
+
|
1450
|
+
# Only validate callback-specific options, not state transition requirements
|
1451
|
+
callback_options = parsed_options.slice(:do, :if, :unless, :bind_to_object, :terminator)
|
1452
|
+
StateMachines::OptionsValidator.assert_valid_keys!(callback_options, :do, :if, :unless, :bind_to_object, :terminator)
|
1453
|
+
|
1454
|
+
add_callback(:around, parsed_options, &)
|
1717
1455
|
end
|
1718
1456
|
|
1719
1457
|
# Creates a callback that will be invoked *after* a transition failures to
|
1720
1458
|
# be performed so long as the given requirements match the transition.
|
1721
|
-
#
|
1459
|
+
#
|
1722
1460
|
# See +before_transition+ for a description of the possible configurations
|
1723
1461
|
# for defining callbacks. *Note* however that you cannot define the state
|
1724
1462
|
# requirements in these callbacks. You may only define event requirements.
|
1725
|
-
#
|
1463
|
+
#
|
1726
1464
|
# = The callback
|
1727
|
-
#
|
1465
|
+
#
|
1728
1466
|
# Failure callbacks get invoked whenever an event fails to execute. This
|
1729
1467
|
# can happen when no transition is available, a +before+ callback halts
|
1730
1468
|
# execution, or the action associated with this machine fails to succeed.
|
1731
1469
|
# In any of these cases, any failure callback that matches the attempted
|
1732
1470
|
# transition will be run.
|
1733
|
-
#
|
1471
|
+
#
|
1734
1472
|
# For example,
|
1735
|
-
#
|
1473
|
+
#
|
1736
1474
|
# class Vehicle
|
1737
1475
|
# state_machine do
|
1738
1476
|
# after_failure do |vehicle, transition|
|
1739
1477
|
# logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
|
1740
1478
|
# end
|
1741
|
-
#
|
1479
|
+
#
|
1742
1480
|
# after_failure :on => :ignite, :do => :log_ignition_failure
|
1743
|
-
#
|
1481
|
+
#
|
1744
1482
|
# ...
|
1745
1483
|
# end
|
1746
1484
|
# end
|
1747
|
-
def after_failure(*args, &
|
1748
|
-
|
1749
|
-
|
1750
|
-
|
1485
|
+
def after_failure(*args, **options, &)
|
1486
|
+
# Extract legacy positional arguments and merge with keyword options
|
1487
|
+
parsed_options = parse_callback_arguments(args, options)
|
1488
|
+
StateMachines::OptionsValidator.assert_valid_keys!(parsed_options, :on, :do, :if, :unless)
|
1751
1489
|
|
1752
|
-
add_callback(:failure,
|
1490
|
+
add_callback(:failure, parsed_options, &)
|
1753
1491
|
end
|
1754
1492
|
|
1755
1493
|
# Generates a list of the possible transition sequences that can be run on
|
1756
1494
|
# the given object. These paths can reveal all of the possible states and
|
1757
1495
|
# events that can be encountered in the object's state machine based on the
|
1758
1496
|
# object's current state.
|
1759
|
-
#
|
1497
|
+
#
|
1760
1498
|
# Configuration options:
|
1761
1499
|
# * +from+ - The initial state to start all paths from. By default, this
|
1762
1500
|
# is the object's current state.
|
@@ -1767,31 +1505,31 @@ module StateMachines
|
|
1767
1505
|
# state (if specified) is reached. If this is enabled, then paths can
|
1768
1506
|
# continue even after reaching the target state; they will stop when
|
1769
1507
|
# reaching the target state a second time.
|
1770
|
-
#
|
1508
|
+
#
|
1771
1509
|
# *Note* that the object is never modified when the list of paths is
|
1772
1510
|
# generated.
|
1773
|
-
#
|
1511
|
+
#
|
1774
1512
|
# == Examples
|
1775
|
-
#
|
1513
|
+
#
|
1776
1514
|
# class Vehicle
|
1777
1515
|
# state_machine :initial => :parked do
|
1778
1516
|
# event :ignite do
|
1779
1517
|
# transition :parked => :idling
|
1780
1518
|
# end
|
1781
|
-
#
|
1519
|
+
#
|
1782
1520
|
# event :shift_up do
|
1783
1521
|
# transition :idling => :first_gear, :first_gear => :second_gear
|
1784
1522
|
# end
|
1785
|
-
#
|
1523
|
+
#
|
1786
1524
|
# event :shift_down do
|
1787
1525
|
# transition :second_gear => :first_gear, :first_gear => :idling
|
1788
1526
|
# end
|
1789
1527
|
# end
|
1790
1528
|
# end
|
1791
|
-
#
|
1529
|
+
#
|
1792
1530
|
# vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
|
1793
1531
|
# vehicle.state # => "parked"
|
1794
|
-
#
|
1532
|
+
#
|
1795
1533
|
# vehicle.state_paths
|
1796
1534
|
# # => [
|
1797
1535
|
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
@@ -1799,37 +1537,33 @@ module StateMachines
|
|
1799
1537
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
|
1800
1538
|
# # #<StateMachines::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
|
1801
1539
|
# # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
|
1802
|
-
# #
|
1540
|
+
# #
|
1803
1541
|
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
1804
1542
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
1805
1543
|
# # #<StateMachines::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
|
1806
1544
|
# # ]
|
1807
|
-
#
|
1545
|
+
#
|
1808
1546
|
# vehicle.state_paths(:from => :parked, :to => :second_gear)
|
1809
1547
|
# # => [
|
1810
1548
|
# # [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
|
1811
1549
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
|
1812
1550
|
# # #<StateMachines::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
|
1813
1551
|
# # ]
|
1814
|
-
#
|
1552
|
+
#
|
1815
1553
|
# In addition to getting the possible paths that can be accessed, you can
|
1816
1554
|
# also get summary information about the states / events that can be
|
1817
1555
|
# accessed at some point along one of the paths. For example:
|
1818
|
-
#
|
1556
|
+
#
|
1819
1557
|
# # Get the list of states that can be accessed from the current state
|
1820
1558
|
# vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
|
1821
|
-
#
|
1559
|
+
#
|
1822
1560
|
# # Get the list of events that can be accessed from the current state
|
1823
1561
|
# vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
|
1824
|
-
def paths_for(object, requirements = {})
|
1825
|
-
PathCollection.new(object, self, requirements)
|
1826
|
-
end
|
1827
1562
|
|
1828
1563
|
# Marks the given object as invalid with the given message.
|
1829
|
-
#
|
1564
|
+
#
|
1830
1565
|
# By default, this is a no-op.
|
1831
|
-
def invalidate(_object, _attribute, _message, _values = [])
|
1832
|
-
end
|
1566
|
+
def invalidate(_object, _attribute, _message, _values = []); end
|
1833
1567
|
|
1834
1568
|
# Gets a description of the errors for the given object. This is used to
|
1835
1569
|
# provide more detailed information when an InvalidTransition exception is
|
@@ -1839,328 +1573,63 @@ module StateMachines
|
|
1839
1573
|
end
|
1840
1574
|
|
1841
1575
|
# Resets any errors previously added when invalidating the given object.
|
1842
|
-
#
|
1576
|
+
#
|
1843
1577
|
# By default, this is a no-op.
|
1844
|
-
def reset(_object)
|
1845
|
-
end
|
1578
|
+
def reset(_object); end
|
1846
1579
|
|
1847
1580
|
# Generates the message to use when invalidating the given object after
|
1848
1581
|
# failing to transition on a specific event
|
1849
1582
|
def generate_message(name, values = [])
|
1850
|
-
message =
|
1583
|
+
message = @messages[name] || self.class.default_messages[name]
|
1851
1584
|
|
1852
1585
|
# Check whether there are actually any values to interpolate to avoid
|
1853
1586
|
# any warnings
|
1854
1587
|
if message.scan(/%./).any? { |match| match != '%%' }
|
1855
|
-
message % values.map
|
1588
|
+
message % values.map(&:last)
|
1856
1589
|
else
|
1857
1590
|
message
|
1858
1591
|
end
|
1859
1592
|
end
|
1860
1593
|
|
1861
1594
|
# Runs a transaction, rolling back any changes if the yielded block fails.
|
1862
|
-
#
|
1595
|
+
#
|
1863
1596
|
# This is only applicable to integrations that involve databases. By
|
1864
1597
|
# default, this will not run any transactions since the changes aren't
|
1865
1598
|
# taking place within the context of a database.
|
1866
|
-
def within_transaction(object)
|
1599
|
+
def within_transaction(object, &)
|
1867
1600
|
if use_transactions
|
1868
|
-
transaction(object)
|
1601
|
+
transaction(object, &)
|
1869
1602
|
else
|
1870
1603
|
yield
|
1871
1604
|
end
|
1872
1605
|
end
|
1873
1606
|
|
1607
|
+
def renderer
|
1608
|
+
self.class.renderer
|
1609
|
+
end
|
1874
1610
|
|
1875
|
-
def draw(
|
1876
|
-
|
1611
|
+
def draw(**)
|
1612
|
+
renderer.draw_machine(self, **)
|
1877
1613
|
end
|
1878
1614
|
|
1879
1615
|
# Determines whether an action hook was defined for firing attribute-based
|
1880
1616
|
# event transitions when the configured action gets called.
|
1881
1617
|
def action_hook?(self_only = false)
|
1882
|
-
@action_hook_defined || !self_only && owner_class.state_machines.any? { |
|
1618
|
+
@action_hook_defined || (!self_only && owner_class.state_machines.any? { |_name, machine| machine.action == action && machine != self && machine.action_hook?(true) })
|
1883
1619
|
end
|
1884
1620
|
|
1885
1621
|
protected
|
1886
|
-
# Runs additional initialization hooks. By default, this is a no-op.
|
1887
|
-
def after_initialize
|
1888
|
-
end
|
1889
|
-
|
1890
|
-
# Looks up other machines that have been defined in the owner class and
|
1891
|
-
# are targeting the same attribute as this machine. When accessing
|
1892
|
-
# sibling machines, they will be automatically copied for the current
|
1893
|
-
# class if they haven't been already. This ensures that any configuration
|
1894
|
-
# changes made to the sibling machines only affect this class and not any
|
1895
|
-
# base class that may have originally defined the machine.
|
1896
|
-
def sibling_machines
|
1897
|
-
owner_class.state_machines.inject([]) do |machines, (name, machine)|
|
1898
|
-
if machine.attribute == attribute && machine != self
|
1899
|
-
machines << (owner_class.state_machine(name) {})
|
1900
|
-
end
|
1901
|
-
machines
|
1902
|
-
end
|
1903
|
-
end
|
1904
|
-
|
1905
|
-
# Determines if the machine's attribute needs to be initialized. This
|
1906
|
-
# will only be true if the machine's attribute is blank.
|
1907
|
-
def initialize_state?(object)
|
1908
|
-
value = read(object, :state)
|
1909
|
-
(value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
|
1910
|
-
end
|
1911
|
-
|
1912
|
-
# Adds helper methods for interacting with the state machine, including
|
1913
|
-
# for states, events, and transitions
|
1914
|
-
def define_helpers
|
1915
|
-
define_state_accessor
|
1916
|
-
define_state_predicate
|
1917
|
-
define_event_helpers
|
1918
|
-
define_path_helpers
|
1919
|
-
define_action_helpers if define_action_helpers?
|
1920
|
-
define_name_helpers
|
1921
|
-
end
|
1922
|
-
|
1923
|
-
# Defines the initial values for state machine attributes. Static values
|
1924
|
-
# are set prior to the original initialize method and dynamic values are
|
1925
|
-
# set *after* the initialize method in case it is dependent on it.
|
1926
|
-
def define_state_initializer
|
1927
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1928
|
-
def initialize(*)
|
1929
|
-
self.class.state_machines.initialize_states(self) { super }
|
1930
|
-
end
|
1931
|
-
end_eval
|
1932
|
-
end
|
1933
|
-
|
1934
|
-
# Adds reader/writer methods for accessing the state attribute
|
1935
|
-
def define_state_accessor
|
1936
|
-
attribute = self.attribute
|
1937
|
-
|
1938
|
-
@helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
|
1939
|
-
@helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
|
1940
|
-
end
|
1941
|
-
|
1942
|
-
# Adds predicate method to the owner class for determining the name of the
|
1943
|
-
# current state
|
1944
|
-
def define_state_predicate
|
1945
|
-
call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
|
1946
|
-
define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
|
1947
|
-
def #{name}?(*args)
|
1948
|
-
args.empty? && (#{call_super} || defined?(super)) ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
|
1949
|
-
end
|
1950
|
-
end_eval
|
1951
|
-
end
|
1952
|
-
|
1953
|
-
# Adds helper methods for getting information about this state machine's
|
1954
|
-
# events
|
1955
|
-
def define_event_helpers
|
1956
|
-
# Gets the events that are allowed to fire on the current object
|
1957
|
-
define_helper(:instance, attribute(:events)) do |machine, object, *args|
|
1958
|
-
machine.events.valid_for(object, *args).map { |event| event.name }
|
1959
|
-
end
|
1960
|
-
|
1961
|
-
# Gets the next possible transitions that can be run on the current
|
1962
|
-
# object
|
1963
|
-
define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
|
1964
|
-
machine.events.transitions_for(object, *args)
|
1965
|
-
end
|
1966
|
-
|
1967
|
-
# Fire an arbitrary event for this machine
|
1968
|
-
define_helper(:instance, "fire_#{attribute(:event)}") do |machine, object, event, *args|
|
1969
|
-
machine.events.fetch(event).fire(object, *args)
|
1970
|
-
end
|
1971
|
-
|
1972
|
-
# Add helpers for tracking the event / transition to invoke when the
|
1973
|
-
# action is called
|
1974
|
-
if action
|
1975
|
-
event_attribute = attribute(:event)
|
1976
|
-
define_helper(:instance, event_attribute) do |machine, object|
|
1977
|
-
# Interpret non-blank events as present
|
1978
|
-
event = machine.read(object, :event, true)
|
1979
|
-
event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
|
1980
|
-
end
|
1981
1622
|
|
1982
|
-
|
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
|
1623
|
+
# Runs additional initialization hooks. By default, this is a no-op.
|
1624
|
+
def after_initialize; end
|
2047
1625
|
|
2048
1626
|
# Determines whether there's already a helper method defined within the
|
2049
1627
|
# given scope. This is true only if one of the owner's ancestors defines
|
2050
1628
|
# the method and is further along in the ancestor chain than this
|
2051
1629
|
# machine's helper module.
|
2052
|
-
def owner_class_ancestor_has_method?(scope, method)
|
2053
|
-
return false unless owner_class_has_method?(scope, method)
|
2054
|
-
|
2055
|
-
superclasses = owner_class.ancestors[1..-1].select { |ancestor| ancestor.is_a?(Class) }
|
2056
|
-
|
2057
|
-
if scope == :class
|
2058
|
-
current = owner_class.singleton_class
|
2059
|
-
superclass = superclasses.first
|
2060
|
-
else
|
2061
|
-
current = owner_class
|
2062
|
-
superclass = owner_class.superclass
|
2063
|
-
end
|
2064
|
-
|
2065
|
-
# Generate the list of modules that *only* occur in the owner class, but
|
2066
|
-
# were included *prior* to the helper modules, in addition to the
|
2067
|
-
# superclasses
|
2068
|
-
ancestors = current.ancestors - superclass.ancestors + superclasses
|
2069
|
-
ancestors = ancestors[ancestors.index(@helper_modules[scope])..-1].reverse
|
2070
|
-
|
2071
|
-
# Search for for the first ancestor that defined this method
|
2072
|
-
ancestors.detect do |ancestor|
|
2073
|
-
ancestor = ancestor.singleton_class if scope == :class && ancestor.is_a?(Class)
|
2074
|
-
ancestor.method_defined?(method) || ancestor.private_method_defined?(method)
|
2075
|
-
end
|
2076
|
-
end
|
2077
|
-
|
2078
|
-
def owner_class_has_method?(scope, method)
|
2079
|
-
target = scope == :class ? owner_class.singleton_class : owner_class
|
2080
|
-
target.method_defined?(method) || target.private_method_defined?(method)
|
2081
|
-
end
|
2082
|
-
|
2083
|
-
# Adds helper methods for accessing naming information about states and
|
2084
|
-
# events on the owner class
|
2085
|
-
def define_name_helpers
|
2086
|
-
# Gets the humanized version of a state
|
2087
|
-
define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
|
2088
|
-
machine.states.fetch(state).human_name(klass)
|
2089
|
-
end
|
2090
|
-
|
2091
|
-
# Gets the humanized version of an event
|
2092
|
-
define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
|
2093
|
-
machine.events.fetch(event).human_name(klass)
|
2094
|
-
end
|
2095
|
-
|
2096
|
-
# Gets the state name for the current value
|
2097
|
-
define_helper(:instance, attribute(:name)) do |machine, object|
|
2098
|
-
machine.states.match!(object).name
|
2099
|
-
end
|
2100
|
-
|
2101
|
-
# Gets the human state name for the current value
|
2102
|
-
define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
|
2103
|
-
machine.states.match!(object).human_name(object.class)
|
2104
|
-
end
|
2105
|
-
end
|
2106
|
-
|
2107
|
-
# Defines the with/without scope helpers for this attribute. Both the
|
2108
|
-
# singular and plural versions of the attribute are defined for each
|
2109
|
-
# scope helper. A custom plural can be specified if it cannot be
|
2110
|
-
# automatically determined by either calling +pluralize+ on the attribute
|
2111
|
-
# name or adding an "s" to the end of the name.
|
2112
|
-
def define_scopes(custom_plural = nil)
|
2113
|
-
plural = custom_plural || pluralize(name)
|
2114
|
-
|
2115
|
-
[:with, :without].each do |kind|
|
2116
|
-
[name, plural].map { |s| s.to_s }.uniq.each do |suffix|
|
2117
|
-
method = "#{kind}_#{suffix}"
|
2118
|
-
|
2119
|
-
if scope = send("create_#{kind}_scope", method)
|
2120
|
-
# Converts state names to their corresponding values so that they
|
2121
|
-
# can be looked up properly
|
2122
|
-
define_helper(:class, method) do |machine, klass, *states|
|
2123
|
-
run_scope(scope, machine, klass, states)
|
2124
|
-
end
|
2125
|
-
end
|
2126
|
-
end
|
2127
|
-
end
|
2128
|
-
end
|
2129
|
-
|
2130
|
-
# Generates the results for the given scope based on one or more states to
|
2131
|
-
# filter by
|
2132
|
-
def run_scope(scope, machine, klass, states)
|
2133
|
-
values = states.flatten.map { |state| machine.states.fetch(state).value }
|
2134
|
-
scope.call(klass, values)
|
2135
|
-
end
|
2136
|
-
|
2137
|
-
# Pluralizes the given word using #pluralize (if available) or simply
|
2138
|
-
# adding an "s" to the end of the word
|
2139
|
-
def pluralize(word)
|
2140
|
-
word = word.to_s
|
2141
|
-
if word.respond_to?(:pluralize)
|
2142
|
-
word.pluralize
|
2143
|
-
else
|
2144
|
-
"#{name}s"
|
2145
|
-
end
|
2146
|
-
end
|
2147
|
-
|
2148
|
-
# Creates a scope for finding objects *with* a particular value or values
|
2149
|
-
# for the attribute.
|
2150
|
-
#
|
2151
|
-
# By default, this is a no-op.
|
2152
|
-
def create_with_scope(name)
|
2153
|
-
end
|
2154
|
-
|
2155
|
-
# Creates a scope for finding objects *without* a particular value or
|
2156
|
-
# values for the attribute.
|
2157
|
-
#
|
2158
|
-
# By default, this is a no-op.
|
2159
|
-
def create_without_scope(name)
|
2160
|
-
end
|
2161
1630
|
|
2162
1631
|
# Always yields
|
2163
|
-
def transaction(
|
1632
|
+
def transaction(_object)
|
2164
1633
|
yield
|
2165
1634
|
end
|
2166
1635
|
|
@@ -2175,60 +1644,5 @@ module StateMachines
|
|
2175
1644
|
def owner_class_attribute_default_matches?(state)
|
2176
1645
|
state.matches?(owner_class_attribute_default)
|
2177
1646
|
end
|
2178
|
-
|
2179
|
-
# Updates this machine based on the configuration of other machines in the
|
2180
|
-
# owner class that share the same target attribute.
|
2181
|
-
def add_sibling_machine_configs
|
2182
|
-
# Add existing states
|
2183
|
-
sibling_machines.each do |machine|
|
2184
|
-
machine.states.each { |state| states << state unless states[state.name] }
|
2185
|
-
end
|
2186
|
-
end
|
2187
|
-
|
2188
|
-
# Adds a new transition callback of the given type.
|
2189
|
-
def add_callback(type, options, &block)
|
2190
|
-
callbacks[type == :around ? :before : type] << callback = Callback.new(type, options, &block)
|
2191
|
-
add_states(callback.known_states)
|
2192
|
-
callback
|
2193
|
-
end
|
2194
|
-
|
2195
|
-
# Tracks the given set of states in the list of all known states for
|
2196
|
-
# this machine
|
2197
|
-
def add_states(new_states)
|
2198
|
-
new_states.map do |new_state|
|
2199
|
-
# Check for other states that use a different class type for their name.
|
2200
|
-
# This typically prevents string / symbol misuse.
|
2201
|
-
if new_state && conflict = states.detect { |state| state.name && state.name.class != new_state.class }
|
2202
|
-
raise ArgumentError, "#{new_state.inspect} state defined as #{new_state.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all states must be consistent"
|
2203
|
-
end
|
2204
|
-
|
2205
|
-
unless state = states[new_state]
|
2206
|
-
states << state = State.new(self, new_state)
|
2207
|
-
|
2208
|
-
# Copy states over to sibling machines
|
2209
|
-
sibling_machines.each { |machine| machine.states << state }
|
2210
|
-
end
|
2211
|
-
|
2212
|
-
state
|
2213
|
-
end
|
2214
|
-
end
|
2215
|
-
|
2216
|
-
# Tracks the given set of events in the list of all known events for
|
2217
|
-
# this machine
|
2218
|
-
def add_events(new_events)
|
2219
|
-
new_events.map do |new_event|
|
2220
|
-
# Check for other states that use a different class type for their name.
|
2221
|
-
# This typically prevents string / symbol misuse.
|
2222
|
-
if conflict = events.detect { |event| event.name.class != new_event.class }
|
2223
|
-
raise ArgumentError, "#{new_event.inspect} event defined as #{new_event.class}, #{conflict.name.inspect} defined as #{conflict.name.class}; all events must be consistent"
|
2224
|
-
end
|
2225
|
-
|
2226
|
-
unless event = events[new_event]
|
2227
|
-
events << event = Event.new(self, new_event)
|
2228
|
-
end
|
2229
|
-
|
2230
|
-
event
|
2231
|
-
end
|
2232
|
-
end
|
2233
1647
|
end
|
2234
1648
|
end
|