GithubHelp home page GithubHelp logo

aasm / aasm Goto Github PK

View Code? Open in Web Editor NEW
5.0K 60.0 628.0 1.93 MB

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

License: MIT License

Ruby 99.79% Dockerfile 0.21%
state-machine ruby aasm transition activerecord mongoid hacktoberfest rails

aasm's Introduction

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
end

job = Cleaner.new
job.may_clean?            # => false
job.clean                 # => raises AASM::InvalidTransition
job.may_clean_if_needed?  # => true
job.clean_if_needed!      # idle

job.clean_if_dirty(:clean) # => raises AASM::InvalidTransition
job.clean_if_dirty(:dirty) # => true

You can even provide a number of guards, which all have to succeed to proceed

    def walked_the_dog?; ...; end

    event :sleep do
      transitions from: :running, to: :sleeping, guards: [:cleaning_needed?, :walked_the_dog?]
    end

If you want to provide guards for all transitions within an event, you can use event guards

    event :sleep, guards: [:walked_the_dog?] do
      transitions from: :running, to: :sleeping, guards: [:cleaning_needed?]
      transitions from: :cleaning, to: :sleeping
    end

If you prefer a more Ruby-like guard syntax, you can use if and unless as well:

    event :clean do
      transitions from: :running, to: :cleaning, if: :cleaning_needed?
    end

    event :sleep do
      transitions from: :running, to: :sleeping, unless: :cleaning_needed?
    end
  end

You can invoke a Class instead of a method if the Class responds to call

    event :sleep do
      transitions from: :running, to: :sleeping, guards: Dog
    end
  class Dog
    def call
      cleaning_needed? && walked?
    end
    ...
  end

Transitions

In the event of having multiple transitions for an event, the first transition that successfully completes will stop other transitions in the same event from being processed.

require 'aasm'

class Job
  include AASM

  aasm do
    state :stage1, initial: true
    state :stage2
    state :stage3
    state :completed

    event :stage1_completed do
      transitions from: :stage1, to: :stage3, guard: :stage2_completed?
      transitions from: :stage1, to: :stage2
    end
  end

  def stage2_completed?
    true
  end
end

job = Job.new
job.stage1_completed
job.aasm.current_state # stage3

You can define transition from any defined state by omitting from:

event :abort do
  transitions to: :aborted
end

Display name for state

You can define display name for state using :display option

class Job
  include AASM

  aasm do
    state :stage1, initial: true, display: 'First Stage'
    state :stage2
    state :stage3
  end
end

job = Job.new
job.aasm.human_state

Multiple state machines per class

Multiple state machines per class are supported. Be aware though that AASM has been built with one state machine per class in mind. Nonetheless, here's how to do it (see below). Please note that you will need to specify database columns for where your pertinent states will be stored - we have specified two columns move_state and work_state in the example below. See the Column name & migration section for further info.

class SimpleMultipleExample
  include AASM
  aasm(:move, column: 'move_state') do
    state :standing, initial: true
    state :walking
    state :running

    event :walk do
      transitions from: :standing, to: :walking
    end
    event :run do
      transitions from: [:standing, :walking], to: :running
    end
    event :hold do
      transitions from: [:walking, :running], to: :standing
    end
  end

  aasm(:work, column: 'work_state') do
    state :sleeping, initial: true
    state :processing

    event :start do
      transitions from: :sleeping, to: :processing
    end
    event :stop do
      transitions from: :processing, to: :sleeping
    end
  end
end

simple = SimpleMultipleExample.new

simple.aasm(:move).current_state
# => :standing
simple.aasm(:work).current_state
# => :sleeping

simple.start
simple.aasm(:move).current_state
# => :standing
simple.aasm(:work).current_state
# => :processing

Handling naming conflicts between multiple state machines

AASM doesn't prohibit to define the same event in more than one state machine. If no namespace is provided, the latest definition "wins" and overrides previous definitions. Nonetheless, a warning is issued: SimpleMultipleExample: overriding method 'run'!.

Alternatively, you can provide a namespace for each state machine:

class NamespacedMultipleExample
  include AASM
  aasm(:status) do
    state :unapproved, initial: true
    state :approved

    event :approve do
      transitions from: :unapproved, to: :approved
    end

    event :unapprove do
      transitions from: :approved, to: :unapproved
    end
  end

  aasm(:review_status, namespace: :review) do
    state :unapproved, initial: true
    state :approved

    event :approve do
      transitions from: :unapproved, to: :approved
    end

    event :unapprove do
      transitions from: :approved, to: :unapproved
    end
  end
end

namespaced = NamespacedMultipleExample.new

namespaced.aasm(:status).current_state
# => :unapproved
namespaced.aasm(:review_status).current_state
# => :unapproved
namespaced.approve_review
namespaced.aasm(:review_status).current_state
# => :approved

All AASM class- and instance-level aasm methods accept a state machine selector. So, for example, to use inspection on a class level, you have to use

SimpleMultipleExample.aasm(:move).states.map(&:name)
# => [:standing, :walking, :running]

Binding event

Allow an event to be bound to another

class Example
  include AASM

  aasm(:work) do
    state :sleeping, initial: true
    state :processing

    event :start do
      transitions from: :sleeping, to: :processing
    end
    event :stop do
      transitions from: :processing, to: :sleeping
    end
  end

  aasm(:question) do
    state :answered, initial: true
    state :asked

    event :ask, binding_event: :start do
      transitions from: :answered, to: :asked
    end
    event :answer, binding_event: :stop do
      transitions from: :asked, to: :answered
    end
  end
end

example = Example.new
example.aasm(:work).current_state #=> :sleeping
example.aasm(:question).current_state #=> :answered
example.ask
example.aasm(:work).current_state #=> :processing
example.aasm(:question).current_state #=> :asked

Auto-generated Status Constants

AASM automatically generates constants for each status so you don't have to explicitly define them.

class Foo
  include AASM

  aasm do
    state :initialized
    state :calculated
    state :finalized
  end
end

> Foo::STATE_INITIALIZED
#=> :initialized
> Foo::STATE_CALCULATED
#=> :calculated

Extending AASM

AASM allows you to easily extend AASM::Base for your own application purposes.

Let's suppose we have common logic across many AASM models. We can embody this logic in a sub-class of AASM::Base.

class CustomAASMBase < AASM::Base
  # A custom transition that we want available across many AASM models.
  def count_transitions!
    klass.class_eval do
      aasm with_klass: CustomAASMBase do
        after_all_transitions :increment_transition_count
      end
    end
  end

  # A custom annotation that we want available across many AASM models.
  def requires_guards!
    klass.class_eval do
      attr_reader :authorizable_called,
        :transition_count,
        :fillable_called

      def authorizable?
        @authorizable_called = true
      end

      def fillable?
        @fillable_called = true
      end

      def increment_transition_count
        @transition_count ||= 0
        @transition_count += 1
      end
    end
  end
end

When we declare our model that has an AASM state machine, we simply declare the AASM block with a :with_klass key to our own class.

class SimpleCustomExample
  include AASM

  # Let's build an AASM state machine with our custom class.
  aasm with_klass: CustomAASMBase do
    requires_guards!
    count_transitions!

    state :initialised, initial: true
    state :filled_out
    state :authorised

    event :fill_out do
      transitions from: :initialised, to: :filled_out, guard: :fillable?
    end
    event :authorise do
      transitions from: :filled_out, to: :authorised, guard: :authorizable?
    end
  end
end

ActiveRecord

AASM comes with support for ActiveRecord and allows automatic persisting of the object's state in the database.

Add gem 'after_commit_everywhere', '~> 1.0' to your Gemfile.

class Job < ActiveRecord::Base
  include AASM

  aasm do # default column: aasm_state
    state :sleeping, initial: true
    state :running

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

    event :sleep do
      transitions from: :running, to: :sleeping
    end
  end

end

Bang events

You can tell AASM to auto-save the object or leave it unsaved

job = Job.new
job.run   # not saved
job.run!  # saved

# or
job.aasm.fire(:run) # not saved
job.aasm.fire!(:run) # saved

Saving includes running all validations on the Job class. If whiny_persistence flag is set to true, exception is raised in case of failure. If whiny_persistence flag is set to false, methods with a bang return true if the state transition is successful or false if an error occurs.

If you want make sure the state gets saved without running validations (and thereby maybe persisting an invalid object state), simply tell AASM to skip the validations. Be aware that when skipping validations, only the state column will be updated in the database (just like ActiveRecord update_column is working).

class Job < ActiveRecord::Base
  include AASM

  aasm skip_validation_on_save: true do
    state :sleeping, initial: true
    state :running

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

    event :sleep do
      transitions from: :running, to: :sleeping
    end
  end

end

Also, you can skip the validation at instance level with some_event_name_without_validation! method. With this you have the flexibility of having validation for all your transitions by default and then skip it wherever required. Please note that only state column will be updated as mentioned in the above example.

job.run_without_validation!

If you want to make sure that the AASM column for storing the state is not directly assigned, configure AASM to not allow direct assignment, like this:

class Job < ActiveRecord::Base
  include AASM

  aasm no_direct_assignment: true do
    state :sleeping, initial: true
    state :running

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

end

resulting in this:

job = Job.create
job.aasm_state # => 'sleeping'
job.aasm_state = :running # => raises AASM::NoDirectAssignmentError
job.aasm_state # => 'sleeping'

Timestamps

You can tell AASM to try to write a timestamp whenever a new state is entered. If timestamps: true is set, AASM will look for a field named like the new state plus _at and try to fill it:

class Job < ActiveRecord::Base
  include AASM

  aasm timestamps: true do
    state :sleeping, initial: true
    state :running

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

resulting in this:

job = Job.create
job.running_at # => nil
job.run!
job.running_at # => 2020-02-20 20:00:00

Missing timestamp fields are silently ignored, so it is not necessary to have setters (such as ActiveRecord columns) for all states when using this option.

ActiveRecord enums

You can use enumerations in Rails 4.1+ for your state column:

class Job < ActiveRecord::Base
  include AASM

  enum state: {
    sleeping: 5,
    running: 99
  }

  aasm column: :state, enum: true do
    state :sleeping, initial: true
    state :running
  end
end

You can explicitly pass the name of the method which provides access to the enumeration mapping as a value of enum, or you can simply set it to true. In the latter case AASM will try to use pluralized column name to access possible enum states.

Furthermore, if your column has integer type (which is normally the case when you're working with Rails enums), you can omit :enum setting --- AASM auto-detects this situation and enabled enum support. If anything goes wrong, you can disable enum functionality and fall back to the default behavior by setting :enum to false.

Sequel

AASM also supports Sequel besides ActiveRecord, and Mongoid.

class Job < Sequel::Model
  include AASM

  aasm do # default column: aasm_state
    ...
  end
end

However it's not yet as feature complete as ActiveRecord. For example, there are scopes defined yet. See Automatic Scopes.

Dynamoid

Since version 4.8.0 AASM also supports Dynamoid as persistence ORM.

Mongoid

AASM also supports persistence to Mongodb if you're using Mongoid. Make sure to include Mongoid::Document before you include AASM.

class Job
  include Mongoid::Document
  include AASM
  field :aasm_state
  aasm do
    ...
  end
end

NoBrainer

AASM also supports persistence to RethinkDB if you're using Nobrainer. Make sure to include NoBrainer::Document before you include AASM.

class Job
  include NoBrainer::Document
  include AASM
  field :aasm_state
  aasm do
    ...
  end
end

Redis

AASM also supports persistence in Redis via Redis::Objects. Make sure to include Redis::Objects before you include AASM. Note that non-bang events will work as bang events, persisting the changes on every call.

class User
  include Redis::Objects
  include AASM

  aasm do
  end
end

Automatic Scopes

AASM will automatically create scope methods for each state in the model.

class Job < ActiveRecord::Base
  include AASM

  aasm do
    state :sleeping, initial: true
    state :running
    state :cleaning
  end

  def self.sleeping
    "This method name is already in use"
  end
end
class JobsController < ApplicationController
  def index
    @running_jobs = Job.running
    @recent_cleaning_jobs = Job.cleaning.where('created_at >=  ?', 3.days.ago)

    # @sleeping_jobs = Job.sleeping   #=> "This method name is already in use"
  end
end

If you don't need scopes (or simply don't want them), disable their creation when defining the AASM states, like this:

class Job < ActiveRecord::Base
  include AASM

  aasm create_scopes: false do
    state :sleeping, initial: true
    state :running
    state :cleaning
  end
end

Transaction support

Since version 3.0.13 AASM supports ActiveRecord transactions. So whenever a transition callback or the state update fails, all changes to any database record are rolled back. Mongodb does not support transactions.

There are currently 3 transactional callbacks that can be handled on the event, and 2 transactional callbacks for all events.

  event           before_all_transactions
  event           before_transaction
  event           aasm_fire_event (within transaction)
  event           after_commit (if event successful)
  event           after_transaction
  event           after_all_transactions

If you want to make sure a depending action happens only after the transaction is committed, use the after_commit callback along with the auto-save (bang) methods, like this:

class Job < ActiveRecord::Base
  include AASM

  aasm do
    state :sleeping, initial: true
    state :running

    event :run, after_commit: :notify_about_running_job do
      transitions from: :sleeping, to: :running
    end
  end

  def notify_about_running_job
    ...
  end
end

job = Job.where(state: 'sleeping').first!
job.run! # Saves the model and triggers the after_commit callback

Note that the following will not run the after_commit callbacks because the auto-save method is not used:

job = Job.where(state: 'sleeping').first!
job.run
job.save! #notify_about_running_job is not run

Please note that :after_commit AASM callbacks behaves around custom implementation of transaction pattern rather than a real-life DB transaction. This fact still causes the race conditions and redundant callback calls within nested transaction. In order to fix that it's highly recommended to add gem 'after_commit_everywhere', '~> 1.0' to your Gemfile.

If you want to encapsulate state changes within an own transaction, the behavior of this nested transaction might be confusing. Take a look at ActiveRecord Nested Transactions if you want to know more about this. Nevertheless, AASM by default requires a new transaction transaction(requires_new: true). You can override this behavior by changing the configuration

class Job < ActiveRecord::Base
  include AASM

  aasm requires_new_transaction: false do
    ...
  end

  ...
end

which then leads to transaction(requires_new: false), the Rails default.

Additionally, if you do not want any of your ActiveRecord actions to be wrapped in a transaction, you can specify the use_transactions flag. This can be useful if you want want to persist things to the database that happen as a result of a transaction or callback, even when some error occurs. The use_transactions flag is true by default.

class Job < ActiveRecord::Base
  include AASM

  aasm use_transactions: false do
    ...
  end

  ...
end

Pessimistic Locking

AASM supports ActiveRecord pessimistic locking via with_lock for database persistence layers.

Option Purpose
false (default) No lock is obtained
true Obtain a blocking pessimistic lock e.g. FOR UPDATE
String Obtain a lock based on the SQL string e.g. FOR UPDATE NOWAIT
class Job < ActiveRecord::Base
  include AASM

  aasm requires_lock: true do
    ...
  end

  ...
end
class Job < ActiveRecord::Base
  include AASM

  aasm requires_lock: 'FOR UPDATE NOWAIT' do
    ...
  end

  ...
end

Column name & migration

As a default AASM uses the column aasm_state to store the states. You can override this by defining your favorite column name, using :column like this:

class Job < ActiveRecord::Base
  include AASM

  aasm column: :my_state do
    ...
  end

  aasm :another_state_machine, column: :second_state do
    ...
  end
end

Whatever column name is used, make sure to add a migration to provide this column (of type string). Do not add default value for column at the database level. If you add default value in database then AASM callbacks on the initial state will not be fired upon instantiation of the model.

class AddJobState < ActiveRecord::Migration
  def self.up
    add_column :jobs, :aasm_state, :string
  end

  def self.down
    remove_column :jobs, :aasm_state
  end
end

Log State Changes

Logging state change can be done using paper_trail gem

Example of implementation can be found here https://github.com/nitsujri/aasm-papertrail-example

Inspection

AASM supports query methods for states and events

Given the following Job class:

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, guard: :cleaning_needed?
    end

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

  def cleaning_needed?
    false
  end
end
# show all states
Job.aasm.states.map(&:name)
#=> [:sleeping, :running, :cleaning]

job = Job.new

# show all permitted states (from initial state)
job.aasm.states(permitted: true).map(&:name)
#=> [:running]

# List all the permitted transitions(event and state pairs) from initial state
job.aasm.permitted_transitions
#=> [{ :event => :run, :state => :running }]

job.run
job.aasm.states(permitted: true).map(&:name)
#=> [:sleeping]

# show all non permitted states
job.aasm.states(permitted: false).map(&:name)
#=> [:cleaning]

# show all possible (triggerable) events from the current state
job.aasm.events.map(&:name)
#=> [:clean, :sleep]

# show all permitted events
job.aasm.events(permitted: true).map(&:name)
#=> [:sleep]

# show all non permitted events
job.aasm.events(permitted: false).map(&:name)
#=> [:clean]

# show all possible events except a specific one
job.aasm.events(reject: :sleep).map(&:name)
#=> [:clean]

# list states for select
Job.aasm.states_for_select
#=> [["Sleeping", "sleeping"], ["Running", "running"], ["Cleaning", "cleaning"]]

# show permitted states with guard parameter
job.aasm.states({permitted: true}, guard_parameter).map(&:name)

Warning output

Warnings are by default printed to STDERR. If you want to log those warnings to another output, use

class Job
  include AASM

  aasm logger: Rails.logger do
    ...
  end
end

You can hide warnings by setting AASM::Configuration.hide_warnings = true

RubyMotion support

Now supports CodeDataQuery ! However I'm still in the process of submitting my compatibility updates to their repository. In the meantime you can use my fork, there may still be some minor issues but I intend to extensively use it myself, so fixes should come fast.

Warnings:

  • Due to RubyMotion Proc's lack of 'source_location' method, it may be harder to find out the origin of a "cannot transition from" error. I would recommend using the 'instance method symbol / string' way whenever possible when defining guardians and callbacks.

Testing

RSpec

AASM provides some matchers for RSpec:

  • transition_from,
  • have_state, allow_event
  • and allow_transition_to.
Installation Instructions:
  • Add require 'aasm/rspec' to your spec_helper.rb file.
Examples Of Usage in Rspec:
# classes with only the default state machine
job = Job.new
expect(job).to transition_from(:sleeping).to(:running).on_event(:run)
expect(job).not_to transition_from(:sleeping).to(:cleaning).on_event(:run)
expect(job).to have_state(:sleeping)
expect(job).not_to have_state(:running)
expect(job).to allow_event :run
expect(job).to_not allow_event :clean
expect(job).to allow_transition_to(:running)
expect(job).to_not allow_transition_to(:cleaning)
# on_event also accept multiple arguments
expect(job).to transition_from(:sleeping).to(:running).on_event(:run, :defragmentation)

# classes with multiple state machine
multiple = SimpleMultipleExample.new
expect(multiple).to transition_from(:standing).to(:walking).on_event(:walk).on(:move)
expect(multiple).to_not transition_from(:standing).to(:running).on_event(:walk).on(:move)
expect(multiple).to have_state(:standing).on(:move)
expect(multiple).not_to have_state(:walking).on(:move)
expect(multiple).to allow_event(:walk).on(:move)
expect(multiple).to_not allow_event(:hold).on(:move)
expect(multiple).to allow_transition_to(:walking).on(:move)
expect(multiple).to_not allow_transition_to(:running).on(:move)
expect(multiple).to transition_from(:sleeping).to(:processing).on_event(:start).on(:work)
expect(multiple).to_not transition_from(:sleeping).to(:sleeping).on_event(:start).on(:work)
expect(multiple).to have_state(:sleeping).on(:work)
expect(multiple).not_to have_state(:processing).on(:work)
expect(multiple).to allow_event(:start).on(:move)
expect(multiple).to_not allow_event(:stop).on(:move)
expect(multiple).to allow_transition_to(:processing).on(:move)
expect(multiple).to_not allow_transition_to(:sleeping).on(:move)
# allow_event also accepts arguments
expect(job).to allow_event(:run).with(:defragmentation)

Minitest

AASM provides assertions and rspec-like expectations for Minitest.

Assertions

List of supported assertions: assert_have_state, refute_have_state, assert_transitions_from, refute_transitions_from, assert_event_allowed, refute_event_allowed, assert_transition_to_allowed, refute_transition_to_allowed.

Examples Of Usage (Minitest):

Add require 'aasm/minitest' to your test_helper.rb file and use them like this:

# classes with only the default state machine
job = Job.new
assert_transitions_from job, :sleeping, to: :running, on_event: :run
refute_transitions_from job, :sleeping, to: :cleaning, on_event: :run
assert_have_state job, :sleeping
refute_have_state job, :running
assert_event_allowed job, :run
refute_event_allowed job, :clean
assert_transition_to_allowed job, :running
refute_transition_to_allowed job, :cleaning
# on_event also accept arguments
assert_transitions_from job, :sleeping, :defragmentation, to: :running, on_event: :run

# classes with multiple state machine
multiple = SimpleMultipleExample.new
assert_transitions_from multiple, :standing, to: :walking, on_event: :walk, on: :move
refute_transitions_from multiple, :standing, to: :running, on_event: :walk, on: :move
assert_have_state multiple, :standing, on: :move
refute_have_state multiple, :walking, on: :move
assert_event_allowed multiple, :walk, on: :move
refute_event_allowed multiple, :hold, on: :move
assert_transition_to_allowed multiple, :walking, on: :move
refute_transition_to_allowed multiple, :running, on: :move
assert_transitions_from multiple, :sleeping, to: :processing, on_event: :start, on: :work
refute_transitions_from multiple, :sleeping, to: :sleeping, on_event: :start, on: :work
assert_have_state multiple, :sleeping, on: :work
refute_have_state multiple, :processing, on: :work
assert_event_allowed multiple, :start, on: :move
refute_event_allowed multiple, :stop, on: :move
assert_transition_to_allowed multiple, :processing, on: :move
refute_transition_to_allowed multiple, :sleeping, on: :move
Expectations

List of supported expectations: must_transition_from, wont_transition_from, must_have_state, wont_have_state, must_allow_event, wont_allow_event, must_allow_transition_to, wont_allow_transition_to.

Add require 'aasm/minitest_spec' to your test_helper.rb file and use them like this:

# classes with only the default state machine
job = Job.new
job.must_transition_from :sleeping, to: :running, on_event: :run
job.wont_transition_from :sleeping, to: :cleaning, on_event: :run
job.must_have_state :sleeping
job.wont_have_state :running
job.must_allow_event :run
job.wont_allow_event :clean
job.must_allow_transition_to :running
job.wont_allow_transition_to :cleaning
# on_event also accept arguments
job.must_transition_from :sleeping, :defragmentation, to: :running, on_event: :run

# classes with multiple state machine
multiple = SimpleMultipleExample.new
multiple.must_transition_from :standing, to: :walking, on_event: :walk, on: :move
multiple.wont_transition_from :standing, to: :running, on_event: :walk, on: :move
multiple.must_have_state :standing, on: :move
multiple.wont_have_state :walking, on: :move
multiple.must_allow_event :walk, on: :move
multiple.wont_allow_event :hold, on: :move
multiple.must_allow_transition_to :walking, on: :move
multiple.wont_allow_transition_to :running, on: :move
multiple.must_transition_from :sleeping, to: :processing, on_event: :start, on: :work
multiple.wont_transition_from :sleeping, to: :sleeping, on_event: :start, on: :work
multiple.must_have_state :sleeping, on: :work
multiple.wont_have_state :processing, on: :work
multiple.must_allow_event :start, on: :move
multiple.wont_allow_event :stop, on: :move
multiple.must_allow_transition_to :processing, on: :move
multiple.wont_allow_transition_to :sleeping, on: :move

Manually from RubyGems.org

% gem install aasm

Or if you are using Bundler

# Gemfile
gem 'aasm'

Building your own gems

% rake build
% sudo gem install pkg/aasm-x.y.z.gem

Generators

After installing AASM you can run generator:

% rails generate aasm NAME [COLUMN_NAME]

Replace NAME with the Model name, COLUMN_NAME is optional(default is 'aasm_state'). This will create a model (if one does not exist) and configure it with aasm block. For ActiveRecord orm a migration file is added to add aasm state column to table.

Docker

Run test suite easily on docker

1. docker-compose build aasm
2. docker-compose run --rm aasm

Latest changes

Take a look at the CHANGELOG for details about recent changes to the current version.

Questions?

Feel free to

Maintainers

Stargazers over time

Stargazers over time

Warranty

This software is provided "as is" and without any express or implied warranties, including, without limitation, the implied warranties of merchantibility and fitness for a particular purpose.

License

Copyright (c) 2006-2017 Scott Barron

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

aasm's People

Contributors

alto avatar anilmaurya avatar aryk avatar bkon avatar bokmann avatar chriswoodrich avatar depfu[bot] avatar dubroe avatar etehtsea avatar hoyaboya avatar infotaku avatar jaredsmithse avatar joaovitor avatar johnnyshields avatar petergoldstein avatar petersen avatar pirj avatar ramn avatar reidmorrison avatar spicycode avatar stiff avatar stokarenko avatar the-spectator avatar ttilley avatar wildfalcon avatar y-yagi avatar yui-knk avatar yujideveloper avatar zacviandier avatar zilkey avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aasm's Issues

AR automatic scopes are not chainable

Trying to chain a method after an AASM scope breaks because AASM returns an instance of AASM::Base instead of an ActiveRecord::Relation.

ruby 2.0.0.p0, rails 4.0.0.rc1, rspec 2.13.1, aasm 3.0.17

➜  testin git:(master) ✗ cat app/models/limb.rb 
class Limb < ActiveRecord::Base
  include AASM
  aasm column: :state do
    state :resting, initial: true
  end
end
➜  testin git:(master) ✗ rails console         
Loading development environment (Rails 4.0.0.rc1)
>> Limb.resting.where(true)
NoMethodError: undefined method `where' for #<AASM::Base:0x007fef65425b10>
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/aasm-3.0.17/lib/aasm/persistence/base.rb:93:in `block in state_with_scope'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/activerecord-deprecated_finders-1.0.2/lib/active_record/deprecated_finders/base.rb:28:in `call'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/activerecord-deprecated_finders-1.0.2/lib/active_record/deprecated_finders/base.rb:28:in `call'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/activerecord-4.0.0.rc1/lib/active_record/scoping/named.rb:163:in `block (2 levels) in scope'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/activerecord-4.0.0.rc1/lib/active_record/relation.rb:246:in `scoping'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/activerecord-4.0.0.rc1/lib/active_record/scoping/named.rb:163:in `block in scope'
    from (irb):1
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/railties-4.0.0.rc1/lib/rails/commands/console.rb:90:in `start'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/railties-4.0.0.rc1/lib/rails/commands/console.rb:9:in `start'
    from /Users/lu/.rbenv/versions/2.0.0-p0/lib/ruby/gems/2.0.0/gems/railties-4.0.0.rc1/lib/rails/commands.rb:66:in `<top (required)>'
    from bin/rails:4:in `require'
    from bin/rails:4:in `<main>'

aasm fails tests

aasm_column :state
aasm_initial_state :pending
aasm_state :pending
aasm_state :current, :enter => :deliver
aasm_state :old, :enter => :end_now

aasm_event :activate do
transitions :from => :pending, :to => :current
end

aasm_event :expire do
transitions :from => :current, :to => :old
end

-------------- RSpec:

it "should transition from pending to current with activate! event" do
@my_obj.activate!
@my_obj.state.should eql('current')
end

it "should transition from current to old with expire! event" do
@my_obj.activate!
@my_obj.state.should eql('current')
@my_obj.expire!
@my_obj.state.should eql('old')
end


Although the expire even calls the callback 'end_now', it never transitions the model to the 'old' state. While on the contrary activate! does transition the state correctly. Removing the callback from 'old' doesn't help. I can't see any reason this would fail.

Dynamically load states?

Hi,

I have an app that uses a form-wizard that allows you to go forwards/backawards to form pages. The logic to keep up with the current page is a bit clunky and it sounds like aasm might be a good improvement. I would use a form page as a 'state', then use aasm to track what the next/previous pages are.

However, admin users can create form pages dynamically and these get stored in the database via ActiveRecord. How would you deal with this in aasm, where states are typically static?

Kim

aasm_states_for_select ignores aasm_initial_state

When Amodel.new is invoked the aasm state is not set yet. An ugly solution for this is to put the initial_state to the top of the array:

def aasm_states_for_select
  sm = AASM::StateMachine[self]
  states = sm.states.dup
  if istate = states.find{|s| s.name == sm.initial_state} then states.unshift(istate).uniq! end
  states.map { |state| state.for_select }
end

I get a deprecation warning with Rails 3

While running the application or my tests I keep seeing this messages:

  DEPRECATION WARNING: before_validation_on_create is deprecated. Please use before_validation(arguments, :on =>    :create. (called from included at /Users/chopi/.rvm/gems/ruby-1.8.7-p330@ziizoo/gems/rubyist-aasm-2.1.1/lib/aasm/persistence/active_record_persistence.rb:54)

  DEPRECATION WARNING: Base.named_scope has been deprecated, please use Base.scope instead. (called from aasm_state at /Users/chopi/.rvm/gems/ruby-1.8.7-p330@ziizoo/gems/rubyist-aasm-2.1.1/lib/aasm/persistence/active_record_persistence.rb:243)

  DEPRECATION WARNING: save(false) is deprecated, please give save(:validate => false) instead. (called from aasm_write_state at /Users/chopi/.rvm/gems/ruby-1.8.7-p330@ziizoo/gems/rubyist-aasm-2.1.1/lib/aasm/persistence/active_record_persistence.rb:196)

State methods not reflecting the current state

Hello. In AASM 3.0.16, the following code works like this:

my_object.state # => "not_posted"
my_object.state = "completed"
my_object.state # => "completed"
my_object.completed? # => true

In version 3.0.17, it no longer works. In the last line, the "completed?" method now returns "false" - still thinks it's in the "not_posted" state, even though calling the "state" attribute directly shows the correct value. I've also tried persisting the state to the database with the same result, i.e. reloading the object with "my_object.reload" still does not work with the "completed?" method.

If I'm in the Rails console, exiting the console and re-entering it is the only way to obtain the updated state when calling the method.

No database transaction wraps the state transition

Calling #delete! does not wrap a real database transaction around the transition. This is a problem if you have :on_transition callbacks to do real work, such as change the state of other entities. Having a problem deep down won't rollback the whole transition.

A partial solution is to manually wrap (in the controller or model) the state transition, but this is inconsistent, prone to error, and easy to forget:

def update
  model = Model.find(params[:id])
  model.transaction do
    model.transition!
  end
end

Undefined method 'aasm_column' in seeds.rb

If AASM model files are used in seeds.rb it doesn't work. it shows the message "undefined method aasm_column' for -- aasm_column(:state) rake aborted! undefined methodaasm_column' for #ActiveRecord::ConnectionAdapters::MysqlAdapter:0xb6ed0268

I'm using aasm (2.1.5) with Rails 2.3.5

undefined method ... for nil:NilClass on :before callback

Hello,
updating aasm from version 3.0.3 to version 3.0.17 breaks on :before callback in my application.

My code looks like this:

module Renewable

 def self.included(klass)

      klass.class_eval do
        include AASM
      end

      klass.aasm :column => 'state' do
        ....
        event :expire, :before => Proc.new{|s| s.expired_at = Time.now}, :after => :after_expiration_tasks do
          transitions :from => [:active], :to => :expired
        end

      end # aasm do

end # module Renewable

class License

  include Renewable
  ...

end

Then, in console:

> l = License.last
> l.expire!
NoMethodError: undefined method `expired_at=' for nil:NilClass
    from /Users/domenicovele/RubymineProjects/mater/app/models/billing/renewable/renewable.rb:85:in `block (2 levels) in included'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/event.rb:99:in `instance_exec'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/event.rb:99:in `invoke_callbacks'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/event.rb:46:in `fire_callbacks'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/aasm.rb:147:in `aasm_fire_event'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/persistence/active_record_persistence.rb:136:in `block in aasm_fire_event'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/activerecord-3.2.13/lib/active_record/connection_adapters/abstract/database_statements.rb:192:in `transaction'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/activerecord-3.2.13/lib/active_record/transactions.rb:208:in `transaction'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/activerecord-3.2.13/lib/active_record/transactions.rb:250:in `transaction'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/persistence/active_record_persistence.rb:135:in `aasm_fire_event'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/aasm-3.0.18/lib/aasm/base.rb:57:in `block in event'
    from (irb):2
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
    from /Users/domenicovele/.rvm/gems/ruby-1.9.3-p374@mater/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

I've then reverted to version 3.0.3 since I cannot get where the error is.

I'd appreciate your feedback.

Thanks!

after block no longer works (or did it ever?)

In the README, there's an example for the callsbacks using an after block within the event block ( 9f39a22 ) which no longer works:

$ cat test.rb 
require 'rubygems'
require 'aasm'

class Job
  include AASM
  aasm do

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

    event :run do
      after do
        puts "Hello, world"
      end
      transitions :from => :sleeping, :to => :running
    end

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

    event :sleep do
      transitions :from => :running, :to => :sleeping
    end
  end
end
$ ruby test.rb
test.rb:13:in `block (2 levels) in <class:Job>': undefined method `after' for #<AASM::SupportingClasses::Event:0x007f89b476dc58> (NoMethodError)
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/supporting_classes/event.rb:94:in `instance_eval'
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/supporting_classes/event.rb:94:in `update'
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/supporting_classes/event.rb:9:in `initialize'
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/base.rb:42:in `new'
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/base.rb:42:in `event'
    from test.rb:12:in `block in <class:Job>'
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/aasm.rb:20:in `instance_eval'
    from /Users/mark/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/aasm-3.0.16/lib/aasm/aasm.rb:20:in `aasm'
    from test.rb:6:in `<class:Job>'
    from test.rb:4:in `<main>'
$ gem list | grep -i aasm
aasm (3.0.16)

Any ideas?

on_transition method call requires extra parameter

When using the :on_transition option to transitions argument passing requires an extra parameter

aasm_event :something do

transitions :from=>:state1, :to=>:state2, :on_transition=>:foo

end

def foo(a,b)
......
end

In this case for foo to be called with a=1 and b=2 when the transition occurs we need to call
something(nil,1,2) or something(:state2,1,2) - This is because the first parameter is taken as the destination state. This seems superfluous when there is only one destination state - Could this be changed to support a syntax like something(1,2) in the case of only one destination state or something(1,2,:to=>:state2) in the case of multiple possible destinations?

rails4 scoping

DEPRECATION WARNING: Calling #scope or #default_scope with a hash is deprecated. Please use a lambda containing a scope. E.g. scope :red, -> { where(color: 'red') }. (called from block in <class:Shipment> at /home/tim/rails/dhl/app/models/shipment.rb:14)

DEPRECATION WARNING: Calling #scope or #default_scope with a hash is deprecated. Please use a lambda containing a scope. E.g. scope :red, -> { where(color: 'red') }. (called from block in <class:Shipment> at /home/tim/rails/dhl/app/models/shipment.rb:15)

problem with name of an aasm_state

So, if one of my states is named as

aasm_state :valid

then when I call a transition method with the bang sign then I get validation errors. I'm not sure why... Shouldn't I be able to call one of my states "valid" ?? This smells like a bug. I'm using Rails 3... there are no methods called "valid" in Rails nor are there any Ruby keywords called "valid".

Required argument (to_state) when calling event with *args

With reference to the README.md (as of 4 Mar 2013), specifically the definition of a 'custom' on_transition:

...
    event :run, :after => :notify_somebody do
      transitions :from => :sleeping, :to => :running, :on_transition => Proc.new {|obj, *args| obj.set_process(*args) }
    end
...

And the subsequent invocation:

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

Would it not be cleaner (and I hope unambiguous) to instead write:

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

because we have previously defined that this event transitions to running?

Having looked at the source I can see how and why this fails and why therefore the first argument is necessary.

Am I missing something here? Or would a refactor of the code theoretically make this shorthand possible?

Importing records, a way to ignore initial state

I wrote a bulk uploader importing existing data in csv form. Unfortunately the state is reset regardless of the state on import. Is there any way to ignore set_initial_state so I can import existing data?

invalid date format in specification

With rubygems 1.7.2 I get:

Invalid gemspec in [/usr/local/lib/ruby/gems/1.8/specifications/aasm-3.0.4.gemspec]: invalid date format in specification: "2012-04-02 00:00:00.000000000Z"

That usually happens with gems built with an old rubygems. Can you verify?

Behavior when subclassing

I’m glad to be the first one to file an AASM issue in this stellar new tracking tool ;-)

Let’s say I have the following classes defined:

```
class Airplane
include AASM
aasm_initial_state :landed
aasm_state :taking_off
aasm_state :landed
aasm_state :descending
aasm_state :in_flight
aasm_state :crashing

aasm_event :descend do transitions :to => :descending, :from => :in_flight end aasm_event :land do transitions :to => :landed, :from => :descending end aasm_event :take_off do transitions :to => :in_flight, :from => :landed end

end

class FighterPlane < Airplane
include AASM

aasm_state :barrel_rolling aasm_state :warp_speed aasm_state :in_combat

end
```

Now, I should assume that since FighterPlane is a subclass of Airplane, FighterPlane would have all of Airplane’s states as well as its own defined states, such that calling FighterPlane.aasm_states would yield the following results:

```
[ :taking_off, :landed, :descending, :in_flight, :crashing, :barrel_rolling, :warp_speed, :in_combat ]
```

However, FighterPlane.aasm_states seems to only contain those states it defines itself. Is this the intended behavior? Is there something I’m missing?

is enter-callback correct behavior for same_state event transition?

If event X for state A stays at A, (transitions :to => :A, :from :A), why is the state :A, :enter callback entered again? The state is already at A, it is not being re-entered. There are many state machines where this is the desired behavior. Something gets done when machine first enters state A, but not when already at A. Example, an ON switch turns a machine on, going thru some type of start up sequence, but pushing ON again, is a NOP: machine is still ON and doesn't need to be restarted. Example: A subscription is CANCELLED, the state goes to WAIT FOR END OF BILLING PERIOD. Cancelling the subscription again is thus a NOP, because the state is already at WAIT FOR END OF BILLING PERIOD and any actions associated with first entering that state have already been performed.

Persistence for mongoid (MongoDB)

I couldn't find any info about AASM working with mongoid (MongoDB). Of course great part of the gem is independent of database, however there is persistence module for active_record. Do anybody know about equivalent of active_record_persistence.rb for mongoid?

State transitions fire and return true but do nothing

I have a class:

class Item < ActiveRecord::Base
  include AASM
  aasm_initial_state :available
  aasm_state :available
  aasm_state :in_use
  aasm_event(:check_out) do
    transitions :from => :available, :to => :in_use
  end
  aasm_event(:check_in) do
    transitions :from => :in_use, :to => :available
  end
end

This works:

i = Item.create!
i.write_attribute(:state, 'in_use')
# => "in_use"
i.save!
# => true
i.state
# => "in_use"

But this doesn't do anything:

i = Item.create!
i.check_out!
# => true
i.state
# => "available"

New syntax does not create scopes for the states

ActiveRecordPersistence creates the named scopes for each action by hooking into aasm_state. This method is no longer used when using the new syntax, therefore the scopes are no longer created

aasm 3.0.2, rails 3.2.1, ruby 1.9.3-p0

As a temporary workaround, I've added an initializer to my app to create them:

module AASM
  class Base
    def state_with_scopes_hax(name, options={})
      state_without_scopes_hax name, options

      @clazz.singleton_class.send(:define_method, name) do
        where(:state => name)
      end
    end

    alias_method :state_without_scopes_hax, :state
    alias_method :state, :state_with_scopes_hax
  end
end

Memory leak with Rails 3 and AASM

Hello,

I am investigating a memory leak in my application which was ported from Rails 2.3.10 to 3.0.4. Created a bare-bones application with just one controller that renders :text => "foobar" and one model which "include AASM".

With this app, a "gem 'aasm', :git => 'git://github.com/rubyist/aasm.git'" in Gemfile and create a model which has
include AASM
and 32 states, 16 transitions and 9 events, with each request, the numbers of state and transition instances in ObjectSpace will grow. After the first request:

OBJECT SPACE: AASM::SupportingClasses::Event: 9
OBJECT SPACE: AASM::SupportingClasses::State: 16
OBJECT SPACE: AASM::SupportingClasses::StateTransition: 32
OBJECT SPACE: Hash: 817
OBJECT SPACE: Array: 9713
OBJECT SPACE: Proc: 776

after 100 requests:

OBJECT SPACE: AASM::SupportingClasses::Event: 909
OBJECT SPACE: AASM::SupportingClasses::State: 1616
OBJECT SPACE: AASM::SupportingClasses::StateTransition: 3232
OBJECT SPACE: Hash: 10306
OBJECT SPACE: Array: 10701
OBJECT SPACE: Proc: 13899

Consequently, the memory usage grows from 31MB (first request) to 188 MB (100th request) and will not stop growing until I stop the requests, and is never freed.

Why is this happening? It wasn't happening (or not as badly) in Rails 2.3.10.

State transitions do not persist to DB in ActiveRecord models

foo.bar! does not persist to the database due to a failed validation. The documentation for the WriteState::aasm_write_state method indicates that update_attribute is being used to bypass validation, but this is not the case.

After calling write_attribute, save is being called to persist the changes. Since save does validation, this breaks the design of state transitions in AASM.

A proposed fix would be to call save(false) to skip validation, see pastie:

http://www.pastie.org/444480

small issue , but error updating gem, when building doc

Building YARD (yri) index for aasm-3.0.7...
[error]: ParserSyntaxError: syntax error in LICENSE:(1,18): syntax error, unexpected tINTEGER, expecting $end
[error]: Stack trace:
/Users/yves/.rvm/gems/ruby-1.9.3-p125@rails32/gems/yard-0.7.3/lib/yard/parser/ruby/ruby_parser.rb:505:in on_parse_error' /Users/yves/.rvm/gems/ruby-1.9.3-p125@rails32/gems/yard-0.7.3/lib/yard/parser/ruby/ruby_parser.rb:49:inparse'
/Users/yves/.rvm/gems/ruby-1.9.3-p125@rails32/gems/yard-0.7.3/lib/yard/parser/ruby/ruby_parser.rb:49:in parse' /Users/yves/.rvm/gems/ruby-1.9.3-p125@rails32/gems/yard-0.7.3/lib/yard/parser/ruby/ruby_parser.rb:15:inparse'
/Users/yves/.rvm/gems/ruby-1.9.3-p125@rails32/gems/yard-0.7.3/lib/yard/parser/source_parser.rb:438:in parse' /Users/yves/.rvm/gems/ruby-1.9.3-p125@rails32/gems/yard-0.7.3/lib/yard/parser/source_parser.rb:361:inparse_in_order'

pass parameter into AASM states events.

We have a model like given below and we want to pass comments like this sitestatus.close('site is not working'). How much effort is required to add this feature of passing parameters into state-machine events. Thanks. Junaid
class SiteStatus < ActiveRecord::Base

include AASM

aasm_column :status
aasm_initial_state :open

States

aasm_state :open
aasm_state :replied
aasm_state :closed

Events

aasm_event :re_open, :after=>:notify_re_opened do transitions :to => :open, :from => [:replied, :closed],:guard => :can_re_open? end
aasm_event :close, :after=>:notify_closed do transitions :to => :closed, :from => [:open,:replied],:guard => :can_close? end

end

sitestatus = SiteStatus.new
sitestatus.close('site is not working');

Rails3 support

  1. before_validation_on_create is deprecated. Please use before_validation(arguments, :on => :create.
  2. Base.named_scope has been deprecated, please use Base.scope instead

Leak again (2.3.1)?

I'm at 2.3.1 and still getting memory leak in development mode.

Workaround:

 config.to_prepare do
     AASM::StateMachine['ClassName'] = nil
  end

in development.rb.

Get list of states & permissible_states based on current state of an AASM object

We have the use case where we want to display a select dropdown for the user to select from all the possible states to transition to. For that we needed 2 things: 1) a list of all the states 2) a list of the states that the object could potentially transition to based on the current state.

This is the code that we came up with

def permissible_states_for_current_state
  aasm_permissible_events_for_current_state.map do |event|
    AASM::StateMachine[MyClass].events[event].instance_variable_get("@transitions").first.opts[:to]
  end
end

def states
  AASM::StateMachine[MyClass].states.map &:name
end

def not_permissible_states_for_current_state
  states - permissible_states_for_current_state
end

def states_for_select
  states.map{ |state| [I18n.t("#{state}"), state] }
end

And in the view

options_for_select(@my_object.states_for_select, selected: @my_object.state, disabled: @my_object.impossible_states)

The question is where there is already and easier/better way to do it, or if it would be possible to implement those two methods aasm_permissible_states_for_current_state and aasm_states within AASM?

Issue with Subclass trying to use the Superclass state machine

Hello,

It seems that subclassing is supported by aasm but I am getting an error which seems to be related to this.

I am getting:

Failure/Error: @subclass.update_state
  NoMethodError:
  undefined method `may_fire?' for nil:NilClass

My code looks like this:

class Superclass < ActiveRecord::Base
  include AASM

  aasm(column: 'state') do
    state :missing_details, initial: true
    state :invalid_details
    state :confirmed_details, after_enter: :after_confirm
    state :pending_details_confirmation
  end

  def update_state

     if may_add_details?
       add_details!
     end

     # More stuff

 end
end

class Subclass < Superclass
  # Subclass specific code
end

After debugging the code, I see that aasm is looking for the state machine inside Subclass and not finding it, as intended (or finding an empty state machine)

So, when it looks for the event add_details it is not finding it and then raising the undefined method error.

What's the expected behavior here? Is this correct or a bug?

I would expect aasm to be smart enough to know that:

  1. There is no state machine in Subclass
  2. Use the state machine in Superclass

Maybe there is something I am doing wrong.

Proposed solutions:

A. Add an inheritance section in the README
B. Change the way aasm looks up the state machine

I'd appreciate your feedback.

Thanks!

Hooking Observers with Events

Hi,

We are using AASM in quite a few of our models, but we're looking at simplifying a bit the models. One of the things we'd like to do is to move all the Notification stuff out of the models and into Observers.

So considering:

class ClarificationRequest < ActiveRecord::Base
  include AASM

  aasm_initial_state :open

  # States
  aasm_state :open
  aasm_state :closed

  # Events
  aasm_event :close, :after => :notify_closed do transitions :to => :closed, :from => [:open,:replied], :guard => :can_close? end
end

I've tried this, but with no luck:

class ClarificationRequestObserver < ActiveRecord::Observer
  observe :clarification_request

  def after_close
    puts '############### message from observer!!!!!'
  end

end

How can I move the :notify_closed to an Observer?

Thx!

.Karim

Why event throws InvalidTransition exception when a transition is not found for the current state for the event?

I thought aasm would ignore an event if the event is not applicable for the current state, but it throws and InvalidTransition exception. This forces us to write all unnecessary do-nothing transitions! Any reason for this design choice?

I plan to comment out the line of code in event.rb (fire()). I could put this change back here through a pull request, if there is an interest in this change.

Multiple state machines in one model?

Hi,

I am migrating from state_machine to aasm but I've noticed an issue when trying to use more than one state machines in my model.

My code looks like this:

class Student
  # State Machine #1 
  aasm(column: 'tuition_state') do
    ...
  end

  # State Machine #2 
  aasm(column: 'attendance_state') do
    ...
  end

I am calling an event from tuition_state but my test fails because it tells me it cannot transition from one state to the other.

Somehow it gets confused and an event from state machine #1 is considering states from state machine # 2.

So, are multiple state machines in one model supported?

I could refactor my code and move the second state machine to another model, but I'd prefer not to do that for now.

Please let me know.

Thanks,
Ernesto

AASM is using scope syntax which will be deprecated in Rails 4

Rails 4 forces to use the lambda syntax of scopes. I am currently using this initializer to avoid the deprecation warning. Hope it helps :

#config/initializers/aasm_scope_deprecation.rb
require 'aasm/persistence/base'
module AASM
  class Base
    def state_with_scope(name, *args)
      state_without_scope(name, *args)
      unless @clazz.respond_to?(name)
        if @clazz.ancestors.map {|klass| klass.to_s}.include?("ActiveRecord::Base")
          scope_options_hash = {:conditions => { "#{@clazz.table_name}.#{@clazz.aasm_column}" => name.to_s}}
          scope_method = ActiveRecord::VERSION::MAJOR >= 3 ? :scope : :named_scope
          scope_options = ActiveRecord::VERSION::MAJOR >= 4 ? lambda {  where(scope_options_hash[:conditions])} : scope_options_hash
          @clazz.send(scope_method, name, scope_options)
        elsif @clazz.ancestors.map {|klass| klass.to_s}.include?("Mongoid::Document")
          scope_options = lambda { @clazz.send(:where, {@clazz.aasm_column.to_sym => name.to_s}) }
          @clazz.send(:scope, name, scope_options)
        end
      end
    end
    alias_method :state, :state_with_scope
  end # Base
end

potential stack overflow

Hello,

I don't actually know that much about AASM, but it's part of a project I'm involved in that I wanted to graph using railroad. Unfortunately, railroad crashes when it processes AASM - but it appears to be due to a bug in AASM. There seem to be lots of people wanting to do something similar. I believe I've identified the problem, and a solution that at least fixes the railroad crash, so I thought I would let you know.

When railroad runs against projects that use AASM, it crashes with a stack overflow. I don't really understand the AASM code, so I can't really tell if this could happen under normal operations. I've identified the "bug" and a "fix." The fix seems safe, but it's definitely a band-aid. If the problem can't come up in normal operation, then the band-aid is probably sufficient.

The problem occurs on line 242 of active_record_persistence.rb (as for versions, I don't use git, but FWIW, the file I downloaded is called rubyist-aasm-7a1c70158c67113bc1c8dd6bd6dbd327af150648):

 module NamedScopeMethods
   def aasm_state_with_named_scope name, options = {}
  aasm_state_without_named_scope name, options
     self.named_scope name, :conditions => { "#{table_name}.#{self.aasm_column}" => name.to_s} unless self.respond_to?(name)
   end
 end

The problem seems to be that for some reason, when railroad tries to process AASM code, the aliasing earlier in this file causes the above method to call itself:

(lines 44-50)
base.class_eval do
class << self
unless method_defined?(:aasm_state_without_named_scope)
alias_method :aasm_state_without_named_scope, :aasm_state
alias_method :aasm_state, :aasm_state_with_named_scope
end
end

The band-aid fix is to put a limiter on the call like so:

     aasm_state_without_named_scope name, options.merge(:called_as_unnamed => true) unless options[:called_as_unnamed]

I believe this is safe because it seems to me that in any case, if this line is calling the function that contains it, then it's going to crash anyway.

-Avram

AASM transition guards, object.send and update_attributes in a Ruby on Rails model/controller

We are using the AASM gem to attach state to a particular model.

There are a good number of transitions for one state and rather than have a huge switch block we are taking the event that is passed in and using object.send in the model like so:

def event=(event_name)
self.send("#{event_name}!")
end

For one of the transitions we have added a :guard clause that returns false and attempts to add an error to the model.errors.

In the controller we use update_attributes in this particular case.

The issue is that update_attributes doesn't return false and the error in the guard isn't retained. That being said the transition isn't completed so the guard is working...

Is there something funky about using AASM guard, send method for objects and update_attributes? I can't seem to track down why it isn't working.

Thanks!

Test whether a event would be successful?

Hello,

I am using AASM for a user applicaition and in the views, I would like to offer buttons / options which would change the state of a model only when this transition would be successful.

E.g. (model):

  • aasm_event :remember

(view):

  • <% if @object.can_fire(:remember) %> <%= link_to "Remember", whatever_path %> <% end %>

Since the transitions for this event are rather numerous and complex, I'd prefer not duplicating this code in the view which defines whether (in this example) the transitions in the event "remember" would succeed.

Thanks!

on_entry not called for initial state

Is it intentional that the :enter method is not called for the initial state?
(i.e. if this is intentional, let me know and I'll update the README)

e.g. with the following AASM definition, the do_create is not called on the initial creation of the object.

include AASM
aasm_column :state
aasm_initial_state :pending

aasm_state :pending, :enter => :do_create
aasm_state :active, :enter => :do_activate

aasm_event :activate do
  transitions :to => :active, :from => [:pending]
end

aasm_event :deactivate do
  transitions :to => :pending, :from => [ :active ]
end

Need 'after' call back

I need an 'after' call back just like acts_as_state_machine. It is posable that a callback may need to transition to a new state, currently that isn't posable since the state transition hasn't completed yet when the 'enter' callback is called. This might happen for example when you call a pay! action, and may end up with a 'cleared' or 'decline' state.

valid? is called despite config.skip_validation being set

On line https://github.com/rubyist/aasm/blob/master/lib/aasm/persistence/active_record_persistence.rb#L186 !self. valid? is called. Is there a reason for this? Calling valid? here not only fires all validation-related callbacks on a given model but will also populate the associated error hashes. I would expect that if I ask aasm to skip validation, valid? is never called.

In our case, we have an order model that transitions on the shipping page load. This model has a shipping_detail child object that is created prior to page load. The shipping_detail contains some information necessary for shipping but is not a valid object upon initial page render. Because we transition in the controller action, valid? is called and errors are introduced which are then displayed on our associated view.

Corrected in source code but not compiled in gem

It seemed that the code is changed on line #53 inside active_record_persistence.rb file, to make it Rails (3.0 rc) compatible, for method before_validation_on_create.
But when I installed gem, the code wasn't changed.
Later on, I found that it is changed in source code only. It will be definitely useful to others, saving lots of time googling for this as I did if you compile the changes in gem.

AASM column attribute direct assignment restriction

Just to make the main idea of th state machine more clear I have a proposal to restrict the aasm_column setter at all, because it barely makes any sense, if we have events to transition from one state to another. This way it would have been very strict and clean, which is good - we won't need to think if we accidently assigned a new state, which could be very unwanted.

So, basically, it would look something like this (exampe):

@user = User.new
@user.state # sleeping (Why so ? Please see my pull request for this one: alto#9)
@user.state = :run # Some TransitionException exception thrown - "Direct assignment is not allowed"
@user.sleeping? # true. No state change.
@user.run! # => true. Changing states only by using events
@user.running? # true. New state is active

I think it makes more sense to do it that way.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.