require 'state_machine/event' module PluginAWeek #:nodoc: module StateMachine # Represents a state machine for a particular attribute # # == State callbacks # # These callbacks are invoked in the following order: # 1. before_exit (old state) # 2. before_enter (new state) # 3. after_exit (old state) # 4. after_enter (new state) class Machine # The events that trigger transitions attr_reader :events # The attribute for which the state machine is being defined attr_accessor :attribute # The initial state that the machine will be in attr_reader :initial_state # The class that the attribute belongs to attr_reader :owner_class # Creates a new state machine for the given attribute # # Configuration options: # * +initial+ - The initial value to set the attribute to # # == Scopes # # This will automatically created a named scope called with_#{attribute} # that will find all records that have the attribute set to a given value. # For example, # # Switch.with_state('on') # => Finds all switches where the state is on # Switch.with_states('on', 'off') # => Finds all switches where the state is either on or off def initialize(owner_class, attribute, options = {}) options.assert_valid_keys(:initial) @owner_class = owner_class @attribute = attribute.to_s @initial_state = options[:initial] @events = {} add_named_scopes end # Gets the initial state of the machine for the given record. The record # is only used if a dynamic initial state is being used def initial_state(record) @initial_state.is_a?(Proc) ? @initial_state.call(record) : @initial_state end # Gets the initial state without processing it against a particular record def initial_state_without_processing @initial_state end # Defines an event of the system. This can take an optional hash that # defines callbacks which will be invoked when the object enters/exits # the event. # # Configuration options: # * +before+ - Invoked before the event has been executed # * +after+ - Invoked after the event has been executed # # == Callback order # # These callbacks are invoked in the following order: # 1. before # 2. after # # == Instance methods # # The following instance methods are generated when a new event is defined # (the "park" event is used as an example): # * park(*args) - Fires the "park" event, transitioning from the current state to the next valid state. This takes an optional +args+ list which is passed to the event callbacks. # * park!(*args) - Fires the "park" event, transitioning from the current state to the next valid state. This takes an optional +args+ list which is passed to the event callbacks. If the transition cannot happen (for validation, database, etc. reasons), then an error will be raised # # == Defining transitions # # +event+ requires a block which allows you to define the possible # transitions that can happen as a result of that event. For example, # # event :park do # transition :to => 'parked', :from => 'idle' # end # # event :first_gear do # transition :to => 'first_gear', :from => 'parked', :if => :seatbelt_on? # end # # See PluginAWeek::StateMachine::Event#transition for more information on # the possible options that can be passed in. # # == Example # # class Car < ActiveRecord::Base # state_machine(:state, :initial => 'parked') do # event :park, :after => :release_seatbelt do # transition :to => 'parked', :from => %w(first_gear reverse) # end # ... # end # end def event(name, options = {}, &block) name = name.to_s event = events[name] = Event.new(self, name, options) event.instance_eval(&block) event end # Define state callbacks %w(before_exit before_enter after_exit after_enter).each do |callback_type| define_method(callback_type) {|state, callback| add_callback(callback_type, state, callback)} end private # Adds the given callback to the callback chain during a state transition def add_callback(type, state, callback) callback_name = "#{type}_#{attribute}_#{state}" owner_class.define_callbacks(callback_name) owner_class.send(callback_name, callback) end # Add named scopes for finding records with a particular value or values # for the attribute def add_named_scopes [attribute, attribute.pluralize].each do |name| unless owner_class.respond_to?("with_#{name}") name = "with_#{name}" owner_class.named_scope name, Proc.new {|*values| {:conditions => {attribute => values.flatten}}} end end end end end end