Thursday, July 01, 2010

Using module_eval to Define Instance Methods in a Ruby Gem to Enable Per-Model Configuration

In the last post, I mentioned that I wrote a small gem to post changes to ActiveRecord models to Twitter. In that version, the configuration was handled by a YAML file. I wanted to evolve the gem so that developers could switch Twitter accounts for each model.

Basically, I wanted to able to the use the following:

class Place < ActiveRecord::Base
  alastrina :twitter => { :username => 'alastrina_gem', :password => 'QQQQQQ' }
end

After a bit of tinkering and searching, I decided to use the following code in my gem.

ALASTRINA_CONFIGURATION_FILE = 'config/alastrina.yml'

module Alastrina
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  module ClassMethods
    def alastrina hash
      module_eval do
        def configuration
          throw "Missing #{ALASTRINA_CONFIGURATION_FILE}" unless File.exists? ALASTRINA_CONFIGURATION_FILE 
          @config ||= YAML::load(File.read(ALASTRINA_CONFIGURATION_FILE))
        end
        if hash[:twitter]
          def send_to_twitter?
            true
          end
          eval"def twitter_username\n\"#{hash[:twitter][:username]}\"\nend\n"
          eval "def twitter_password\n\"#{hash[:twitter][:password]}\"\nend\n"
        else
          def send_to_twitter?
            @twitter_flag ||= !configuration['twitter'].blank?
          end
          def twitter_username
            @twitter_username ||= configuration['twitter']['username'] if send_to_twitter?
          end
          def twitter_password
            @twitter_password ||= configuration['twitter']['password'] if send_to_twitter?
          end
        end
      end
    end
  end

  def after_save
    if send_to_twitter?
      require 'twitter' 
      throw "Missing Twitter userid" if twitter_username.blank? 
      throw "Missing Twitter password" if twitter_password.blank?
      send_via_twitter
    end
  end

private

  def send_via_twitter
    if changes.size > 0
      httpauth = Twitter::HTTPAuth.new(twitter_username, twitter_password)
      client = Twitter::Base.new(httpauth)
      begin
        client.update(changes.to_yaml)
      rescue
        RAILS_DEFAULT_LOGGER.error "alastrina.send_via_twitter; Unable to send change. Message[#{$!}] Change[#{changes.to_yaml}]"
      end
    end
  end
  
end

ActiveRecord::Base.class_eval { include Alastrina }

The key insight to this code is how the hash passed on the alastrina of the Model is passed down into the instance. The code is fairly straightforward once you see what the eval is doing.

Post a Comment