Sep 12, 2011

Ruby's instance_eval and Structured Configuration Files

Recently at work I was setting up a section of the program involving a user-configurable GUI element. Initially I thought about using something like XML to handle this, however if I went that route I'd have to manage parsing the XML through some library, traversing the XML tree, and create some sort of GUI element based on what type of node it was, what the attributes were, etc.

On top of that it was highly likely that the requirements for this GUI system would change and the level of sophistication behind the GUI system would increase - buttons would need to do more complicated tasks that could potentially be arbitrary. Having a hard-coded system in C# would not be feasible since tweaking logic would require a re-compile and an application restart.

I decided that since the system already had an IronRuby install built in, perhaps I could do this configuration file in Ruby as well. Given an XML configuration file that looks like this:
<group name="Buttons">
<line>
<button text="Cool Script">
<something_crazy_and_awesome />
</button>
</line>
</group>
I could translate this to Ruby that looks like this:

group "Buttons" do
line do
button "Cool Script" do
something_crazy_and_awesome
end
end
end


On top of that in order to use this configuration file in a program I don't actually need to parse the file, I can just execute it within the context of different objects. For the dialog box that allows the user to set up the configuration I can just execute the Ruby code within that dialog box object and it will automatically create the elements that allow the user to change the configuration. Then in the part of the application where the user actually uses the configuration, I can just execute the file again but with a different definition of group, line, etc. that construct the proper GUI elements.

This is not just applicable to GUI elements, you can use this for anything that uses some sort of structured configuration. Rake uses this to great effect with tasks. But how would you go about implementing this pattern?

Turns out it's really simple using instance_eval. Here's an example that constructs the GUI element (this is in
IronRuby so it uses .NET for GUI construction):
class GroupBuilder
def initialize parent, name, &block
@group_box = GroupBox.new(name)
parent.Controls.Add(@group_box)
instance_eval &block
end

def line &block
LineBuilder.new(@group_box, &block)
end
end

class LineBuilder
attr_accessor :panel

def initialize parent, &block
# in the GUI environment we use a panel for adding things
@panel = Panel.new
parent.Controls.Add @panel

instance_eval &block
end

def button text, &block
ButtonBuilder.new(@panel, text, &block)
end

# ... anything else that can be placed in a line ...
end

class ButtonBuilder
def initialize parent, text, &block
# create a button
btn = Button.new(text)
# bind the click event of the button to execute the
# Ruby code within the block
btn.Click { self.instance_eval &block }
parent.Controls.Add btn
end

def something_crazy_and_awesome
# do something crazy and awesome
end
end

class ScriptProcessor
def initialize control, script
@control = control
instance_eval File.read(script)
end

def group name, &block
GroupBuilder.new(@control, name, &block)
end
end
To do all this stuff, you can just create a ScriptProcessor object, pass in a .NET control and a script filename:

f = Form.new
ScriptProcessor.new(f, "my_config_file.rb")
f.Show


For each type of nested element you can create a class which define a method for the various types of sub-elements that you are allowed to have. Each of those methods will then handle the processing that needs to happen when the system sees an element of that type.

I think you could probably do this without classes and just use lambdas, but I think the code is clearer when you have objects since it is very explicit as to what each object is for.

Doing this with C# is a bit trickier since C# doesn't have instance_eval, but it turns out you can have a bit of a hack in order to get it to work quite well. I'll write up a quick post about this at a later date.

No comments: