lib/state_machine/machine.rb in state_machine-0.10.1 vs lib/state_machine/machine.rb in state_machine-0.10.2
- old
+ new
@@ -443,13 +443,12 @@
@namespace = options[:namespace]
@messages = options[:messages] || {}
@action = options[:action]
@use_transactions = options[:use_transactions]
@initialize_state = options[:initialize]
- @helpers = {:instance => {}, :class => {}}
self.owner_class = owner_class
- self.initial_state = options[:initial]
+ self.initial_state = options[:initial] unless owner_class.state_machines.any? {|name, machine| machine.attribute == attribute && machine != self}
# Define class integration
define_helpers
define_scopes(options[:plural])
after_initialize
@@ -467,11 +466,10 @@
@events = @events.dup
@events.machine = self
@states = @states.dup
@states.machine = self
@callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup, :failure => @callbacks[:failure].dup}
- @helpers = {:instance => @helpers[:instance].dup, :class => @helpers[:class].dup}
end
# Sets the class which is the owner of this state machine. Any methods
# generated by states, events, or other parts of the machine will be defined
# on the given owner class.
@@ -509,60 +507,10 @@
# Update all states to reflect the new initial state
states.each {|state| state.initial = (state.name == @initial_state)}
end
- # Initializes the state on the given object. Initial values are only set if
- # the machine's attribute hasn't been previously initialized.
- def initialize_state(object, options = {})
- write(object, :state, initial_state(object).value) if initialize_state?(object, options)
- end
-
- # Gets the actual name of the attribute on the machine's owner class that
- # stores data with the given name.
- def attribute(name = :state)
- name == :state ? @attribute : :"#{self.name}_#{name}"
- end
-
- # Defines a new helper method in an instance or class scope with the given
- # name. If the method is already defined in the scope, then this will not
- # override it.
- #
- # Example:
- #
- # # Instance helper
- # machine.define_helper(:instance, :state_name) do |machine, object, _super|
- # machine.states.match(object)
- # end
- #
- # # Class helper
- # machine.define_helper(:class, :state_machine_name) do |machine, klass, _super|
- # "State"
- # end
- def define_helper(scope, method, &block)
- @helpers.fetch(scope)[method] = block
- @helper_modules.fetch(scope).class_eval <<-end_eval, __FILE__, __LINE__
- def #{method}(*args)
- _super = lambda {|*new_args| new_args.empty? ? super(*args) : super(*new_args)}
- #{scope == :class ? 'self' : 'self.class'}.state_machine(#{name.inspect}).call_helper(#{scope.inspect}, #{method.inspect}, self, _super, *args)
- end
- end_eval
- end
-
- # Invokes the helper method defined in the given scope.
- #
- # Example:
- #
- # # Instance helper
- # machine.call_helper(:instance, :state_name, self, lambda {super})
- #
- # # Class helper
- # machine.call_helper(:class, :state_machine_name, self, lambda {super})
- def call_helper(scope, method, object, _super, *args)
- @helpers.fetch(scope).fetch(method).call(self, object, _super, *args)
- end
-
# Gets the initial state of the machine for the given object. If a dynamic
# initial state was configured for this machine, then the object will be
# passed into the lambda block to help determine the actual state.
#
# == Examples
@@ -594,18 +542,89 @@
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:idling value="idling" initial=false>
#
# vehicle.force_idle = false
# Vehicle.state_machine.initial_state(vehicle) # => #<StateMachine::State name=:parked value="parked" initial=false>
def initial_state(object)
- states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state)
+ states.fetch(dynamic_initial_state? ? evaluate_method(object, @initial_state) : @initial_state) if instance_variable_defined?('@initial_state')
end
# Whether a dynamic initial state is being used in the machine
def dynamic_initial_state?
@initial_state.is_a?(Proc)
end
+ # Initializes the state on the given object. Initial values are only set if
+ # the machine's attribute hasn't been previously initialized.
+ #
+ # Configuration options:
+ # * <tt>:force</tt> - Whether to initialize the state regardless of its
+ # current value
+ # * <tt>:to</tt> - A hash to set the initial value in instead of writing
+ # directly to the object
+ def initialize_state(object, options = {})
+ state = initial_state(object)
+ if state && (options[:force] || initialize_state?(object))
+ value = state.value
+
+ if hash = options[:to]
+ hash[attribute.to_s] = value
+ else
+ write(object, :state, value)
+ end
+ end
+ end
+
+ # Gets the actual name of the attribute on the machine's owner class that
+ # stores data with the given name.
+ def attribute(name = :state)
+ name == :state ? @attribute : :"#{self.name}_#{name}"
+ end
+
+ # Defines a new helper method in an instance or class scope with the given
+ # name. If the method is already defined in the scope, then this will not
+ # override it.
+ #
+ # Example:
+ #
+ # # Instance helper
+ # machine.define_helper(:instance, :state_name) do |machine, object|
+ # machine.states.match(object).name
+ # end
+ #
+ # # Class helper
+ # machine.define_helper(:class, :state_machine_name) do |machine, klass|
+ # "State"
+ # end
+ #
+ # You can also define helpers using string evaluation like so:
+ #
+ # # Instance helper
+ # machine.define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ # def state_name
+ # self.class.state_machine(:state).states.match(self).name
+ # end
+ # end_eval
+ #
+ # # Class helper
+ # machine.define_helper :class, <<-end_eval, __FILE__, __LINE__ + 1
+ # def state_machine_name
+ # "State"
+ # end
+ # end_eval
+ def define_helper(scope, method, *args, &block)
+ if block_given?
+ name = self.name
+ @helper_modules.fetch(scope).class_eval do
+ define_method(method) do |*args|
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *args)
+ end
+ end
+ else
+ @helper_modules.fetch(scope).class_eval(method, *args)
+ end
+ end
+
# Customizes the definition of one or more states in the machine.
#
# Configuration options:
# * <tt>:value</tt> - The actual value to store when an object transitions
# to the state. Default is the name (stringified).
@@ -1533,13 +1552,13 @@
def after_initialize
end
# Determines if the machine's attribute needs to be initialized. This
# will only be true if the machine's attribute is blank.
- def initialize_state?(object, options = {})
+ def initialize_state?(object)
value = read(object, :state)
- value.nil? || value.respond_to?(:empty?) && value.empty?
+ (value.nil? || value.respond_to?(:empty?) && value.empty?) && !states[value, :value]
end
# Adds helper methods for interacting with the state machine, including
# for states, events, and transitions
def define_helpers
@@ -1553,13 +1572,15 @@
# Defines the initial values for state machine attributes. Static values
# are set prior to the original initialize method and dynamic values are
# set *after* the initialize method in case it is dependent on it.
def define_state_initializer
- define_helper(:instance, :initialize) do |machine, object, _super, *|
- object.class.state_machines.initialize_states(object) { _super.call }
- end
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def initialize(*)
+ self.class.state_machines.initialize_states(self) { super }
+ end
+ end_eval
end
# Adds reader/writer methods for accessing the state attribute
def define_state_accessor
attribute = self.attribute
@@ -1569,55 +1590,59 @@
# Adds predicate method to the owner class for determining the name of the
# current state
def define_state_predicate
call_super = owner_class_ancestor_has_method?("#{name}?")
- define_helper(:instance, "#{name}?") do |machine, object, _super, *args|
- args.empty? && call_super ? _super.call : machine.states.matches?(object, args.first)
- end
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def #{name}?(*args)
+ args.empty? && #{call_super} ? super : self.class.state_machine(#{name.inspect}).states.matches?(self, *args)
+ end
+ end_eval
end
# Adds helper methods for getting information about this state machine's
# events
def define_event_helpers
# Gets the events that are allowed to fire on the current object
- define_helper(:instance, attribute(:events)) do |machine, object, _super, *args|
+ define_helper(:instance, attribute(:events)) do |machine, object, *args|
machine.events.valid_for(object, *args).map {|event| event.name}
end
# Gets the next possible transitions that can be run on the current
# object
- define_helper(:instance, attribute(:transitions)) do |machine, object, _super, *args|
+ define_helper(:instance, attribute(:transitions)) do |machine, object, *args|
machine.events.transitions_for(object, *args)
end
# Add helpers for tracking the event / transition to invoke when the
# action is called
if action
event_attribute = attribute(:event)
- define_helper(:instance, event_attribute) do |machine, object, *|
+ define_helper(:instance, event_attribute) do |machine, object|
# Interpret non-blank events as present
event = machine.read(object, :event, true)
event && !(event.respond_to?(:empty?) && event.empty?) ? event.to_sym : nil
end
# A roundabout way of writing the attribute is used here so that
# integrations can hook into this modification
- define_helper(:instance, "#{event_attribute}=") do |machine, object, _super, value|
+ define_helper(:instance, "#{event_attribute}=") do |machine, object, value|
machine.write(object, :event, value, true)
end
event_transition_attribute = attribute(:event_transition)
- @helper_modules[:instance].class_eval { protected; attr_accessor event_transition_attribute }
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ protected; attr_accessor #{event_transition_attribute.inspect}
+ end_eval
end
end
# Adds helper methods for getting information about this state machine's
# available transition paths
def define_path_helpers
# Gets the paths of transitions available to the current object
- define_helper(:instance, attribute(:paths)) do |machine, object, _super, *args|
+ define_helper(:instance, attribute(:paths)) do |machine, object, *args|
machine.paths_for(object, *args)
end
end
# Determines whether action helpers should be defined for this machine.
@@ -1643,15 +1668,17 @@
action_hook = self.action_hook
action = self.action
private_action_hook = owner_class.private_method_defined?(action_hook)
# Only define helper if it hasn't
- define_helper(:instance, action_hook) do |machine, object, _super, *args|
- object.class.state_machines.transitions(object, action).perform { _super.call }
- end
-
- @helper_modules[:instance].class_eval { private action_hook } if private_action_hook
+ define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
+ def #{action_hook}(*)
+ self.class.state_machines.transitions(self, #{action.inspect}).perform { super }
+ end
+
+ private #{action_hook.inspect} if #{private_action_hook}
+ end_eval
end
# The method to hook into for triggering transitions when invoked. By
# default, this is the action configured for the machine.
#
@@ -1672,26 +1699,26 @@
# Adds helper methods for accessing naming information about states and
# events on the owner class
def define_name_helpers
# Gets the humanized version of a state
- define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, _super, state|
+ define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, state|
machine.states.fetch(state).human_name(klass)
end
# Gets the humanized version of an event
- define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, _super, event|
+ define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, event|
machine.events.fetch(event).human_name(klass)
end
# Gets the state name for the current value
- define_helper(:instance, attribute(:name)) do |machine, object, *|
+ define_helper(:instance, attribute(:name)) do |machine, object|
machine.states.match!(object).name
end
# Gets the human state name for the current value
- define_helper(:instance, "human_#{attribute(:name)}") do |machine, object, *|
+ define_helper(:instance, "human_#{attribute(:name)}") do |machine, object|
machine.states.match!(object).human_name(object.class)
end
end
# Defines the with/without scope helpers for this attribute. Both the
@@ -1707,10 +1734,10 @@
method = "#{kind}_#{name}"
if scope = send("create_#{kind}_scope", method)
# Converts state names to their corresponding values so that they
# can be looked up properly
- define_helper(:class, method) do |machine, klass, _super, *states|
+ define_helper(:class, method) do |machine, klass, *states|
values = states.flatten.map {|state| machine.states.fetch(state).value}
scope.call(klass, values)
end
end
end