lib/state_machine/machine.rb in state_machine-1.0.0 vs lib/state_machine/machine.rb in state_machine-1.0.1
- old
+ new
@@ -374,10 +374,15 @@
:invalid => 'is invalid',
:invalid_event => 'cannot transition when %s',
:invalid_transition => 'cannot transition via "%s"'
}
+ # Whether to ignore any conflicts that are detected for helper methods that
+ # get generated for a machine's owner class. Default is false.
+ class << self; attr_accessor :ignore_method_conflicts; end
+ @ignore_method_conflicts = false
+
# The class that the machine is defined in
attr_accessor :owner_class
# The name of the machine, used for scoping methods generated for the
# machine as a whole (not states or events)
@@ -581,10 +586,15 @@
# 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.
#
+ # If passing in a block, there are two side effects to be aware of
+ # 1. The method cannot be chained, meaning that the block cannot call +super+
+ # 2. If the method is already defined in an ancestor, then it will not get
+ # overridden and a warning will be output.
+ #
# Example:
#
# # Instance helper
# machine.define_helper(:instance, :state_name) do |machine, object|
# machine.states.match(object).name
@@ -609,19 +619,26 @@
# def state_machine_name
# "State"
# end
# end_eval
def define_helper(scope, method, *args, &block)
+ helper_module = @helper_modules.fetch(scope)
+
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)
+ if !self.class.ignore_method_conflicts && conflicting_ancestor = owner_class_ancestor_has_method?(scope, method)
+ ancestor_name = conflicting_ancestor.name && !conflicting_ancestor.name.empty? ? conflicting_ancestor.name : conflicting_ancestor.to_s
+ warn "#{scope == :class ? 'Class' : 'Instance'} method \"#{method}\" is already defined in #{ancestor_name}, use generic helper instead."
+ else
+ name = self.name
+ helper_module.class_eval do
+ define_method(method) do |*args|
+ block.call((scope == :instance ? self.class : self).state_machine(name), self, *args)
+ end
end
end
else
- @helper_modules.fetch(scope).class_eval(method, *args)
+ helper_module.class_eval(method, *args)
end
end
# Customizes the definition of one or more states in the machine.
#
@@ -1583,17 +1600,18 @@
# Adds reader/writer methods for accessing the state attribute
def define_state_accessor
attribute = self.attribute
- @helper_modules[:instance].class_eval { attr_accessor attribute }
+ @helper_modules[:instance].class_eval { attr_reader attribute } unless owner_class_ancestor_has_method?(:instance, attribute)
+ @helper_modules[:instance].class_eval { attr_writer attribute } unless owner_class_ancestor_has_method?(:instance, "#{attribute}=")
end
# 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}?")
+ call_super = !!owner_class_ancestor_has_method?(:instance, "#{name}?")
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
@@ -1684,19 +1702,40 @@
#
# 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
+ action && owner_class_ancestor_has_method?(:instance, 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))
+ # Determines whether there's already a helper method defined within the
+ # given scope. This is true only if one of the owner's ancestors defines
+ # the method and is further along in the ancestor chain than this
+ # machine's helper module.
+ def owner_class_ancestor_has_method?(scope, method)
+ superclasses = owner_class.ancestors[1..-1].select {|ancestor| ancestor.is_a?(Class)}
+
+ if scope == :class
+ # Use singleton classes
+ current = (class << owner_class; self; end)
+ superclass = superclasses.first
+ else
+ current = owner_class
+ superclass = owner_class.superclass
end
+
+ # Generate the list of modules that *only* occur in the owner class, but
+ # were included *prior* to the helper modules, in addition to the
+ # superclasses
+ ancestors = current.ancestors - superclass.ancestors + superclasses
+ ancestors = ancestors[ancestors.index(@helper_modules[scope]) + 1..-1].reverse
+
+ # Search for for the first ancestor that defined this method
+ ancestors.detect do |ancestor|
+ ancestor = (class << ancestor; self; end) if scope == :class && ancestor.is_a?(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
@@ -1735,15 +1774,21 @@
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, *states|
- values = states.flatten.map {|state| machine.states.fetch(state).value}
- scope.call(klass, values)
+ run_scope(scope, machine, klass, states)
end
end
end
end
+ end
+
+ # Generates the results for the given scope based on one or more states to
+ # filter by
+ def run_scope(scope, machine, klass, states)
+ values = states.flatten.map {|state| machine.states.fetch(state).value}
+ scope.call(klass, values)
end
# Pluralizes the given word using #pluralize (if available) or simply
# adding an "s" to the end of the word
def pluralize(word)