state_machine

https://github.com/pluginaweek/state_machine

Ruby

Adds support for creating state machines for attributes on any Ruby class

StateMachine::Machine#before_transition

Creates a callback that will be invoked *before* a transition is
performed so long as the given requirements match the transition.

== The callback

Callbacks must be defined as either an argument, in the :do option, or
as a block.  For example,

  class Vehicle
    state_machine do
      before_transition :set_alarm
      before_transition :set_alarm, all => :parked
      before_transition all => :parked, :do => :set_alarm
      before_transition all => :parked do |vehicle, transition|
        vehicle.set_alarm
      end
      ...
    end
  end

Notice that the first three callbacks are the same in terms of how the
methods to invoke are defined.  However, using the <tt>:do</tt> can
provide for a more fluid DSL.

In addition, multiple callbacks can be defined like so:

  class Vehicle
    state_machine do
      before_transition :set_alarm, :lock_doors, all => :parked
      before_transition all => :parked, :do => [:set_alarm, :lock_doors]
      before_transition :set_alarm do |vehicle, transition|
        vehicle.lock_doors
      end
    end
  end

Notice that the different ways of configuring methods can be mixed.

== State requirements

Callbacks can require that the machine be transitioning from and to
specific states.  These requirements use a Hash syntax to map beginning
states to ending states.  For example,

  before_transition :parked => :idling, :idling => :first_gear, :do => :set_alarm

In this case, the +set_alarm+ callback will only be called if the machine
is transitioning from +parked+ to +idling+ or from +idling+ to +parked+.

To help define state requirements, a set of helpers are available for
slightly more complex matching:
* <tt>all</tt> - Matches every state/event in the machine
* <tt>all - [:parked, :idling, ...]</tt> - Matches every state/event except those specified
* <tt>any</tt> - An alias for +all+ (matches every state/event in the machine)
* <tt>same</tt> - Matches the same state being transitioned from

See StateMachine::MatcherHelpers for more information.

Examples:

  before_transition :parked => [:idling, :first_gear], :do => ...     # Matches from parked to idling or first_gear
  before_transition all - [:parked, :idling] => :idling, :do => ...   # Matches from every state except parked and idling to idling
  before_transition all => :parked, :do => ...                        # Matches all states to parked
  before_transition any => same, :do => ...                           # Matches every loopback

== Event requirements

In addition to state requirements, an event requirement can be defined so
that the callback is only invoked on specific events using the +on+
option.  This can also use the same matcher helpers as the state
requirements.

Examples:

  before_transition :on => :ignite, :do => ...                        # Matches only on ignite
  before_transition :on => all - :ignite, :do => ...                  # Matches on every event except ignite
  before_transition :parked => :idling, :on => :ignite, :do => ...    # Matches from parked to idling on ignite

== Verbose Requirements

Requirements can also be defined using verbose options rather than the
implicit Hash syntax and helper methods described above.

Configuration options:
* <tt>:from</tt> - One or more states being transitioned from.  If none
  are specified, then all states will match.
* <tt>:to</tt> - One or more states being transitioned to.  If none are
  specified, then all states will match.
* <tt>:on</tt> - One or more events that fired the transition.  If none
  are specified, then all events will match.
* <tt>:except_from</tt> - One or more states *not* being transitioned from
* <tt>:except_to</tt> - One more states *not* being transitioned to
* <tt>:except_on</tt> - One or more events that *did not* fire the transition

Examples:

  before_transition :from => :ignite, :to => :idling, :on => :park, :do => ...
  before_transition :except_from => :ignite, :except_to => :idling, :except_on => :park, :do => ...

== Conditions

In addition to the state/event requirements, a condition can also be
defined to help determine whether the callback should be invoked.

Configuration options:
* <tt>:if</tt> - A method, proc or string to call to determine if the
  callback should occur (e.g. :if => :allow_callbacks, or
  :if => lambda {|user| user.signup_step > 2}). The method, proc or string
  should return or evaluate to a true or false value. 
* <tt>:unless</tt> - A method, proc or string to call to determine if the
  callback should not occur (e.g. :unless => :skip_callbacks, or
  :unless => lambda {|user| user.signup_step <= 2}). The method, proc or
  string should return or evaluate to a true or false value. 

Examples:

  before_transition :parked => :idling, :if => :moving?, :do => ...
  before_transition :on => :ignite, :unless => :seatbelt_on?, :do => ...

== Accessing the transition

In addition to passing the object being transitioned, the actual
transition describing the context (e.g. event, from, to) can be accessed
as well.  This additional argument is only passed if the callback allows
for it.

For example,

  class Vehicle
    # Only specifies one parameter (the object being transitioned)
    before_transition all => :parked do |vehicle|
      vehicle.set_alarm
    end
    
    # Specifies 2 parameters (object being transitioned and actual transition)
    before_transition all => :parked do |vehicle, transition|
      vehicle.set_alarm(transition)
    end
  end

*Note* that the object in the callback will only be passed in as an
argument if callbacks are configured to *not* be bound to the object
involved.  This is the default and may change on a per-integration basis.

See StateMachine::Transition for more information about the
attributes available on the transition.

== Usage with delegates

As noted above, state_machine uses the callback method's argument list
arity to determine whether to include the transition in the method call.
If you're using delegates, such as those defined in ActiveSupport or
Forwardable, the actual arity of the delegated method gets masked.  This
means that callbacks which reference delegates will always get passed the
transition as an argument.  For example:

  class Vehicle
    extend Forwardable
    delegate :refresh => :dashboard
    
    state_machine do
      before_transition :refresh
      ...
    end
    
    def dashboard
      @dashboard ||= Dashboard.new
    end
  end
  
  class Dashboard
    def refresh(transition)
      # ...
    end
  end

In the above example, <tt>Dashboard#refresh</tt> *must* defined a
+transition+ argument.  Otherwise, an +ArgumentError+ exception will get
raised.  The only way around this is to avoid the use of delegates and
manually define the delegate method so that the correct arity is used.

== Examples

Below is an example of a class with one state machine and various types
of +before+ transitions defined for it:

  class Vehicle
    state_machine do
      # Before all transitions
      before_transition :update_dashboard
      
      # Before specific transition:
      before_transition [:first_gear, :idling] => :parked, :on => :park, :do => :take_off_seatbelt
      
      # With conditional callback:
      before_transition all => :parked, :do => :take_off_seatbelt, :if => :seatbelt_on?
      
      # Using helpers:
      before_transition all - :stalled => same, :on => any - :crash, :do => :update_dashboard
      ...
    end
  end

As can be seen, any number of transitions can be created using various
combinations of configuration options.

Source | Google | Stack overflow

Edit

git clone [email protected]:pluginaweek/state_machine.git

cd state_machine

open lib/state_machine/machine.rb

Contribute

# Make a new branch

git checkout -b -your-name--update-docs-StateMachine--Machine-before_transition-for-pr


# Commit to git

git add lib/state_machine/machine.rbgit commit -m "better docs for StateMachine::Machine#before_transition"


# Open pull request

gem install hub # on a mac you can `brew install hub`

hub fork

git push <your name> -your-name--update-docs-StateMachine--Machine-before_transition-for-pr

hub pull-request


# Celebrate!