You are here: Home Blog BreakingPoint Labs Blog Constantly Hacking Ruby Constants

Constantly Hacking Ruby Constants

Here at BreakingPoint, we write all of our application simulation code in Ruby. Lately, I've been working on adding a slew of new behaviors to our IMAPv4 implementation so users can fine-tune their IMAP Application Simulator and Client Simulator flows. At first, this seemed like it would mean a whole lot of typing to wire up twelve new actions.

Instead of copying and pasting all over the place (and dreading the possibility of fixing the same bug in fifteen zillion places), I needed to come up with a code reuse technique that takes advantage of the existing codebase written using standardized naming conventions. Since I'm swimming in these standard names, I figured there must be a way to use Ruby's dynamic typing and extensible classes to make this easier on myself, both now and in the future.

The first trick is to programmatically figure out which application profile class to use when I'm in a particular protocol. For example, if we're in a function in the "Imap" object, I need to get protocol configuration from the "ImapProfile" singleton object. This is pretty easy with Ruby's introspection and the nifty Kernel.const_get() function.

So, let's say we have a (simplified) ImapProfile class:

class ImapProfile
 def self.config
	{:username => "todb",
	 :password => "Shadowfax" # Unguessable! 
	}
 end
end

In the Imap subclass of Application Manager, I'll want to get a hold of those configuration parameters. I can do so with something like this:

module AppManager
 class Imap

  # Get my class name, strip off the superclass
  def my_protocol
  self.class.name.to_s.split("::").last
  end

  # get_profile_params() takes the string from 
my_profile_object(), # gets the associated constant, and invokes
the config() method. def get_profile_params Kernel.const_get(my_protocol + "Profile").send :config end end end

Now we can call the profile object's "config" method by deriving the class name from our own class's name:

irb(main):001:0> @app = AppManager::Imap.new
=> #
irb(main):002:0> @app.get_profile_params
=> {:username="todb", :password=>"Shadowfax"}

That's pretty neat and all, but the real trick is to figure out how to do the same thing with a method name, since (as you'll see) they bear a resembelence to individual action classes. After a little bit of research, it turns out we can perform something similar with the Kernel.caller() function, and again use some string manipulation to get what we want:

def caller_action_to_constant
	caller[1] =~ /`([^']+?)'/
	$1 =~ /^do_(client|server)_(.*)/
	$2.split("_").map {|s| s.capitalize}.join
end

This function takes the second element of the execution stack, extracts the calling method's name (the first regex), extracts the part we care about (the second regex), then splits on the underscores in order to CamelCase the result. In the end, the string:

"do_client_send_user_name"

becomes 

SendUserName

Why not the first element of the call stack array? Well, I'm wrapping this up in an intermediary function, called the action_executor, which takes this string and performs another const_get to actually use it for something:

def action_executor(args={})
 Kernel.const_get
(my_protocol + caller_action_to_constant + "Cmd").send :data end

So, from now on, the do_ actions can call the action_executor in order to track down the right classes to get the data from:

def do_client_send_user_name(args={})
	action_executor(args)
end

Pretty neat, if you ask me. A complete code listing should be available here, at Pastie.

In the end, this strikes me as an implementation of the OO Delegation design pattern. However, it includes some extra smarts about where the delegatee is, all based on a common naming convention for classes and methods. While the example code is sparse, in reality, the application actions I'm replacing in IMAP were each around 15 lines, and this technique compresses them down to one. I also get the added bonus of centralizing a common function to one spot, to ease future tweaks to the way application protocols work, or, the laughably remote possibility that there's ever a bug discovered there.

Posted by Tod Beardsley (2009/06/23)
0 comments | Tags: