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)