状態遷移の管理を行うAASMのコードリーディングをしました。バージョンは4.12.3です
まずはActiveRecordで使われるこのパターンで追ってみます
class Hoge < ApplicationRecord
include AASM
aasm column: :state do
state :disabled, initial: true
state :enabled
event :enable do
transitions from: :disabled, to: :enabled
end
end
end
AASMをincludeするとAASM::ClassMethodsのメソッドがクラスメソッドとして定義されます
module AASM
...
def self.included(base) #:nodoc:
base.extend AASM::ClassMethods
AASM::StateMachineStore.register(base)
AASM::Persistence.load_persistence(base)
super
end
AASM::Persitstence.load_persistenceメソッドでincludeするクラスに応じてさらにモジュールをincludeします。ActiveRecordの場合は、AASM::Persistence::ActiveRecordPersistenceがincludeされます。
module AASM
module Persistence
class << self
def load_persistence(base)
# Use a fancier auto-loading thingy, perhaps. When there are more persistence engines.
hierarchy = base.ancestors.map {|klass| klass.to_s}
if hierarchy.include?("ActiveRecord::Base")
require_persistence :active_record
include_persistence base, :active_record
elsif hierarchy.include?("Mongoid::Document")
require_persistence :mongoid
include_persistence base, :mongoid
elsif hierarchy.include?("Sequel::Model")
...
AASM::ClassMethodsはaasmメソッドを含んでいるので、これを使って状態遷移の定義ができます。argsにcolumn: :stateを定義した場合、state_machine_nameは:default、optionsにcolumn: :stateのハッシュが入ります
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
...
# 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]
...
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
AASM::Baseのインスタンスのinstance_evalによってaasmのブロックが評価されます。aasmのブロック内で記述できるstateやeventなどのメソッドはAASM::Baseのインスタンスメソッドになります。
stateとeventはそれぞれ以下のように定義されています。stateはAASM::StateMachineにステートを追加したり#{name}?
のメソッドやSTATE_#{name.upcase}
の定数を動的に定義しています。
module AASM
class Base
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
また、 persistence/baseの方にもAASM::Baseがオープンクラスによって定義されていて、stateメソッドがscopeを作成するようになっています。
module AASM
class Base
# make sure to create a (named) scope for each state
def state_with_scope(*args)
names = state_without_scope(*args)
names.each { |name| create_scope(name) if create_scope?(name) }
end
alias_method :state_without_scope, :state
alias_method :state, :state_with_scope
private
def create_scope?(name)
@state_machine.config.create_scopes && !@klass.respond_to?(name) && @klass.respond_to?(:aasm_create_scope)
end
def create_scope(name)
@klass.aasm_create_scope(@name, name)
end
end # Base
aasm_create_scopeによってscopeが作られます。 これによってHoge.active
のようなstate値を使ったscopeが定義されることになります
module AASM
module Persistence
module ActiveRecordPersistence
...
module ClassMethods
def aasm_create_scope(state_machine_name, scope_name)
conditions = {
table_name => { aasm(state_machine_name).attribute_name => scope_name.to_s }
}
if ActiveRecord::VERSION::MAJOR >= 3
class_eval do
scope scope_name, lambda { where(conditions) }
end
else
class_eval do
named_scope scope_name, :conditions => conditions
end
end
end
end
eventは#{name}!
や#{name}
のメソッドを動的に定義します。
module AASM
class Base
# 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
# 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
これらのメソッドを呼び出すと、AASM::StateMachine#add_eventにeventメソッドのブロックが渡されます。渡されたブロックはDslHelper.add_options_from_dslに渡されて、Proxy#instance_evalによって評価されます。eventメソッドのブロック内のtransitionsメソッドはプロキシされてAASM::Core::Eventインスタンスのコンテキストで実行されます。
module DslHelper
class Proxy
attr_accessor :options
...
def method_missing(name, *args, &block)
if @valid_keys.include?(name)
options[name] = Array(options[name])
options[name] << block if block
options[name] += Array(args)
else
@source.send name, *args, &block
end
end
end
def add_options_from_dsl(options, valid_keys, &block)
proxy = Proxy.new(options, valid_keys, self)
proxy.instance_eval(&block)
proxy.options
end
end
transitionsは@transitions変数内にAASM::Core::Transitionインスタンスを格納します
module AASM::Core
class Event
include DslHelper
## DSL interface
def transitions(definitions=nil, &block)
if definitions # define new transitions
# Create a separate transition for each from-state to the given state
Array(definitions[:from]).each do |s|
@transitions << AASM::Core::Transition.new(self, attach_event_guards(definitions.merge(:from => s.to_sym)), &block)
end
# Create a transition if :to is specified without :from (transitions from ANY state)
if !definitions[:from] && definitions[:to]
@transitions << AASM::Core::Transition.new(self, attach_event_guards(definitions), &block)
end
end
@transitions
end
eventによって定義されるメソッドを実行するとaasm_fire_eventが呼び出されます。aasm_fire_eventではコールバックを実行しつつ状態を変更します
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)
...
if may_fire_to = event.may_fire?(self, *args)
old_state.fire_callbacks(:before_exit, self,
*process_args(event, aasm(state_machine_name).current_state, *args))
old_state.fire_callbacks(:exit, self,
*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
...
end
end
aasm_firedメソッド内でもコールバックを実行しています。このメソッド内のAASM::InstanceBase#set_current_state_with_persistenceが状態の変更・保存を行っています。
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)
...
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)
event.fire_transition_callbacks(self, *process_args(event, old_state.name, *args))
event.fire_callbacks(:success, self)
end
else
aasm(state_machine_name).current_state = new_state_name
yield if block_given?
end
...
persist_successful
end
Hoge.new.aasm.human_state
といったようにモデルのaasmインスタンスメソッドを介して状態に関するメソッドを呼び出せますが、このaasmメソッドで返されるのがAASM::InstanceBaseのインスタンスです。set_current_state_with_persistenceはaasm_write_stateメソッドを呼び出して状態を変更・保存します。
module AASM
class InstanceBase
...
def set_current_state_with_persistence(state)
save_success = @instance.aasm_write_state(state, @name)
self.current_state = state if save_success
save_success
end
end
end