SimplyFSM

Gem Version

simply_fsm is a bare-necessities finite state machine data-type for use with any Ruby class. I created simply_fsm because I wanted the minimal FSM data type that was easy to use and did everything I would expect from a core data type.

If you need storage/persistence/Rails/etc support, I recommend AASM whose API was an inspiration for this gem.

Contents

Installation

Add this line to your application's Gemfile:

gem 'simply_fsm'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install simply_fsm

Usage

One state machine

Here's a single-state example of a Job class (unashamedly based on this aasm example).

class Job
  include SimplyFSM

  state_machine :status do
    state :sleeping, initial: true
    state :running
    state :cleaning

    event :run, transitions: { from: :sleeping, to: :running } do
      # executed when transition succeeds
    end

    event :clean, transitions: { from: :running, to: :cleaning } do
      # do the cleaning since transition succeeded
    end

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

This provides the following public methods for the class.

job = Job.new
job.status    # => :sleeping
job.sleeping? # => true
job.may_run?  # => true
job.run       # => true on success
job.running?  # => true
job.sleeping? # => false
job.may_run?  # => false
job.run       # => false on failure

Multiple state machines

A class can define as many state machines as needed as long as each has a unique name.

class Player
  include SimplyFSM

  state_machine :motion do
    state :idling, initial: true
    state :walking

    event :idle, transitions: { from: :any, to: :idling }
    event :walk, transitions: { from: :idling, to: :walking }
  end

  state_machine :action do
    state :ready, initial: true
    state :blocking

    event :hold, transitions: { from: :any, to: :ready }
    event :block, transitions: { from: :any, to: :blocking }
  end
end

This provides the following public methods for the class.

Public method for motion state machine action state machine
current state motion action
event methods idle, walk hold, block
event precondition checking may_idle?, may_walk? may_hold?, may_block?
state checking idling?, walking? ready?, blocking?

Handle failed events

It's possible to specify an ebent failure handler via lambda or method name either for the entire state machine or for each event. If specified, the fail handler is called before an event returns false

class JobWithErrors
  class Error < StandardError; end
  class RunError < StandardError; end

  include SimplyFSM

  state_machine :activity, fail: :on_any_fail do
    state :sleeping, initial: true
    state :running
    state :cleaning

    event :sleep, transitions: { from: %i[running cleaning], to: :sleeping }
    event :clean, transitions: { from: :running, to: :cleaning }
    event :run,
          fail: ->(_event) { raise RunError, "Cannot run" },
          transitions: { from: :sleeping, to: :running }
  end

  def on_any_fail(event_name)
    raise Error, "Cannot do: #{event_name}"
  end
end

Guarding events

It's possible guard events against additional constraints by specifying a lambda for each event which is executed in the instance of the class with the state machine. In addition to checking the allowed from state, the guard, if specified, must return true for the transition to occur.

class AgilePlayer
  class LeapError < StandardError; end

  include SimplyFSM

  state_machine :motion do
    state :idling, initial: true
    state :walking
    state :running

    event :idle, transitions: { from: :any, to: :idling }
    event :walk, transitions: { from: :any, to: :walking }
    event :run, transitions: { from: :any, to: :running }
  end

  state_machine :action do
    state :ready, initial: true
    state :jumping
    state :leaping

    event :hold, transitions: { from: :any, to: :ready }
    event :jump,
          guard: -> { !running? },
          transitions: { from: :ready, to: :jumping }
    event :leap,
          guard: -> { running? },
          fail: ->(_event) { raise LeapError, "Cannot leap" },
          transitions: { from: :ready, to: :leaping }
  end
end

Multiple transitions for an event

Sometimes a single event can transition to different end states based on different input states. In those situations you can specify an array of transitions. Consider the following example where the hunt event transitions to walking or running depending on some condition outside the state machine.

class Critter
  include SimplyFSM

  def tired?
    @ate_at <= 12.hours.ago || @slept_at <= 24.hours.ago
  end

  state_machine :activity do
    state :sleeping, initial: true
    state :running
    state :walking
    state :eating

    event :eat, transitions: { to: :eating } do
      @ate_at = DateTime.new
    end
    event :sleep, transitions: { from: :eating, to: :sleeping } do
      @slept_at = DateTime.new
    end
    event :hunt, transitions: [
      { when: -> { tired? }, to: :walking },
      { to: :running }
    ]
  end
end

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and sugestions are welcome. Otherwise, at this time, this project is closed for code changes and pull requests. I appreciate your understanding.

This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the simply_fsm project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.