MicroMachine Documentation

repository·master·Indexed 19 days ago

https://github.com/soveran/micromachine

A minimal, lightweight finite state machine (FSM) implementation for Ruby. MicroMachine provides essential FSM functionality including state transitions via the `when` method, event triggering with `trigger` and `trigger!`, and state/transition callbacks using `on`. It is designed for composition, allowing integration with models like ActiveRecord through manual or callback-based persistence.

Tokens
2.3K
Snippets
10
Records
12
Agent score
18%

What's inside MicroMachine

  1. Integrate MicroMachine with models via composition

    master

    MicroMachine is designed for composition rather than being a mixin. To use it within a model (like ActiveRecord), instantiate the machine inside a method and manage the persistence of the state yourself.

    Two common patterns:

    1. Manual Persistence: Use a lifecycle hook (like before_save) to copy the machine's current state to a database column.
    2. Callback-based Persistence: Use machine.on(:any) to update the model's attribute automatically whenever a transition occurs.
    class Event < ActiveRecord::Base
      def confirmation
        @confirmation ||= begin
          # Initialize machine with current persisted state
          fsm = MicroMachine.new(confirmation_state || "pending")
    
          fsm.when(:confirm, "pending" => "confirmed")
          fsm.when(:cancel, "confirmed" => "cancelled")
          fsm.when(:reset, "confirmed" => "pending", "cancelled" => "pending")
    
          # Option: Automatically sync state to the model on any transition
          fsm.on(:any) { self.confirmation_state = fsm.state }
    
          fsm
        end
      end
    
      def confirm!
        confirmation.trigger(:confirm)
      end
    end
  2. Basic usage of MicroMachine

    master

    To use MicroMachine, instantiate MicroMachine.new(initial_state). Use the when method to define transitions by mapping an event to a hash of { current_state => next_state }. Use trigger(event) to attempt a state transition, which returns true if successful or false if the event is not valid for the current state. Use state to retrieve the current state.

    require 'micromachine'
    
    machine = MicroMachine.new(:new) # Initial state.
    
    # Define transitions
    machine.when(:confirm, :new => :confirmed)
    machine.when(:ignore, :new => :ignored)
    machine.when(:reset, :confirmed => :new, :ignored => :new)
    
    machine.trigger(:confirm)  #=> true
    machine.state              #=> :confirmed
    
    machine.trigger(:ignore)   #=> false
    machine.state              #=> :confirmed
  3. Configure callbacks for states and transitions

    master

    You can register callbacks that execute when entering a specific state or when any transition occurs:

    • on(state) { ... }: Executes the block when the machine enters the specified state.
    • on(:any) { ... }: Executes the block on every transition. Note that :any is a special reserved key; do not use it as a regular state name in when definitions.

    Callbacks can accept an optional second argument (a payload) if one was passed to the trigger or trigger! method.

    # Callback on a specific state
    machine.on(:confirmed) do
      puts "Confirmed"
    end
    
    # Callback on any transition with a payload
    machine.on(:any) do |_status, payload|
      puts payload.inspect
    end
    
    machine.trigger(:cancel, from: :user)
  4. Query available events and states

    master

    Use the following methods to inspect the machine's configuration:

    • events: Returns an array of all possible events defined in the machine.
    • states: Returns an array of all possible states.
    • triggerable_events: Returns an array of events that can be successfully triggered from the current state.
    machine.events              #=> [:confirm, :ignore, :reset]
    machine.triggerable_events  #=> [:confirm, :ignore]
    machine.states              #=> [:new, :confirmed, :ignored]
  5. How to trigger events and handle invalid states

    master

    MicroMachine provides two ways to attempt a transition:

    1. trigger(event[, payload]): Returns true if the transition was successful, or false if the event is not valid for the current state. The state remains unchanged on failure.
    2. trigger!(event[, payload]): Attempts the transition but raises a MicroMachine::InvalidState exception if the event is not valid for the current state.

    You can also use trigger?(event) to check if an event is valid for the current state without actually performing the transition.

    machine.state              #=> :ignored
    
    machine.trigger?(:ignore)  #=> false
    machine.trigger?(:reset)   #=> true
    
    # State is preserved after trigger?
    
    machine.trigger!(:ignore)  #=> raises MicroMachine::InvalidState
  6. Define state transitions with `when`

    master

    Use the when method to map events to state transitions. The transitions argument should be a hash where keys are current states and values are the next states resulting from the event.

    Example mapping: { current_state: next_state }.

    machine = MicroMachine.new(:idle)
    
    # When the :start event occurs:
    # - If in :idle, move to :running
    # - If in :paused, move to :running
    machine.when(:start, {
      idle: :running,
      paused: :running
    })
  7. Register callbacks with `on`

    master

    Use the on method to register blocks that execute whenever the machine enters a specific state. You can also register a callback for the special :any key, which will run on every state transition.

    # Callback for when entering :running state
    machine.on(:running) do |event, payload|
      puts "Entered running via #{event} with payload #{payload}"
    end
    
    # Global callback for any state change
    machine.on(:any) do |event, payload|
      puts "Event #{event} processed"
    end
  8. Trigger events with `trigger` and `trigger!`

    master

    You can move the state machine forward by triggering events:

    • trigger(event, payload = nil): Returns true if the transition was successful, or false if the event is not valid for the current state. It accepts an optional payload passed to callbacks.
    • trigger!(event, payload = nil): Similar to trigger, but raises an InvalidState error if the event is not valid for the current state.

    If a transition occurs, any registered callbacks for the new state (or the :any key) will be executed with the event and payload.

    # Using trigger (safe)
    if machine.trigger(:start, { user_id: 1 })
      puts "Transition successful"
    end
    
    # Using trigger! (raises error on invalid transition)
    begin
      machine.trigger!(:unknown_event)
    rescue MicroMachine::InvalidState => e
      puts e.message
    end
  9. Inspect machine states and events

    master

    Use the following methods to query the machine's configuration and current status:

    • state: Returns the current state.
    • events: Returns an array of all event names defined via when.
    • triggerable_events: Returns an array of events that are valid to trigger from the current state.
    • states: Returns an array of all possible states defined in the transition maps.
  10. MicroMachine Error Classes

    master

    MicroMachine defines two specific error classes:

    • MicroMachine::InvalidEvent: Raised by trigger? if the event provided has not been defined via when.
    • MicroMachine::InvalidState: An ArgumentError subclass raised by trigger! if the event is not valid for the current state.