Search code examples
ruby-on-railsrubyactiverecordruby-on-rails-plugins

Aliasing activerecord methods inside plugin


I'm trying to write a plugin that aliases some methods in ActiveRecord in the following fashion:

class Foo < ActiveRecord::Base
  include MyOwnPlugin
  acts_as_my_own_plugin :methods => [:bar]

  def bar
    puts 'do something'
  end
end

Inside the plugin:

module MyOwnPlugin
  def self.included(base)    
    base.class_eval do
      extend ClassMethods
    end
  end
  module ClassMethods
    def acts_as_my_own_plugin(options)
      options[:methods].each do |m|
        self.class_eval <<-END
          alias_method :origin_#{m}, :#{m}
        END
      end
    end
  end
end

This approach won't work because when #acts_as_my_own_plugin is run, Foo#bar is not defined yet because it hasn't been run.

putting acts_as_my_own_plugin :methods => [:bar] AFTER the bar function declaration will work. However this is not pretty.

I want to be able to have the acts_as_my_own_plugin placed on top of the class definition as most plugins do.

Is there an alternative approach to satisfy this condition?


Solution

  • Always remember: There is a callback for almost everything in Ruby.

    Try the following:

    module MyOwnPlugin
      def self.included(base)
        base.extend(ClassMethods)
      end
    
      module ClassMethods
        # gets called from within the models
        def acts_as_my_own_plugin(options)
          # store the list of methods in a class variable and symbolize them
          @@methods = []
          options[:methods].each { |method| @@methods << method.to_sym }
        end
    
        # callback method. gets called by ruby if a new method is added.
        def method_added(name_of_method)
          if @@methods.include?(name_of_method)
            # delete the current method from our @@methods array
            # in order to avoid infinite loops
            @@methods.delete(name_of_method)
            #puts "DEBUG: #{name_of_method.to_s} has been added!"
    
            # code from your original plugin
            self.class_eval <<-END
              alias_method :origin_#{name_of_method}, :#{name_of_method}
              def #{name_of_method}
                puts "Called #{name_of_method}"
                origin_#{name_of_method}
              end
            END
    
          end
        end
      end
    end
    
    # include the plugin module in ActiveRecord::Base
    # in order to make acts_as_my_own_plugin available in all models 
    ActiveRecord::Base.class_eval do
      include MyOwnPlugin
    end
    
    # just call acts_as_my_own_plugin and define your methods afterwards
    class Foo < ActiveRecord::Base
      acts_as_my_own_plugin :methods => [:bar]
    
      def bar
        puts 'do something'
      end
    end
    

    I hope this is useful. The crazy things you can do with Ruby are just soooo cool ;)

    If you want to allow methods to be defined before AND after the call of acts_as_my_own_plugin you need to change the code again to allow this. However, the hard part is done.

    Disclaimer: This has been tested with Ruby 1.8.7. May not work with Ruby 1.9.*.