Example of Ruby Meta Programming

I had started to write this for a Rake task, where I wanted the option of both logging to the console versus the file. Suppose for sake of this argument that you don’t want the same thing to go to both places. (If you did, then the answer for that is at the bottom). Suppose you had the following, and then realized that you wanted to add a method called error. But that’s just too much duplicated code!

def warn(message = nil, &block)
  if @console_mode
    if message.nil? && block_given?
        message = yield
    end
    puts "WARNING: #{message}"
  else
    Rails.logger.warn(message, &block)
  end
end

def info(message = nil, &block)
  if @console_mode
    if message.nil? && block_given?
      message = yield
    end
    puts message
  else
    Rails.logger.info(message, &block)
  end
end

Here’s the solution. I encourage you to try to develop this on your own.

There’s a few key tricks:

  1. Read up a little on what class_eval does in Ruby.
  2. Pass in the FILE and LINE + 1 so you get a stack trace.
  3. Understand how and when to escape the # when doing string interpolation. Take a look at the line puts "#{severity}: \#{message}". Do you see how the \#{message} needs to get quoted? Try with and without this this \.
  4. Consider that the class_eval is really taking a STRING, so you need to consider when you need to put an actual string in quotes. Especially see the call below to Rails.logger.send where the #{severity.downcase} is quoted.
 %w(INFO WARN ERROR).each do |severity|
    class_eval <<-EOT, __FILE__, __LINE__ + 1
      def #{severity.downcase}(message = nil, &block) # def info(message = nil, &block)
        if @console_mode
          if message.nil? && block_given?
              message = yield
          end
          puts "#{severity}: \#{message}"
        else
          Rails.logger.send("#{severity.downcase}", message, &block)
        end
      end
    EOT
  end

Incidentally, if you want to just change the logger to STDOUT for a rake task, you can call:

Rails.logger = Logger.new(STDOUT)

However, that ends up logging all your queries to STDOUT as well, which might swamp the messages you want to see.