SkillAgentSearch skills...

Aasm

AASM - State machines for Ruby classes (plain Ruby, ActiveRecord, Mongoid, NoBrainer, Dynamoid)

Install / Use

/learn @aasm/Aasm
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

AASM - Ruby state machines

Gem Version Build Status Code Climate codecov

Index

This package contains AASM, a library for adding finite state machines to Ruby classes.

AASM started as the acts_as_state_machine plugin but has evolved into a more generic library that no longer targets only ActiveRecord models. It currently provides adapters for many ORMs but it can be used for any Ruby class, no matter what parent class it has (if any).

Upgrade from version 3 to 4

Take a look at the README_FROM_VERSION_3_TO_4 for details how to switch from version 3.x to 4.0 of AASM.

Usage

Adding a state machine is as simple as including the AASM module and start defining states and events together with their transitions:

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

    event :sleep do
      transitions from: [:running, :cleaning], to: :sleeping
    end
  end

end

This provides you with a couple of public methods for instances of the class Job:

job = Job.new
job.sleeping? # => true
job.may_run?  # => true
job.run
job.running?  # => true
job.sleeping? # => false
job.may_run?  # => false
job.run       # => raises AASM::InvalidTransition

If you don't like exceptions and prefer a simple true or false as response, tell AASM not to be whiny:

class Job
  ...
  aasm whiny_transitions: false do
    ...
  end
end

job.running?  # => true
job.may_run?  # => false
job.run       # => false

When firing an event, you can pass a block to the method, it will be called only if the transition succeeds :

  job.run do
    job.user.notify_job_ran # Will be called if job.may_run? is true
  end

Callbacks

You can define a number of callbacks for your events, transitions and states. These methods, Procs or classes will be called when certain criteria are met, like entering a particular state:

class Job
  include AASM

  aasm do
    state :sleeping, initial: true, before_enter: :do_something
    state :running, before_enter: Proc.new { do_something && notify_somebody }
    state :finished

    after_all_transitions :log_status_change

    event :run, after: :notify_somebody do
      before do
        log('Preparing to run')
      end

      transitions from: :sleeping, to: :running, after: Proc.new {|*args| set_process(*args) }
      transitions from: :running, to: :finished, after: LogRunTime
    end

    event :sleep do
      after do
        ...
      end
      error do |e|
        ...
      end
      transitions from: :running, to: :sleeping
    end
  end

  def log_status_change
    puts "changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})"
  end

  def set_process(name)
    ...
  end

  def do_something
    ...
  end

  def notify_somebody
    ...
  end

end

class LogRunTime
  def call
    log "Job was running for X seconds"
  end
end

In this case do_something is called before actually entering the state sleeping, while notify_somebody is called after the transition run (from sleeping to running) is finished.

AASM will also initialize LogRunTime and run the call method for you after the transition from running to finished in the example above. You can pass arguments to the class by defining an initialize method on it, like this:

Note that Procs are executed in the context of a record, it means that you don't need to expect the record as an argument, just call the methods you need.

class LogRunTime
  # optional args parameter can be omitted, but if you define initialize
  # you must accept the model instance as the first parameter to it.
  def initialize(job, args = {})
    @job = job
  end

  def call
    log "Job was running for #{@job.run_time} seconds"
  end
end

Parameters

You can pass parameters to events:

  job = Job.new
  job.run(:defragmentation)

All guards and after callbacks will receive these parameters. In this case set_process would be called with :defragmentation argument.

If the first argument to the event is a state (e.g. :running or :finished), the first argument is consumed and the state machine will attempt to transition to that state. Add comma separated parameter for guards and callbacks

  job = Job.new
  job.run(:running, :defragmentation)

In this case set_process won't be called, job will transition to running state and callback will receive :defragmentation as parameter

Error Handling

In case of an error during the event processing the error is rescued and passed to :error callback, which can handle it or re-raise it for further propagation.

Also, you can define a method that will be called if any event fails:

def aasm_event_failed(event_name, old_state_name)
  # use custom exception/messages, report metrics, etc
end

During the transition's :after callback (and reliably only then, or in the global after_all_transitions callback) you can access the originating state (the from-state) and the target state (the to state), like this:

  def set_process(name)
    logger.info "from #{aasm.from_state} to #{aasm.to_state}"
  end

Lifecycle

Here you can see a list of all possible callbacks, together with their order of calling:

begin
  event           before_all_events
  event           before
  event           guards
  transition      guards
  old_state       before_exit
  old_state       exit
                  after_all_transitions
  transition      after
  new_state       before_enter
  new_state       enter
  ...update state...
  event           before_success      # if persist successful
  transition      success             # if persist successful, database update not guaranteed
  event           success             # if persist successful, database update not guaranteed
  old_state       after_exit
  new_state       after_enter
  event           after
  event           after_all_events
rescue
  event           error
  event           error_on_all_events
ensure
  event           ensure
  event           ensure_on_all_events
end

Use event's after_commit callback if it should be fired after database update.

The current event triggered

While running the callbacks you can easily retrieve the name of the event triggered by using aasm.current_event:

  # taken the example callback from above
  def do_something
    puts "triggered #{aasm.current_event}"
  end

and then

  job = Job.new

  # without bang
  job.sleep # => triggered :sleep

  # with bang
  job.sleep! # => triggered :sleep!

Guards

Let's assume you want to allow particular transitions only if a defined condition is given. For this you can set up a guard per transition, which will run before actually running the transition. If the guard returns false the transition will be denied (raising AASM::InvalidTransition):

class Cleaner
  include AASM

  aasm do
    state :idle, initial: true
    state :cleaning

    event :clean do
      transitions from: :idle, to: :cleaning, guard: :cleaning_needed?
    end

    event :clean_if_needed do
      transitions from: :idle, to: :cleaning do
        guard do
          cleaning_needed?
        end
      end
      transitions from: :idle, to: :idle
    end

    event :clean_if_dirty do
      transitions from: :idle, to: :cleaning, guard: :if_dirty?
    end
  end

  def cleaning_needed?
    false
  end

  def if_dirty?(status)
    status == :dirty
  end

Related Skills

View on GitHub
GitHub Stars5.2k
CategoryDevelopment
Updated1d ago
Forks647

Languages

Ruby

Security Score

100/100

Audited on Apr 1, 2026

No findings