I recently wrote a small application to parse Apache log files. The output is this context is not really important, but rather the API of the application that I wrote.
Two things were required to parse a log file:
- the log file format
- the location of the log file
I wanted the initialize the object with block notation. So starting off, this is what we wrote, which is faily typical in terms of most internal DSLs:
LogParser::Application.new do |p| p.format = "%V %h %l %u %t \"%r\" %s" p.log_file = '/apache_path/access_log' end
This would then result in a constructor that looks something like this…
module LogParser class Application def initialize(&block) block.call(self) if block_given? end end end
That’s perfectly acceptable and it will work just fine. But it’s not a winner in terms of elegance. Enter instance_eval.
instance_eval will execute a block, but it will change the value of
self as it executes the block.
So, to get an API that looks like this…
LogParser::Application.new do format "%V %h %l %u %t \"%r\" %s" log_file '/apache_path/access_log' end
…we must change the constructor to look like this…
module LogParser class Application def initialize(&block) instance_eval(block) if block_given? end end end
The example above has a few caveats though. In the first example, we are assigning values to
log_file using setter methods:
log_file=. These can be created manually or by using
In the second example, we are calling methods
log_file. It’s a subtle difference but something that should be considered when developing your own API.
Here’s the final code for the
Application class used above…
module LogParser class Application def initialize(&block) instance_eval(&block) if block_given? end def format regex_pattern @format = regex_pattern end def log_file filepath @log_file = filepath end # rest of code omitted end end
Hope this helps.