2018-01-11

AASMコードリーディング

状態遷移の管理を行う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
このエントリーをはてなブックマークに追加