|
|
|
Sponsored Link •
|
|
Advertisement
|
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.
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
[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
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.
|
Sponsored Links
|