lib/aasm/aasm.rb

Summary

Maintainability
B
6 hrs
Test Coverage
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