lib/state_machine/machine.rb in state_machine-0.9.4 vs lib/state_machine/machine.rb in state_machine-0.10.0
- old
+ new
@@ -6,10 +6,11 @@
require 'state_machine/event'
require 'state_machine/callback'
require 'state_machine/node_collection'
require 'state_machine/state_collection'
require 'state_machine/event_collection'
+require 'state_machine/path_collection'
require 'state_machine/matcher_helpers'
module StateMachine
# Represents a state machine for a particular attribute. State machines
# consist of states, events and a set of transitions that define how the
@@ -88,12 +89,12 @@
# One important note about using this technique for running transitions is
# that if the class in which the state machine is defined *also* defines the
# action being invoked (and not a superclass), then it must manually run the
# StateMachine hook that checks for event attributes.
#
- # For example, in ActiveRecord, DataMapper, MongoMapper, and Sequel, the
- # default action (+save+) is already defined in a base class. As a result,
+ # For example, in ActiveRecord, DataMapper, Mongoid, MongoMapper, and Sequel,
+ # the default action (+save+) is already defined in a base class. As a result,
# when a state machine is defined in a model / resource, StateMachine can
# automatically hook into the +save+ action.
#
# On the other hand, the Vehicle class from above defined its own +save+
# method (and there is no +save+ method in its superclass). As a result, it
@@ -288,14 +289,14 @@
# database that may allow for transactions, persistent storage,
# search/filters, callbacks, etc.
#
# When a state machine is defined for classes using any of the above libraries,
# it will try to automatically determine the integration to use (Agnostic,
- # ActiveModel, ActiveRecord, DataMapper, MongoMapper, or Sequel) based on the
- # class definition. To see how each integration affects the machine's
- # behavior, refer to all constants defined under the StateMachine::Integrations
- # namespace.
+ # ActiveModel, ActiveRecord, DataMapper, Mongoid, MongoMapper, or Sequel)
+ # based on the class definition. To see how each integration affects the
+ # machine's behavior, refer to all constants defined under the
+ # StateMachine::Integrations namespace.
class Machine
include Assertions
include EvalHelpers
include MatcherHelpers
@@ -414,37 +415,39 @@
attr_reader :use_transactions
# Creates a new state machine for the given attribute
def initialize(owner_class, *args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
- assert_valid_keys(options, :attribute, :initial, :action, :plural, :namespace, :integration, :messages, :use_transactions)
+ assert_valid_keys(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
# Find an integration that matches this machine's owner class
if options.include?(:integration)
integration = StateMachine::Integrations.find(options[:integration]) if options[:integration]
else
integration = StateMachine::Integrations.match(owner_class)
end
if integration
extend integration
- options = integration.defaults.merge(options) if integration.respond_to?(:defaults)
+ options = (integration.defaults || {}).merge(options)
end
# Add machine-wide defaults
- options = {:use_transactions => true}.merge(options)
+ options = {:use_transactions => true, :initialize => true}.merge(options)
# Set machine configuration
@name = args.first || :state
@attribute = options[:attribute] || @name
@events = EventCollection.new(self)
@states = StateCollection.new(self)
- @callbacks = {:before => [], :after => []}
+ @callbacks = {:before => [], :after => [], :failure => []}
@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]
# Define class integration
define_helpers
@@ -463,35 +466,35 @@
@events = @events.dup
@events.machine = self
@states = @states.dup
@states.machine = self
- @callbacks = {:before => @callbacks[:before].dup, :after => @callbacks[:after].dup}
+ @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.
def owner_class=(klass)
@owner_class = klass
# Create modules for extending the class with state/event-specific methods
- class_helper_module = @class_helper_module = Module.new
- instance_helper_module = @instance_helper_module = Module.new
+ @helper_modules = helper_modules = {:instance => Module.new, :class => Module.new}
owner_class.class_eval do
- extend class_helper_module
- include instance_helper_module
+ extend helper_modules[:class]
+ include helper_modules[:instance]
end
# Add class-/instance-level methods to the owner class for state initialization
unless owner_class < StateMachine::InstanceMethods
owner_class.class_eval do
extend StateMachine::ClassMethods
include StateMachine::InstanceMethods
end
- define_state_initializer
+ define_state_initializer if @initialize_state
end
# Record this machine as matched to the name in the current owner class.
# This will override any machines mapped to the same name in any superclasses.
owner_class.state_machines[name] = self
@@ -506,59 +509,58 @@
# 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. This will always write to the
- # attribute regardless of whether a value is already present.
- def initialize_state(object)
- write(object, :state, initial_state(object).value)
+ # 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 instance method with the given name on the machine's owner
- # class. If the method is already defined in the class, then this will not
+ # 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:
#
- # machine.define_instance_method(:state_name) do |machine, object|
+ # # Instance helper
+ # machine.define_helper(:instance, :state_name) do |machine, object, _super|
# machine.states.match(object)
# end
- def define_instance_method(method, &block)
- name = self.name
-
- @instance_helper_module.class_eval do
- define_method(method) do |*args|
- block.call(self.class.state_machine(name), self, *args)
+ #
+ # # 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
+ end_eval
end
- attr_reader :instance_helper_module
- # Defines a new class method with the given name on the machine's owner
- # class. If the method is already defined in the class, then this will not
- # override it.
+ # Invokes the helper method defined in the given scope.
#
# Example:
#
- # machine.define_class_method(:states) do |machine, klass|
- # machine.states.keys
- # end
- def define_class_method(method, &block)
- name = self.name
-
- @class_helper_module.class_eval do
- define_method(method) do |*args|
- block.call(self.state_machine(name), self, *args)
- end
- end
+ # # 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.
@@ -899,12 +901,13 @@
# vehicle = Vehicle.new # => #<Vehicle:0xb7d94ab0 @state="parked">
# Vehicle.state_machine.write(vehicle, :state, 'idling') # => Equivalent to vehicle.state = 'idling'
# Vehicle.state_machine.write(vehicle, :event, 'park') # => Equivalent to vehicle.state_event = 'park'
# vehicle.state # => "idling"
# vehicle.event # => "park"
- def write(object, attribute, value)
- object.send("#{self.attribute(attribute)}=", value)
+ def write(object, attribute, value, ivar = false)
+ attribute = self.attribute(attribute)
+ ivar ? object.instance_variable_set("@#{attribute}", value) : object.send("#{attribute}=", value)
end
# Defines one or more events for the machine and the transitions that can
# be performed when those events are run.
#
@@ -918,34 +921,44 @@
#
# == Instance methods
#
# The following instance methods are generated when a new event is defined
# (the "park" event is used as an example):
- # * <tt>can_park?</tt> - Checks whether the "park" event can be fired given
- # the current state of the object. This will *not* run validations in
- # ORM integrations. To check whether an event can fire *and* passes
- # validations, use event attributes (e.g. state_event) as described in the
- # "Events" documentation of each ORM integration.
- # * <tt>park_transition</tt> - Gets the next transition that would be
- # performed if the "park" event were to be fired now on the object or nil
- # if no transitions can be performed.
# * <tt>park(..., run_action = true)</tt> - Fires the "park" event,
# transitioning from the current state to the next valid state. If the
# last argument is a boolean, it will control whether the machine's action
# gets run.
# * <tt>park!(..., run_action = true)</tt> - Fires the "park" event,
# transitioning from the current state to the next valid state. If the
# transition fails, then a StateMachine::InvalidTransition error will be
# raised. If the last argument is a boolean, it will control whether the
# machine's action gets run.
+ # * <tt>can_park?(requirements = {})</tt> - Checks whether the "park" event can be fired given
+ # the current state of the object. This will *not* run validations in
+ # ORM integrations. To check whether an event can fire *and* passes
+ # validations, use event attributes (e.g. state_event) as described in the
+ # "Events" documentation of each ORM integration.
+ # * <tt>park_transition(requirements = {})</tt> - Gets the next transition that would be
+ # performed if the "park" event were to be fired now on the object or nil
+ # if no transitions can be performed.
#
# With a namespace of "car", the above names map to the following methods:
# * <tt>can_park_car?</tt>
# * <tt>park_car_transition</tt>
# * <tt>park_car</tt>
# * <tt>park_car!</tt>
#
+ # The <tt>can_park?</tt> and <tt>park_transition</tt> helpers both take an
+ # optional set of requirements for determining what transitions are available
+ # for the current object. These requirements include:
+ # * <tt>:from</tt> - One or more states to transition from. If none are
+ # specified, then this will be the object's current state.
+ # * <tt>:to</tt> - One or more states to transition to. If none are
+ # specified, then this will match any to state.
+ # * <tt>:guard</tt> - Whether to guard transitions with the if/unless
+ # conditionals defined for each one. Default is true.
+ #
# == 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,
#
@@ -1130,26 +1143,10 @@
#
# 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
#
- # == Result requirements
- #
- # By default, +after_transition+ callbacks and code executed after an
- # +around_transition+ callback yields will only be run if the transition
- # was performed successfully. A transition is successful if the machine's
- # action is not configured or does not return false when it is invoked.
- # In order to include failed attempts when running an +after_transition+ or
- # +around_transition+ callback, the <tt>:include_failures</tt> option can be
- # specified like so:
- #
- # after_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
- # after_transition :do => ... # Runs only on successful attempts to transition
- #
- # around_transition :include_failures => true, :do => ... # Runs on all attempts to transition, including failures
- # around_transition :do => ... # Runs only on successful attempts to transition
- #
# == Verbose Requirements
#
# Requirements can also be defined using verbose options rather than the
# implicit Hash syntax and helper methods described above.
#
@@ -1317,10 +1314,119 @@
options = (args.last.is_a?(Hash) ? args.pop : {})
options[:do] = args if args.any?
add_callback(:around, options, &block)
end
+ # Creates a callback that will be invoked *after* a transition failures to
+ # be performed so long as the given requirements match the transition.
+ #
+ # See +before_transition+ for a description of the possible configurations
+ # for defining callbacks. *Note* however that you cannot define the state
+ # requirements in these callbacks. You may only define event requirements.
+ #
+ # = The callback
+ #
+ # Failure callbacks get invoked whenever an event fails to execute. This
+ # can happen when no transition is available, a +before+ callback halts
+ # execution, or the action associated with this machine fails to succeed.
+ # In any of these cases, any failure callback that matches the attempted
+ # transition will be run.
+ #
+ # For example,
+ #
+ # class Vehicle
+ # state_machine do
+ # after_failure do |vehicle, transition|
+ # logger.error "vehicle #{vehicle} failed to transition on #{transition.event}"
+ # end
+ #
+ # after_failure :on => :ignite, :do => :log_ignition_failure
+ #
+ # ...
+ # end
+ # end
+ def after_failure(*args, &block)
+ options = (args.last.is_a?(Hash) ? args.pop : {})
+ options[:do] = args if args.any?
+ assert_valid_keys(options, :on, :do, :if, :unless)
+
+ add_callback(:failure, options, &block)
+ end
+
+ # Generates a list of the possible transition sequences that can be run on
+ # the given object. These paths can reveal all of the possible states and
+ # events that can be encountered in the object's state machine based on the
+ # object's current state.
+ #
+ # Configuration options:
+ # * +from+ - The initial state to start all paths from. By default, this
+ # is the object's current state.
+ # * +to+ - The target state to end all paths on. By default, paths will
+ # end when they loop back to the first transition on the path.
+ # * +deep+ - Whether to allow the target state to be crossed more than once
+ # in a path. By default, paths will immediately stop when the target
+ # state (if specified) is reached. If this is enabled, then paths can
+ # continue even after reaching the target state; they will stop when
+ # reaching the target state a second time.
+ #
+ # *Note* that the object is never modified when the list of paths is
+ # generated.
+ #
+ # == Examples
+ #
+ # class Vehicle
+ # state_machine :initial => :parked do
+ # event :ignite do
+ # transition :parked => :idling
+ # end
+ #
+ # event :shift_up do
+ # transition :idling => :first_gear, :first_gear => :second_gear
+ # end
+ #
+ # event :shift_down do
+ # transition :second_gear => :first_gear, :first_gear => :idling
+ # end
+ # end
+ # end
+ #
+ # vehicle = Vehicle.new # => #<Vehicle:0xb7c27024 @state="parked">
+ # vehicle.state # => "parked"
+ #
+ # vehicle.state_paths
+ # # => [
+ # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_down from="second_gear" from_name=:second_gear to="first_gear" to_name=:first_gear>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>],
+ # #
+ # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_down from="first_gear" from_name=:first_gear to="idling" to_name=:idling>]
+ # # ]
+ #
+ # vehicle.state_paths(:from => :parked, :to => :second_gear)
+ # # => [
+ # # [#<StateMachine::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="idling" from_name=:idling to="first_gear" to_name=:first_gear>,
+ # # #<StateMachine::Transition attribute=:state event=:shift_up from="first_gear" from_name=:first_gear to="second_gear" to_name=:second_gear>]
+ # # ]
+ #
+ # In addition to getting the possible paths that can be accessed, you can
+ # also get summary information about the states / events that can be
+ # accessed at some point along one of the paths. For example:
+ #
+ # # Get the list of states that can be accessed from the current state
+ # vehicle.state_paths.to_states # => [:idling, :first_gear, :second_gear]
+ #
+ # # Get the list of events that can be accessed from the current state
+ # vehicle.state_paths.events # => [:ignite, :shift_up, :shift_down]
+ def paths_for(object, requirements = {})
+ PathCollection.new(object, self, requirements)
+ end
+
# Marks the given object as invalid with the given message.
#
# By default, this is a no-op.
def invalidate(object, attribute, message, values = [])
end
@@ -1398,163 +1504,194 @@
edges.each {|edge| edge.fontname = options[:font]}
end
# Generate the graph
graphvizVersion = Constants::RGV_VERSION.split('.')
+ file = File.join(options[:path], "#{options[:name]}.#{options[:format]}")
if graphvizVersion[1] == '9' && graphvizVersion[2] == '0'
- outputOptions = {
- :output => options[:format],
- :file => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
- }
+ outputOptions = {:output => options[:format], :file => file}
else
- outputOptions = {
- options[:format] => File.join(options[:path], "#{options[:name]}.#{options[:format]}")
- }
+ outputOptions = {options[:format] => file}
end
graph.output(outputOptions)
graph
rescue LoadError
$stderr.puts 'Cannot draw the machine. `gem install ruby-graphviz` >= v0.9.0 and try again.'
false
end
end
- # Determines whether a helper method was defined for firing attribute-based
- # event transitions when the configuration action gets called.
- def action_helper_defined?
- @action_helper_defined
+ # Determines whether an action hook was defined for firing attribute-based
+ # event transitions when the configured action gets called.
+ def action_hook?(self_only = false)
+ @action_hook_defined || !self_only && owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self && machine.action_hook?(true)}
end
protected
# Runs additional initialization hooks. By default, this is a no-op.
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 = {})
+ value = read(object, :state)
+ value.nil? || value.respond_to?(:empty?) && value.empty?
+ end
+
# Adds helper methods for interacting with the state machine, including
# for states, events, and transitions
def define_helpers
define_state_accessor
define_state_predicate
define_event_helpers
- define_action_helpers if action
+ define_path_helpers
+ define_action_helpers if define_action_helpers?
define_name_helpers
end
# 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
- @instance_helper_module.class_eval <<-end_eval, __FILE__, __LINE__
- def initialize(*args)
- initialize_state_machines(:dynamic => false)
- super
- initialize_state_machines(:dynamic => true)
- end
- end_eval
+ define_helper(:instance, :initialize) do |machine, object, _super, *|
+ object.class.state_machines.initialize_states(object) { _super.call }
+ end
end
# Adds reader/writer methods for accessing the state attribute
def define_state_accessor
attribute = self.attribute
- @instance_helper_module.class_eval do
- attr_accessor attribute
- end
+ @helper_modules[:instance].class_eval { attr_accessor attribute }
end
# Adds predicate method to the owner class for determining the name of the
# current state
def define_state_predicate
- define_instance_method("#{name}?") do |machine, object, state|
- machine.states.matches?(object, state)
+ 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
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_instance_method(attribute(:events)) do |machine, object|
- machine.events.valid_for(object).map {|event| event.name}
+ define_helper(:instance, attribute(:events)) do |machine, object, _super, *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_instance_method(attribute(:transitions)) do |machine, object, *args|
+ define_helper(:instance, attribute(:transitions)) do |machine, object, _super, *args|
machine.events.transitions_for(object, *args)
end
- # Add helpers for interacting with the action
+ # Add helpers for tracking the event / transition to invoke when the
+ # action is called
if action
- # Tracks the event / transition to invoke when the action is called
event_attribute = attribute(:event)
- event_transition_attribute = attribute(:event_transition)
- @instance_helper_module.class_eval do
- attr_writer event_attribute
-
- protected
- attr_accessor event_transition_attribute
- end
-
- # Interpret non-blank events as present
- define_instance_method(attribute(:event)) 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|
+ machine.write(object, :event, value, true)
+ end
+
+ event_transition_attribute = attribute(:event_transition)
+ @helper_modules[:instance].class_eval { protected; attr_accessor event_transition_attribute }
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|
+ machine.paths_for(object, *args)
+ end
+ end
+
+ # Determines whether action helpers should be defined for this machine.
+ # This is only true if there is an action configured and no other machines
+ # have process this same configuration already.
+ def define_action_helpers?
+ action && !owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
+ end
+
# Adds helper methods for automatically firing events when an action
# is invoked
- def define_action_helpers(action_hook = self.action)
- private_action = owner_class.private_method_defined?(action_hook)
- action_defined = @action_helper_defined = owner_class.ancestors.any? do |ancestor|
- ancestor != owner_class && (ancestor.method_defined?(action_hook) || ancestor.private_method_defined?(action_hook))
+ def define_action_helpers
+ if action_hook
+ @action_hook_defined = true
+ define_action_hook
end
- action_overridden = owner_class.state_machines.any? {|name, machine| machine.action == action && machine != self}
+ end
+
+ # Hooks directly into actions by defining the same method in an included
+ # module. As a result, when the action gets invoked, any state events
+ # defined for the object will get run. Method visibility is preserved.
+ def define_action_hook
+ action_hook = self.action_hook
+ action = self.action
+ private_action_hook = owner_class.private_method_defined?(action_hook)
- # Only define helper if:
- # 1. Action was originally defined somewhere other than the owner class
- # 2. It hasn't already been overridden by another machine
- if action_defined && !action_overridden
- action = self.action
- @instance_helper_module.class_eval do
- define_method(action_hook) do |*args|
- self.class.state_machines.transitions(self, action).perform { super(*args) }
- end
-
- private action_hook if private_action
- end
-
- true
- else
- false
+ # 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
end
+ # The method to hook into for triggering transitions when invoked. By
+ # default, this is the action configured for the machine.
+ #
+ # Since the default hook technique relies on module inheritance, the
+ # action must be defined in an ancestor of the owner classs in order for
+ # it to be the action hook.
+ def action_hook
+ action && owner_class_ancestor_has_method?(action) ? action : nil
+ end
+
+ # Determines whether any of the ancestors for this machine's owner class
+ # has the given method defined, even if it's private.
+ def owner_class_ancestor_has_method?(method)
+ owner_class.ancestors.any? do |ancestor|
+ ancestor != owner_class && (ancestor.method_defined?(method) || ancestor.private_method_defined?(method))
+ end
+ end
+
# 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_class_method("human_#{attribute(:name)}") do |machine, klass, state|
+ define_helper(:class, "human_#{attribute(:name)}") do |machine, klass, _super, state|
machine.states.fetch(state).human_name(klass)
end
# Gets the humanized version of an event
- define_class_method("human_#{attribute(:event_name)}") do |machine, klass, event|
+ define_helper(:class, "human_#{attribute(:event_name)}") do |machine, klass, _super, event|
machine.events.fetch(event).human_name(klass)
end
# Gets the state name for the current value
- define_instance_method(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_instance_method("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
@@ -1570,10 +1707,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_class_method(method) do |machine, klass, *states|
+ define_helper(:class, method) do |machine, klass, _super, *states|
values = states.flatten.map {|state| machine.states.fetch(state).value}
scope.call(klass, values)
end
end
end