require 'state_machine/assertions'
require 'state_machine/eval_helpers'
module StateMachine
# A method was called in an invalid state context
class InvalidContext < Error
end
# Represents a module which will get evaluated within the context of a state.
#
# Class-level methods are proxied to the owner class, injecting a custom
# :if condition along with method. This assumes that the method has
# support for a set of configuration options, including :if. This
# condition will check that the object's state matches this context's state.
#
# Instance-level methods are used to define state-driven behavior on the
# state's owner class.
#
# == Examples
#
# class Vehicle
# class << self
# attr_accessor :validations
#
# def validate(options, &block)
# validations << options
# end
# end
#
# self.validations = []
# attr_accessor :state, :simulate
#
# def moving?
# self.class.validations.all? {|validation| validation[:if].call(self)}
# end
# end
#
# In the above class, a simple set of validation behaviors have been defined.
# Each validation consists of a configuration like so:
#
# Vehicle.validate :unless => :simulate
# Vehicle.validate :if => lambda {|vehicle| ...}
#
# In order to scope validations to a particular state context, the class-level
# +validate+ method can be invoked like so:
#
# machine = StateMachine::Machine.new(Vehicle)
# context = StateMachine::StateContext.new(machine.state(:first_gear))
# context.validate(:unless => :simulate)
#
# vehicle = Vehicle.new # => #
# vehicle.moving? # => false
#
# vehicle.state = 'first_gear'
# vehicle.moving? # => true
#
# vehicle.simulate = true
# vehicle.moving? # => false
class StateContext < Module
include Assertions
include EvalHelpers
# The state machine for which this context's state is defined
attr_reader :machine
# The state that must be present in an object for this context to be active
attr_reader :state
# Creates a new context for the given state
def initialize(state)
@state = state
@machine = state.machine
state_name = state.name
machine_name = machine.name
@condition = lambda {|object| object.class.state_machine(machine_name).states.matches?(object, state_name)}
end
# Creates a new transition that determines what to change the current state
# to when an event fires from this state.
#
# Since this transition is being defined within a state context, you do
# *not* need to specify the :from option for the transition. For
# example:
#
# state_machine do
# state :parked do
# transition :to => :idling, :on => [:ignite, :shift_up] # Transitions to :idling
# transition :from => [:idling, :parked], :on => :park, :unless => :seatbelt_on? # Transitions to :parked if seatbelt is off
# end
# end
#
# See StateMachine::Machine#transition for a description of the possible
# configurations for defining transitions.
def transition(options)
assert_valid_keys(options, :from, :to, :on, :if, :unless)
raise ArgumentError, 'Must specify :on event' unless options[:on]
raise ArgumentError, 'Must specify either :to or :from state' unless !options[:to] ^ !options[:from]
machine.transition(options.merge(options[:to] ? {:from => state.name} : {:to => state.name}))
end
# Hooks in condition-merging to methods that don't exist in this module
def method_missing(*args, &block)
# Get the configuration
if args.last.is_a?(Hash)
options = args.last
else
args << options = {}
end
# Get any existing condition that may need to be merged
if_condition = options.delete(:if)
unless_condition = options.delete(:unless)
# Provide scope access to configuration in case the block is evaluated
# within the object instance
proxy = self
proxy_condition = @condition
# Replace the configuration condition with the one configured for this
# proxy, merging together any existing conditions
options[:if] = lambda do |*condition_args|
# Block may be executed within the context of the actual object, so
# it'll either be the first argument or the executing context
object = condition_args.first || self
proxy.evaluate_method(object, proxy_condition) &&
Array(if_condition).all? {|condition| proxy.evaluate_method(object, condition)} &&
!Array(unless_condition).any? {|condition| proxy.evaluate_method(object, condition)}
end
# Evaluate the method on the owner class with the condition proxied
# through
machine.owner_class.send(*args, &block)
end
end
end