Module: StateMachines::MacroMethods

Defined in:
lib/state_machines/macro_methods.rb

Instance Method Summary collapse

Instance Method Details

#state_machine(*args, &block) ⇒ Object

Creates a new state machine with the given name. The default name, if not specified, is :state.

Configuration options:

  • :attribute - The name of the attribute to store the state value in. By default, this is the same as the name of the machine.

  • :initial - The initial state of the attribute. This can be a static state or a lambda block which will be evaluated at runtime (e.g. lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling}). Default is nil.

  • :initialize - Whether to automatically initialize the attribute by hooking into #initialize on the owner class. Default is true.

  • :action - The instance method to invoke when an object transitions. Default is nil unless otherwise specified by the configured integration.

  • :namespace - The name to use for namespacing all generated state / event instance methods (e.g. “heater” would generate :turn_on_heater and :turn_off_heater for the :turn_on/:turn_off events). Default is nil.

  • :integration - The name of the integration to use for adding library-specific behavior to the machine. Built-in integrations include :active_model, :active_record, :data_mapper, :mongo_mapper, and :sequel. By default, this is determined automatically.

Configuration options relevant to ORM integrations:

  • :plural - The pluralized version of the name. By default, this will attempt to call pluralize on the name. If this method is not available, an “s” is appended. This is used for generating scopes.

  • :messages - The error messages to use when invalidating objects due to failed transitions. Messages include:

    • :invalid

    • :invalid_event

    • :invalid_transition

  • :use_transactions - Whether transactions should be used when firing events. Default is true unless otherwise specified by the configured integration.

This also expects a block which will be used to actually configure the states, events and transitions for the state machine. Note that this block will be executed within the context of the state machine. As a result, you will not be able to access any class methods unless you refer to them directly (i.e. specifying the class name).

For examples on the types of state machine configurations and blocks, see the section below.

Examples

With the default name/attribute and no configuration:

class Vehicle
  state_machine do
    event :park do
      ...
    end
  end
end

The above example will define a state machine named “state” that will store the value in the state attribute. Every vehicle will start without an initial state.

With a custom name / attribute:

class Vehicle
  state_machine :status, :attribute => :status_value do
    ...
  end
end

With a static initial state:

class Vehicle
  state_machine :status, :initial => :parked do
    ...
  end
end

With a dynamic initial state:

class Vehicle
  state_machine :status, :initial => lambda {|vehicle| vehicle.speed == 0 ? :parked : :idling} do
    ...
  end
end

Class Methods

The following class methods will be automatically generated by the state machine based on the name of the machine. Any existing methods will not be overwritten.

  • human_state_name(state) - Gets the humanized value for the given state. This may be generated by internationalization libraries if supported by the integration.

  • human_state_event_name(event) - Gets the humanized value for the given event. This may be generated by internationalization libraries if supported by the integration.

For example,

class Vehicle
  state_machine :state, :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end

    event :shift_up do
      transition :idling => :first_gear
    end
  end
end

Vehicle.human_state_name(:parked)         # => "parked"
Vehicle.human_state_name(:first_gear)     # => "first gear"
Vehicle.human_state_event_name(:park)     # => "park"
Vehicle.human_state_event_name(:shift_up) # => "shift up"

Instance Methods

The following instance methods will be automatically generated by the state machine based on the name of the machine. Any existing methods will not be overwritten.

  • state - Gets the current value for the attribute

  • state=(value) - Sets the current value for the attribute

  • state?(name) - Checks the given state name against the current state. If the name is not a known state, then an ArgumentError is raised.

  • state_name - Gets the name of the state for the current value

  • human_state_name - Gets the human-readable name of the state for the current value

  • state_events(requirements = {}) - Gets the list of events that can be fired on the current object’s state (uses the unqualified event names)

  • state_transitions(requirements = {}) - Gets the list of transitions that can be made on the current object’s state

  • state_paths(requirements = {}) - Gets the list of sequences of transitions that can be run from the current object’s state

  • fire_state_event(name, *args) - Fires an arbitrary event with the given argument list. This is essentially the same as calling the actual event method itself.

The state_events, state_transitions, and state_paths helpers all take an optional set of requirements for determining what’s available for the current object. These requirements include:

  • :from - One or more states to transition from. If none are specified, then this will be the object’s current state.

  • :to - One or more states to transition to. If none are specified, then this will match any to state.

  • :on - One or more events to transition on. If none are specified, then this will match any event.

  • :guard - Whether to guard transitions with the if/unless conditionals defined for each one. Default is true.

For example,

class Vehicle
  state_machine :state, :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end

    event :park do
      transition :idling => :parked
    end
  end
end

vehicle = Vehicle.new
vehicle.state                             # => "parked"
vehicle.state_name                        # => :parked
vehicle.human_state_name                  # => "parked"
vehicle.state?(:parked)                   # => true

# Changing state
vehicle.state = 'idling'
vehicle.state                             # => "idling"
vehicle.state_name                        # => :idling
vehicle.state?(:parked)                   # => false

# Getting current event / transition availability
vehicle.state_events                      # => [:park]
vehicle.park                              # => true
vehicle.state_events                      # => [:ignite]
vehicle.state_events(:from => :idling)    # => [:park]
vehicle.state_events(:to => :parked)      # => []

vehicle.state_transitions                 # => [#<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
vehicle.ignite                            # => true
vehicle.state_transitions                 # => [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>]

vehicle.state_transitions(:on => :ignite) # => []

# Getting current path availability
vehicle.state_paths                       # => [
                                          #     [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
                                          #      #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
                                          #   ]
vehicle.state_paths(:guard => false)      # =>
                                          #     [#<StateMachines::Transition attribute=:state event=:park from="idling" from_name=:idling to="parked" to_name=:parked>,
                                          #      #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>]
                                          #   ]

# Fire arbitrary events
vehicle.fire_state_event(:park)           # => true

Attribute initialization

For most classes, the initial values for state machine attributes are automatically assigned when a new object is created. However, this behavior will not work if the class defines an initialize method without properly calling super.

For example,

class Vehicle
  state_machine :state, :initial => :parked do
    ...
  end
end

vehicle = Vehicle.new   # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state           # => "parked"

In the above example, no initialize method is defined. As a result, the default behavior of initializing the state machine attributes is used.

In the following example, a custom initialize method is defined:

class Vehicle
  state_machine :state, :initial => :parked do
    ...
  end

  def initialize
  end
end

vehicle = Vehicle.new   # => #<Vehicle:0xb7c77678>
vehicle.state           # => nil

Since the initialize method is defined, the state machine attributes never get initialized. In order to ensure that all initialization hooks are called, the custom method must call super without any arguments like so:

class Vehicle
  state_machine :state, :initial => :parked do
    ...
  end

  def initialize(attributes = {})
    ...
    super()
  end
end

vehicle = Vehicle.new   # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state           # => "parked"

Because of the way the inclusion of modules works in Ruby, calling super() will not only call the superclass’s initialize, but also initialize on all included modules. This allows the original state machine hook to get called properly.

If you want to avoid calling the superclass’s constructor, but still want to initialize the state machine attributes:

class Vehicle
  state_machine :state, :initial => :parked do
    ...
  end

  def initialize(attributes = {})
    ...
    initialize_state_machines
  end
end

vehicle = Vehicle.new   # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state           # => "parked"

You may also need to call the initialize_state_machines helper manually in cases where you want to change how static / dynamic initial states get set. For example, the following example forces the initialization of static states regardless of their current value:

class Vehicle
  state_machine :state, :initial => :parked do
    state nil, :idling
    ...
  end

  def initialize(attributes = {})
    @state = 'idling'
    initialize_state_machines(:static => :force) do
      ...
    end
  end
end

vehicle = Vehicle.new   # => #<Vehicle:0xb7c8dbf8 @state="parked">
vehicle.state           # => "parked"

The above example is also noteworthy because it demonstrates how to avoid initialization issues when nil is a valid state. Without passing in :static => :force, state_machine would never have initialized the state because nil (the default attribute value) would have been interpreted as a valid current state. As a result, state_machine would have simply skipped initialization.

States

All of the valid states for the machine are automatically tracked based on the events, transitions, and callbacks defined for the machine. If there are additional states that are never referenced, these should be explicitly added using the StateMachines::Machine#state or StateMachines::Machine#other_states helpers.

When a new state is defined, a predicate method for that state is generated on the class. For example,

class Vehicle
  state_machine :initial => :parked do
    event :ignite do
      transition all => :idling
    end
  end
end

…will generate the following instance methods (assuming they’re not already defined in the class):

  • parked?

  • idling?

Each predicate method will return true if it matches the object’s current state. Otherwise, it will return false.

Attribute access

The actual value for a state is stored in the attribute configured for the state machine. In most cases, this is the same as the name of the state machine. For example:

class Vehicle
  attr_accessor :state

  state_machine :state, :initial => :parked do
    ...
    state :parked, :value => 0
    start :idling, :value => 1
  end
end

vehicle = Vehicle.new # => #<Vehicle:0xb712da60 @state=0>
vehicle.state         # => 0
vehicle.parked?       # => true
vehicle.state = 1
vehicle.idling?       # => true

The most important thing to note from the example above is what it means to read from and write to the state machine’s attribute. In particular, state_machine treats the attribute (state in this case) like a basic attr_accessor that’s been defined on the class. There are no special behaviors added, such as allowing the attribute to be written to based on the name of a state in the machine. This is the case for a few reasons:

  • Setting the attribute directly is an edge case that is meant to only be used when you want to skip state_machine altogether. This means that state_machine shouldn’t have any effect on the attribute accessor methods. If you want to change the state, you should be using one of the events defined in the state machine.

  • Many ORMs provide custom behavior for the attribute reader / writer - it may even be defined by your own framework / method implementation just the example above showed. In order to avoid having to worry about the different ways an attribute can get written, state_machine just makes sure that the configured value for a state is always used when writing to the attribute.

If you were interested in accessing the name of a state (instead of its actual value through the attribute), you could do the following:

vehicle.state_name    # => :idling

Events and Transitions

Events defined on the machine are the interface to transitioning states for an object. Events can be fired either directly (through the method generated for the event) or indirectly (through attributes defined on the machine).

For example,

class Vehicle
  include DataMapper::Resource
  property :id, Serial

  state_machine :initial => :parked do
    event :ignite do
      transition :parked => :idling
    end
  end

  state_machine :alarm_state, :initial => :active do
    event :disable do
      transition all => :off
    end
  end
end

# Fire +ignite+ event directly
vehicle = Vehicle.create    # => #<Vehicle id=1 state="parked" alarm_state="active">
vehicle.ignite              # => true
vehicle.state               # => "idling"
vehicle.alarm_state         # => "active"

# Fire +disable+ event automatically
vehicle.alarm_state_event = 'disable'
vehicle.save                # => true
vehicle.alarm_state         # => "off"

In the above example, the state attribute is transitioned using the ignite action that’s generated from the state machine. On the other hand, the alarm_state attribute is transitioned using the alarm_state_event attribute that automatically gets fired when the machine’s action (save) is invoked.

For more information about how to configure an event and its associated transitions, see StateMachines::Machine#event.

Defining callbacks

Within the state_machine block, you can also define callbacks for transitions. For more information about defining these callbacks, see StateMachines::Machine#before_transition, StateMachines::Machine#after_transition, and StateMachines::Machine#around_transition, and StateMachines::Machine#after_failure.

Namespaces

When a namespace is configured for a state machine, the name provided will be used in generating the instance methods for interacting with states/events in the machine. This is particularly useful when a class has multiple state machines and it would be difficult to differentiate between the various states / events.

For example,

class Vehicle
  state_machine :heater_state, :initial => :off, :namespace => 'heater' do
    event :turn_on do
      transition all => :on
    end

    event :turn_off do
      transition all => :off
    end
  end

  state_machine :alarm_state, :initial => :active, :namespace => 'alarm' do
    event :turn_on do
      transition all => :active
    end

    event :turn_off do
      transition all => :off
    end
  end
end

The above class defines two state machines: heater_state and alarm_state. For the heater_state machine, the following methods are generated since it’s namespaced by “heater”:

  • can_turn_on_heater?

  • turn_on_heater

  • can_turn_off_heater?

  • turn_off_heater

  • ..

  • heater_off?

  • heater_on?

As shown, each method is unique to the state machine so that the states and events don’t conflict. The same goes for the alarm_state machine:

  • can_turn_on_alarm?

  • turn_on_alarm

  • can_turn_off_alarm?

  • turn_off_alarm

  • ..

  • alarm_active?

  • alarm_off?

Scopes

For integrations that support it, a group of default scope filters will be automatically created for assisting in finding objects that have the attribute set to one of a given set of states.

For example,

Vehicle.with_state(:parked)               # => All vehicles where the state is parked
Vehicle.with_states(:parked, :idling)     # => All vehicles where the state is either parked or idling

Vehicle.without_state(:parked)            # => All vehicles where the state is *not* parked
Vehicle.without_states(:parked, :idling)  # => All vehicles where the state is *not* parked or idling

Note that if class methods already exist with those names (i.e. :with_state, :with_states, :without_state, or :without_states), then a scope will not be defined for that name.

See StateMachines::Machine for more information about using integrations and the individual integration docs for information about the actual scopes that are generated.



516
517
518
# File 'lib/state_machines/macro_methods.rb', line 516

def state_machine(*args, &block)
  StateMachines::Machine.find_or_create(self, *args, &block)
end