module AASM
# this is used internally as an argument default value to represent no value
NO_VALUE = :_aasm_no_value
# provide a state machine for the including class
# make sure to load class methods as well
# initialize persistence for the state machine
def self.included(base) #:nodoc:
base.extend AASM::ClassMethods
# do not overwrite existing state machines, which could have been created by
# inheritance, see class method inherited
AASM::StateMachineStore.register(base)
AASM::Persistence.load_persistence(base)
super
end
module ClassMethods
# make sure inheritance (aka subclassing) works with AASM
def inherited(base)
AASM::StateMachineStore.register(base, self)
super
end
# this is the entry point for all state and event definitions
def aasm(*args, &block)
if args[0].is_a?(Symbol) || args[0].is_a?(String)
# using custom name
state_machine_name = args[0].to_sym
options = args[1] || {}
else
# using the default state_machine_name
state_machine_name = :default
options = args[0] || {}
end
AASM::StateMachineStore.fetch(self, true).register(state_machine_name, AASM::StateMachine.new(state_machine_name))
# use a default despite the DSL configuration default.
# this is because configuration hasn't been setup for the AASM class but we are accessing a DSL option already for the class.
aasm_klass = options[:with_klass] || AASM::Base
raise ArgumentError, "The class #{aasm_klass} must inherit from AASM::Base!" unless aasm_klass.ancestors.include?(AASM::Base)
@aasm ||= Concurrent::Map.new
if @aasm[state_machine_name]
# make sure to use provided options
options.each do |key, value|
@aasm[state_machine_name].state_machine.config.send("#{key}=", value)
end
else
# create a new base
@aasm[state_machine_name] = aasm_klass.new(
self,
state_machine_name,
AASM::StateMachineStore.fetch(self, true).machine(state_machine_name),
options
)
end
@aasm[state_machine_name].instance_eval(&block) if block # new DSL
@aasm[state_machine_name]
end
end # ClassMethods
# this is the entry point for all instance-level access to AASM
def aasm(name=:default)
unless AASM::StateMachineStore.fetch(self.class, true).machine(name)
raise AASM::UnknownStateMachineError.new("There is no state machine with the name '#{name}' defined in #{self.class.name}!")
end
@aasm ||= Concurrent::Map.new
@aasm[name.to_sym] ||= AASM::InstanceBase.new(self, name.to_sym)
end
def initialize_dup(other)
@aasm = Concurrent::Map.new
super
end
private
# Takes args and a from state and removes the first
# element from args if it is a valid to_state for
# the event given the from_state
def process_args(event, from_state, *args)
# If the first arg doesn't respond to to_sym then
# it isn't a symbol or string so it can't be a state
# name anyway
return args unless args.first.respond_to?(:to_sym)
if event.transitions_from_state(from_state).map(&:to).flatten.include?(args.first)
return args[1..-1]
end
return args
end
def aasm_fire_event(state_machine_name, event_name, options, *args, &block)
event = self.class.aasm(state_machine_name).state_machine.events[event_name]
begin
old_state = aasm(state_machine_name).state_object_for_name(aasm(state_machine_name).current_state)
fire_default_callbacks(event, *process_args(event, aasm(state_machine_name).current_state, *args))
if may_fire_to = event.may_fire?(self, *args)
fire_exit_callbacks(old_state, *process_args(event, aasm(state_machine_name).current_state, *args))
if new_state_name = event.fire(self, {:may_fire => may_fire_to}, *args)
aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args, &block)
else
aasm_failed(state_machine_name, event_name, old_state, event.failed_callbacks)
end
else
aasm_failed(state_machine_name, event_name, old_state, event.failed_callbacks)
end
rescue StandardError => e
event.fire_callbacks(:error, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) ||
event.fire_global_callbacks(:error_on_all_events, self, e, *process_args(event, aasm(state_machine_name).current_state, *args)) ||
raise(e)
false
ensure
event.fire_callbacks(:ensure, self, *process_args(event, aasm(state_machine_name).current_state, *args))
event.fire_global_callbacks(:ensure_on_all_events, self, *process_args(event, aasm(state_machine_name).current_state, *args))
end
end
def fire_default_callbacks(event, *processed_args)
event.fire_global_callbacks(
:before_all_events,
self,
*processed_args
)
# new event before callback
event.fire_callbacks(
:before,
self,
*processed_args
)
end
def fire_exit_callbacks(old_state, *processed_args)
old_state.fire_callbacks(:before_exit, self, *processed_args)
old_state.fire_callbacks(:exit, self, *processed_args)
end
def aasm_fired(state_machine_name, event, old_state, new_state_name, options, *args)
persist = options[:persist]
new_state = aasm(state_machine_name).state_object_for_name(new_state_name)
callback_args = process_args(event, aasm(state_machine_name).current_state, *args)
new_state.fire_callbacks(:before_enter, self, *callback_args)
new_state.fire_callbacks(:enter, self, *callback_args) # TODO: remove for AASM 4?
persist_successful = true
if persist
persist_successful = aasm(state_machine_name).set_current_state_with_persistence(new_state_name)
if persist_successful
yield if block_given?
event.fire_callbacks(:before_success, self, *callback_args)
event.fire_transition_callbacks(self, *process_args(event, old_state.name, *args))
event.fire_callbacks(:success, self, *callback_args)
end
else
aasm(state_machine_name).current_state = new_state_name
yield if block_given?
end
binding_event = event.options[:binding_event]
if binding_event
__send__("#{binding_event}#{'!' if persist}")
end
if persist_successful
old_state.fire_callbacks(:after_exit, self, *callback_args)
new_state.fire_callbacks(:after_enter, self, *callback_args)
event.fire_callbacks(
:after,
self,
*process_args(event, old_state.name, *args)
)
event.fire_global_callbacks(
:after_all_events,
self,
*process_args(event, old_state.name, *args)
)
self.aasm_event_fired(event.name, old_state.name, aasm(state_machine_name).current_state) if self.respond_to?(:aasm_event_fired)
else
self.aasm_event_failed(event.name, old_state.name) if self.respond_to?(:aasm_event_failed)
end
persist_successful
end
def aasm_failed(state_machine_name, event_name, old_state, failures = [])
if self.respond_to?(:aasm_event_failed)
self.aasm_event_failed(event_name, old_state.name)
end
if AASM::StateMachineStore.fetch(self.class, true).machine(state_machine_name).config.whiny_transitions
raise AASM::InvalidTransition.new(self, event_name, state_machine_name, failures)
else
false
end
end
end