lib/finite_machine/safety.rb
# frozen_string_literal: true
require_relative "hook_event"
module FiniteMachine
# Module responsible for safety checks against known methods
module Safety
EVENT_CONFLICT_MESSAGE = \
"You tried to define an event named \"%{name}\", however this would " \
"generate \"%{type}\" method \"%{method}\", which is already defined " \
"by %{source}"
STATE_CALLBACK_CONFLICT_MESSAGE = \
"\"%{type}\" callback is a state listener and cannot be used " \
"with \"%{name}\" event name. Please use on_before or on_after instead."
EVENT_CALLBACK_CONFLICT_MESSAGE = \
"\"%{type}\" callback is an event listener and cannot be used " \
"with \"%{name}\" state name. Please use on_enter, on_transition or " \
"on_exit instead."
CALLBACK_INVALID_MESSAGE = \
"\"%{name}\" is not a valid callback name. " \
"Valid callback names are \"%{callbacks}"
# Raise error when the method is already defined
#
# @example
# detect_event_conflict!(:test, "test=")
#
# @raise [FiniteMachine::AlreadyDefinedError]
#
# @return [nil]
#
# @api public
def detect_event_conflict!(event_name, method_name = event_name)
if method_already_implemented?(method_name)
raise FiniteMachine::AlreadyDefinedError, EVENT_CONFLICT_MESSAGE % {
name: event_name,
type: :instance,
method: method_name,
source: "FiniteMachine"
}
end
end
# Raise error when the callback name is not valid
#
# @example
# ensure_valid_callback_name!(HookEvent::Enter, ":state_name")
#
# @raise [FiniteMachine::InvalidCallbackNameError]
#
# @return [nil]
#
# @api public
def ensure_valid_callback_name!(event_type, name)
message = if wrong_event_name?(name, event_type)
EVENT_CALLBACK_CONFLICT_MESSAGE % {
type: "on_#{event_type}",
name: name
}
elsif wrong_state_name?(name, event_type)
STATE_CALLBACK_CONFLICT_MESSAGE % {
type: "on_#{event_type}",
name: name
}
elsif !callback_names.include?(name)
CALLBACK_INVALID_MESSAGE % {
name: name,
callbacks: callback_names.to_a.inspect
}
else
nil
end
message && raise_invalid_callback_error(message)
end
private
# Check if event name exists
#
# @param [Symbol] name
#
# @param [FiniteMachine::HookEvent] event_type
#
# @return [Boolean]
#
# @api private
def wrong_event_name?(name, event_type)
machine.states.include?(name) &&
!machine.events.include?(name) &&
event_type < HookEvent::Anyaction
end
# Check if state name exists
#
# @param [Symbol] name
#
# @param [FiniteMachine::HookEvent] event_type
#
# @return [Boolean]
#
# @api private
def wrong_state_name?(name, event_type)
machine.events.include?(name) &&
!machine.states.include?(name) &&
event_type < HookEvent::Anystate
end
def raise_invalid_callback_error(message)
exception = InvalidCallbackNameError
machine.catch_error(exception) || raise(exception, message)
end
# Check if method is already implemented inside StateMachine
#
# @param [String] name
# the method name
#
# @return [Boolean]
#
# @api private
def method_already_implemented?(name)
method_defined_within?(name, FiniteMachine::StateMachine)
end
# Check if method is defined within a given class
#
# @param [String] name
# the method name
#
# @param [Object] klass
#
# @return [Boolean]
#
# @api private
def method_defined_within?(name, klass)
klass.method_defined?(name) || klass.private_method_defined?(name)
end
end # Safety
end # FiniteMachine