The Artima Developer Community
Sponsored Link

Ruby Code & Style
Creating DSLs with Ruby
by Jim Freeze
March 16, 2006

<<  Page 4 of 4

Advertisement

Getting More Sophisticated

We are now ready to look at more complex DSL features. Instead of a DSL for manipulating data, let’s look at one that performs a more concrete action. Imagine that we are tired of manually creating a common set of directories and files whenever we start a new project. It would be nice if we had Ruby do this for us. It would even be nicer if we had a small DSL such that we could modify the project directory structure without editing the low-level code.

We begin this project by defining a DSL that makes sense for this problem. The file below is our version 0.0.1 of just such a DSL.

% cat project_template.dsl 
create_project do
  dir "bin" do
    create_from_template :exe, name
  end

  dir "lib" do
    create_rb_file name
    dir name do
      create_rb_file name
    end
  end

  dir "test" 

  touch :CHANGELOG, :README, :TODO
end

In this file, we create a project and add three directories and three files. Inside the “bin” directory we create an executable file with the same name as the project using the :exe template. In the ‘lib’ directory, we create a .rb file, and a directory, both named after the project. Inside that inner directory, another .rb file with the same name as the project. Next, back at the top level, the ‘test’ directory is created, and, finally, three empty files are created.

The methods needed for this DSL are: create_project, dir, create_from_template, create_rb_file and touch. Let’s look at these methods one by one.

The create_project method is our top level wrapper. This method provides scope by letting us put all the DSL code inside a block. (Complete listings are at the end of the article.)

  def create_project()
    yield
  end

The dir method is the workhorse. This method not only creates the directory, it also maintains the current working directory in the @cwd instance variable. Here, the use of ensure allows us to trivially maintain the proper state of @cwd .


  def dir(dir_name)
    old_cwd = @cwd
    @cwd    = File.join(@cwd, dir_name)

    FileUtils.mkdir_p(@cwd)
    yield self if block_given?
   ensure
    @cwd = old_cwd
  end

The touch and create_rb_file methods are the same except that the latter adds ”.rb” to the filename. These methods may be given one or more filenames where the names can be either strings or symbols.

  def touch(*file_names)
    file_names.flatten.each { |file| 
      FileUtils.touch(File.join(@cwd, "#{file}")) 
    }
  end

Finally, the create_from_template method is just a quick dash to illustrate how one may put some actual functionality into a DSL . (See the source listings for the complete code.)

To run and test the code, we build a small test application:

 % cat create_project.rb 
require 'project_builder'

project_name = ARGV.shift
proj = ProjectBuilder.load(project_name)
puts "== DIR TREE OF PROJECT '#{project_name}' =="
puts `find #{project_name}`

And the results are:

 % ruby create_project.rb fred
== DIR TREE OF PROJECT 'fred' ==
fred
fred/bin
fred/bin/fred
fred/CHANGELOG
fred/lib
fred/lib/fred
fred/lib/fred/fred.rb
fred/lib/fred.rb
fred/README
fred/test
fred/TODO
% cat fred/bin/fred 
#!/usr/bin/env ruby

require 'rubygems'
require 'commandline
require 'fred'

class FredApp < CommandLine::Application
  def initialize
  end

  def main
  end
end#class FredApp

Wow! It worked! And with not much effort.

Summary

I work on many projects that require a rather detailed control flow description. For every project, this used to make me pause and consider how to get all this detailed configuration data into the application. Now, Ruby as a DSL is near the top of the list of possibilities, and usually solves the problem quickly and efficiently.

When I was doing Ruby training, I would take the class through a problem solving technique where we would describe the problem in plain English, then in pseudo code, and then in Ruby. But, in some cases, the pseudo code would be valid Ruby code. I think that the high readability quotient of Ruby makes it an ideal language for use as a DSL. And as Ruby becomes known by more people, DSLs written in Ruby will be a favorable way of communicating with an application.

Code listing for project ProjectBuilder DSL:

% cat project_builder.rb 
require 'fileutils'

class ProjectBuilder
  PROJECT_TEMPLATE_DSL = "project_template.dsl"

  attr_reader :name

  TEMPLATES = {
      :exe =>
<<-EOT
#!/usr/bin/env ruby

require 'rubygems'
require 'commandline
require '%name%'

class %name.capitalize%App < CommandLine::Application
  def initialize
  end

  def main
  end
end#class %name.capitalize%App
EOT
    }

  def initialize(name)
    @name          = name
    @top_level_dir = Dir.pwd
    @project_dir   = File.join(@top_level_dir, @name)
    FileUtils.mkdir_p(@project_dir)
    @cwd = @project_dir
  end

  def create_project
    yield
  end

  def self.load(project_name, dsl=PROJECT_TEMPLATE_DSL)
    proj = new(project_name)
    proj = proj.instance_eval(File.read(dsl), dsl)
    proj
  end

  def dir(dir_name)
    old_cwd = @cwd
    @cwd    = File.join(@cwd, dir_name)
    FileUtils.mkdir_p(@cwd)
    yield self if block_given?
  ensure
    @cwd = old_cwd
  end

  def touch(*file_names)
    file_names.flatten.each { |file| 
      FileUtils.touch(File.join(@cwd, "#{file}")) 
    }
  end

  def create_rb_file(file_names)
    file_names.each { |file| touch(file + ".rb") }
  end

  def create_from_template(template_id, filename)
    File.open(File.join(@cwd, filename), "w+") { |f|
      str = TEMPLATES[template_id]
      str.gsub!(/%[^%]+%/) { |m| instance_eval m[1..-2] }
      f.puts str
    }
  end
end#class ProjectBuilder

# Execute as:
# ruby create-project.rb project_name

Resources

[0] BlankSlate is a Ruby class designed to create method-free objects.
http://onestepback.org/index.cgi/Tech/Ruby/BlankSlate.rdoc

[1] Jim Weirich is the creator of BlankSlate, as well as other notable Ruby tools and libraries.
http://onestepback.org

About the author

Jim Freeze has been a Ruby enthusiast since he learned of the language in early 2001.

An Electrical Engineer by trade working in the Semiconductor Industry, Jim has focused on extending Ruby into the EDA space and building libraries to make the language more palatable for the corporate community. Lately Jim has been working on integrating Ruby and Rails with Asterisk.

Jim is the author of the CommandLine and Stax gems.

<<  Page 4 of 4


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us