require 'logger'
module AASM
class Base
attr_reader :klass, :state_machine
def initialize(klass, name, state_machine, options={}, &block)
@klass = klass
@name = name
# @state_machine = klass.aasm(@name).state_machine
@state_machine = state_machine
@state_machine.config.column ||= (options[:column] || default_column).to_sym
# @state_machine.config.column = options[:column].to_sym if options[:column] # master
@options = options
# let's cry if the transition is invalid
configure :whiny_transitions, true
# create named scopes for each state
configure :create_scopes, true
# don't store any new state if the model is invalid (in ActiveRecord)
configure :skip_validation_on_save, false
# raise if the model is invalid (in ActiveRecord)
configure :whiny_persistence, false
# Use transactions (in ActiveRecord)
configure :use_transactions, true
# use requires_new for nested transactions (in ActiveRecord)
configure :requires_new_transaction, true
# use pessimistic locking (in ActiveRecord)
# true for FOR UPDATE lock
# string for a specific lock type i.e. FOR UPDATE NOWAIT
configure :requires_lock, false
# automatically set `"#{state_name}_at" = ::Time.now` on state changes
configure :timestamps, false
# set to true to forbid direct assignment of aasm_state column (in ActiveRecord)
configure :no_direct_assignment, false
# allow a AASM::Base sub-class to be used for state machine
configure :with_klass, AASM::Base
configure :enum, nil
# Set to true to namespace reader methods and constants
configure :namespace, false
# Configure a logger, with default being a Logger to STDERR
configure :logger, Logger.new(STDERR)
# setup timestamp-setting callback if enabled
setup_timestamps(@name)
# make sure to raise an error if no_direct_assignment is enabled
# and attribute is directly assigned though
setup_no_direct_assignment(@name)
end
# This method is both a getter and a setter
def attribute_name(column_name=nil)
if column_name
@state_machine.config.column = column_name.to_sym
else
@state_machine.config.column ||= :aasm_state
end
@state_machine.config.column
end
def initial_state(new_initial_state=nil)
if new_initial_state
@state_machine.initial_state = new_initial_state
else
@state_machine.initial_state
end
end
# define a state
# args
# [0] state
# [1] options (or nil)
# or
# [0] state
# [1..] state
def state(*args)
names, options = interpret_state_args(args)
names.each do |name|
@state_machine.add_state(name, klass, options)
aasm_name = @name.to_sym
state = name.to_sym
method_name = namespace? ? "#{namespace}_#{name}" : name
safely_define_method klass, "#{method_name}?", -> do
aasm(aasm_name).current_state == state
end
const_name = namespace? ? "STATE_#{namespace.upcase}_#{name.upcase}" : "STATE_#{name.upcase}"
unless klass.const_defined?(const_name)
klass.const_set(const_name, name)
end
end
end
# define an event
def event(name, options={}, &block)
@state_machine.add_event(name, options, &block)
aasm_name = @name.to_sym
event = name.to_sym
# an addition over standard aasm so that, before firing an event, you can ask
# may_event? and get back a boolean that tells you whether the guard method
# on the transition will let this happen.
safely_define_method klass, "may_#{name}?", ->(*args) do
aasm(aasm_name).may_fire_event?(event, *args)
end
safely_define_method klass, "#{name}!", ->(*args, &block) do
aasm(aasm_name).current_event = :"#{name}!"
aasm_fire_event(aasm_name, event, {:persist => true}, *args, &block)
end
safely_define_method klass, name, ->(*args, &block) do
aasm(aasm_name).current_event = event
aasm_fire_event(aasm_name, event, {:persist => false}, *args, &block)
end
skip_instance_level_validation(event, name, aasm_name, klass)
# Create aliases for the event methods. Keep the old names to maintain backwards compatibility.
if namespace?
klass.send(:alias_method, "may_#{name}_#{namespace}?", "may_#{name}?")
klass.send(:alias_method, "#{name}_#{namespace}!", "#{name}!")
klass.send(:alias_method, "#{name}_#{namespace}", name)
end
end
def after_all_transitions(*callbacks, &block)
@state_machine.add_global_callbacks(:after_all_transitions, *callbacks, &block)
end
def after_all_transactions(*callbacks, &block)
@state_machine.add_global_callbacks(:after_all_transactions, *callbacks, &block)
end
def before_all_transactions(*callbacks, &block)
@state_machine.add_global_callbacks(:before_all_transactions, *callbacks, &block)
end
def before_all_events(*callbacks, &block)
@state_machine.add_global_callbacks(:before_all_events, *callbacks, &block)
end
def after_all_events(*callbacks, &block)
@state_machine.add_global_callbacks(:after_all_events, *callbacks, &block)
end
def error_on_all_events(*callbacks, &block)
@state_machine.add_global_callbacks(:error_on_all_events, *callbacks, &block)
end
def ensure_on_all_events(*callbacks, &block)
@state_machine.add_global_callbacks(:ensure_on_all_events, *callbacks, &block)
end
def states
@state_machine.states
end
def events
@state_machine.events.values
end
# aasm.event(:event_name).human?
def human_event_name(event) # event_name?
AASM::Localizer.new.human_event_name(klass, event)
end
def states_for_select
states.map { |state| state.for_select }
end
def from_states_for_state(state, options={})
if options[:transition]
@state_machine.events[options[:transition]].transitions_to_state(state).flatten.map(&:from).flatten
else
events.map {|e| e.transitions_to_state(state)}.flatten.map(&:from).flatten
end
end
private
def default_column
@name.to_sym == :default ? :aasm_state : @name.to_sym
end
def configure(key, default_value)
if @options.key?(key)
@state_machine.config.send("#{key}=", @options[key])
elsif @state_machine.config.send(key).nil?
@state_machine.config.send("#{key}=", default_value)
end
end
def safely_define_method(klass, method_name, method_definition)
# Warn if method exists and it did not originate from an enum
if klass.method_defined?(method_name) &&
! ( @state_machine.config.enum &&
klass.respond_to?(:defined_enums) &&
klass.defined_enums.values.any?{ |methods|
methods.keys{| enum | enum + '?' == method_name }
})
unless AASM::Configuration.hide_warnings
@state_machine.config.logger.warn "#{klass.name}: overriding method '#{method_name}'!"
end
end
klass.send(:define_method, method_name, method_definition).tap do |sym|
apply_ruby2_keyword(klass, sym)
end
end
def apply_ruby2_keyword(klass, sym)
if RUBY_VERSION >= '2.7.1'
if klass.instance_method(sym).parameters.find { |type, _| type.to_s.start_with?('rest') }
# If there is a place where you are receiving in *args, do ruby2_keywords.
klass.module_eval do
ruby2_keywords sym
end
end
end
end
def namespace?
!!@state_machine.config.namespace
end
def namespace
if @state_machine.config.namespace == true
@name
else
@state_machine.config.namespace
end
end
def interpret_state_args(args)
if args.last.is_a?(Hash) && args.size == 2
[[args.first], args.last]
elsif args.size > 0
[args, {}]
else
raise "count not parse states: #{args}"
end
end
def skip_instance_level_validation(event, name, aasm_name, klass)
# Overrides the skip_validation config for an instance (If skip validation is set to false in original config) and
# restores it back to the original value after the event is fired.
safely_define_method klass, "#{name}_without_validation!", ->(*args, &block) do
original_config = AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save
begin
AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save = true unless original_config
aasm(aasm_name).current_event = :"#{name}!"
aasm_fire_event(aasm_name, event, {:persist => true}, *args, &block)
ensure
AASM::StateMachineStore.fetch(self.class, true).machine(aasm_name).config.skip_validation_on_save = original_config
end
end
end
def setup_timestamps(aasm_name)
return unless @state_machine.config.timestamps
after_all_transitions do
if self.class.aasm(:"#{aasm_name}").state_machine.config.timestamps
ts_setter = "#{aasm(aasm_name).to_state}_at="
respond_to?(ts_setter) && send(ts_setter, ::Time.now)
end
end
end
def setup_no_direct_assignment(aasm_name)
return unless @state_machine.config.no_direct_assignment
@klass.send(:define_method, "#{@state_machine.config.column}=") do |state_name|
if self.class.aasm(:"#{aasm_name}").state_machine.config.no_direct_assignment
raise AASM::NoDirectAssignmentError.new('direct assignment of AASM column has been disabled (see AASM configuration for this class)')
else
super(state_name)
end
end
end
end
end