Using aasm in service object

Kamil Baćkowski

What is aasm ?

Library for adding finite state machines to Ruby classes.

Example usage

class Job
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running, :cleaning

    event :run do
      transitions :from => :sleeping, :to => :running
    end

    event :clean do
      transitions :from => :running, :to => :cleaning
    end
  end
end

Our scenario

class Account < ApplicationRecord
  enum state: { step1: 'step1', step2: 'step2',
                step3: 'step3', finished: 'finished' }
end

Service object

class AccountManager
  include AASM, ActiveModel::Model
  attr_accessor :account, :name, :country, :language

  validates :name, presence: true, if: :step1?
  validates :country, presence: true, if: :step2?
  validates :language, presence: true, if: :step3?

  aasm do
    state :step1, initial: true
    state :step2, :step3, :finished

    after_all_transitions :update_account_state

    event :next_step do
      transitions from: :step1, to: :step2
      transitions from: :step2, to: :step3
      transitions from: :step3, to: :finished
    end
  end

  def initialize(attributes = {})
    super
    aasm_write_state_without_persistence(account.state.to_sym)
  end

  def save
    if valid?
      account.update! send(STEP_AVAILABLE_VALUES.fetch(aasm.current_state))
    else
      false
    end
  end

  private

  def update_account_state
    account.update_column(:state, aasm.to_state)
  end
end

Controller

class AccountsController < ApplicationController
  def new
    @account = Account.new
  end

  def edit
    @account = Account.find(params[:id])
  end

  def create
    @account = Account.new
    account_manager = AccountManager.new(account_params.merge(account: @account))
    redirect_to edit_account_path(@account) if account_manager.save && account_manager.next_step
  end

  def update
    @account = Account.find(params[:id])
    account_manager = AccountManager.new(account_params.merge(account: @account))
    redirect_to edit_account_path(@account) if account_manager.save && account_manager.next_step
  end

  private

  def account_params
    params.require(:account).permit(:name, :country, :language)
  end
end

Advantages

  • Proper logic separation
  • Easy to maintain
  • Simple to unit test
  • Can bypass aasm transitions in tests

Summary

Always use aasm oustide of ActiveRecord models.

Questions ?