require 'state_machine/assertions'
require 'state_machine/condition_proxy'
module StateMachine
# A state defines a value that an attribute can be in after being transitioned
# 0 or more times. States can represent a value of any type in Ruby, though
# the most common (and default) type is String.
#
# In addition to defining the machine's value, a state can also define a
# behavioral context for an object when that object is in the state. See
# StateMachine::Machine#state for more information about how state-driven
# behavior can be utilized.
class State
include Assertions
# The state machine for which this state is defined
attr_accessor :machine
# The unique identifier for the state used in event and callback definitions
attr_reader :name
# The value that is written to a machine's attribute when an object
# transitions into this state
attr_writer :value
# Whether or not this state is the initial state to use for new objects
attr_accessor :initial
# A custom lambda block for determining whether a given value matches this
# state
attr_accessor :matcher
# Tracks all of the methods that have been defined for the machine's owner
# class when objects are in this state.
#
# Maps :method_name => UnboundMethod
attr_reader :methods
# Creates a new state within the context of the given machine.
#
# Configuration options:
# * :initial - Whether this state is the beginning state for the
# machine. Default is false.
# * :value - The value to store when an object transitions to this
# state. Default is the name (stringified).
# * :if - Determines whether a value matches this state
# (e.g. :value => lambda {Time.now}, :if => lambda {|state| !state.nil?}).
# By default, the configured value is matched.
def initialize(machine, name, options = {}) #:nodoc:
assert_valid_keys(options, :initial, :value, :if)
@machine = machine
@name = name
@value = options.include?(:value) ? options[:value] : name && name.to_s
@matcher = options[:if]
@methods = {}
@initial = options.include?(:initial) && options[:initial]
add_predicate
end
# Creates a copy of this state in addition to the list of associated
# methods to prevent conflicts across different states.
def initialize_copy(orig) #:nodoc:
super
@methods = methods.dup
end
# Generates a human-readable description of this state's name / value:
#
# For example,
#
# State.new(machine, :parked).description # => "parked"
# State.new(machine, :parked, :value => :parked).description # => "parked"
# State.new(machine, :parked, :value => nil).description # => "parked (nil)"
# State.new(machine, :parked, :value => 1).description # => "parked (1)"
# State.new(machine, :parked, :value => lambda {Time.now}).description # => "parked (*)
def description
description = name ? name.to_s : name.inspect
description << " (#{@value.is_a?(Proc) ? '*' : @value.inspect})" unless name.to_s == @value.to_s
description
end
# The value that represents this state. If the value is a lambda block,
# then it will be evaluated at this time. Otherwise, the static value is
# returned.
#
# For example,
#
# State.new(machine, :parked, :value => 1).value # => 1
# State.new(machine, :parked, :value => lambda {Time.now}).value # => Tue Jan 01 00:00:00 UTC 2008
def value
@value.is_a?(Proc) ? @value.call : @value
end
# Determines whether this state matches the given value. If no matcher is
# configured, then this will check whether the values are equivalent.
# Otherwise, the matcher will determine the result.
#
# For example,
#
# # Without a matcher
# state = State.new(machine, :parked, :value => 1)
# state.matches?(1) # => true
# state.matches?(2) # => false
#
# # With a matcher
# state = State.new(machine, :parked, :value => lambda {Time.now}, :if => lambda {|value| !value.nil?})
# state.matches?(nil) # => false
# state.matches?(Time.now) # => true
def matches?(other_value)
matcher ? matcher.call(other_value) : other_value == value
end
# Defines a context for the state which will be enabled on instances of the
# owner class when the machine is in this state.
#
# This can be called multiple times. Each time a new context is created, a
# new module will be included in the owner class.
def context(&block)
owner_class = machine.owner_class
attribute = machine.attribute
name = self.name
# Evaluate the method definitions
context = ConditionProxy.new(owner_class, lambda {|object| object.send("#{attribute}_name") == name})
context.class_eval(&block)
# Define all of the methods that were created in the module so that they
# don't override the core behavior (i.e. calling the state method)
context.instance_methods.each do |method|
unless owner_class.instance_methods.include?(method)
# Calls the method defined by the current state of the machine. This
# is done using string evaluation so that any block passed into the
# method can then be passed to the state's context method, which is
# not possible with lambdas in Ruby 1.8.6.
owner_class.class_eval <<-end_eval, __FILE__, __LINE__
def #{method}(*args, &block)
self.class.state_machines[#{attribute.inspect}].state_for(self).call(self, #{method.inspect}, *args, &block)
end
end_eval
end
# Track the method defined for the context so that it can be invoked
# at a later point in time
methods[method.to_sym] = context.instance_method(method)
end
# Include the context so that it can be bound to the owner class (the
# context is considered an ancestor, so it's allowed to be bound)
owner_class.class_eval do
include context
end
context
end
# Calls a method defined in this state's context on the given object. All
# arguments and any block will be passed into the method defined.
#
# If the method has never been defined for this state, then a NoMethodError
# will be raised.
def call(object, method, *args, &block)
if context_method = methods[method.to_sym]
# Method is defined by the state: proxy it through
context_method.bind(object).call(*args, &block)
else
# Raise exception as if the method never existed on the original object
raise NoMethodError, "undefined method '#{method}' for #{object} in state #{machine.state_for(object).name.inspect}"
end
end
# Draws a representation of this state on the given machine. This will
# create a new node on the graph with the following properties:
# * +label+ - The human-friendly description of the state.
# * +width+ - The width of the node. Always 1.
# * +height+ - The height of the node. Always 1.
# * +shape+ - The actual shape of the node. If the state is the initial
# state, then "doublecircle", otherwise "circle".
#
# The actual node generated on the graph will be returned.
def draw(graph)
graph.add_node(name ? name.to_s : 'nil',
:label => description,
:width => '1',
:height => '1',
:shape => initial ? 'doublecircle' : 'ellipse'
)
end
# Generates a nicely formatted description of this state's contents.
#
# For example,
#
# state = StateMachine::State.new(machine, :parked, :value => 1, :initial => true)
# state # => #
def inspect
attributes = [[:name, name], [:value, @value], [:initial, initial], [:context, methods.keys]]
"#<#{self.class} #{attributes.map {|attr, value| "#{attr}=#{value.inspect}"} * ' '}>"
end
private
# Adds a predicate method to the owner class so long as a name has
# actually been configured for the state
def add_predicate
return unless name
attribute = machine.attribute
qualified_name = name = self.name
qualified_name = "#{machine.namespace}_#{name}" if machine.namespace
machine.owner_class.class_eval do
# Checks whether the current value matches this state
define_method("#{qualified_name}?") do
self.class.state_machines[attribute].state(name).matches?(send(attribute))
end
end
end
end
end