12
返回列表 发新帖
楼主: Sky-Tiger

Creating DSLs with Ruby

[复制链接]
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
11#
 楼主| 发表于 2009-3-18 23:06 | 只看该作者
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.

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
12#
 楼主| 发表于 2009-3-18 23:06 | 只看该作者
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.

使用道具 举报

回复
论坛徽章:
350
2006年度最佳版主
日期:2007-01-24 12:56:49NBA大富翁
日期:2008-04-21 22:57:29地主之星
日期:2008-11-17 19:37:352008年度最佳版主
日期:2009-03-26 09:33:53股神
日期:2009-04-01 10:05:56NBA季后赛大富翁
日期:2009-06-16 11:48:01NBA季后赛大富翁
日期:2009-06-16 11:48:01ITPUB年度最佳版主
日期:2011-04-08 18:37:09ITPUB年度最佳版主
日期:2011-12-28 15:24:18ITPUB年度最佳技术原创精华奖
日期:2012-03-13 17:12:05
13#
 楼主| 发表于 2009-3-18 23:06 | 只看该作者
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

使用道具 举报

回复
论坛徽章:
43
ITPUB元老
日期:2007-01-14 09:32:112011新春纪念徽章
日期:2011-01-25 15:42:332011新春纪念徽章
日期:2011-01-25 15:42:56管理团队成员
日期:2011-05-07 01:45:08开发板块每日发贴之星
日期:2011-08-29 01:01:012012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:18
14#
发表于 2009-3-22 16:37 | 只看该作者

使用道具 举报

回复
论坛徽章:
43
ITPUB元老
日期:2007-01-14 09:32:112011新春纪念徽章
日期:2011-01-25 15:42:332011新春纪念徽章
日期:2011-01-25 15:42:56管理团队成员
日期:2011-05-07 01:45:08开发板块每日发贴之星
日期:2011-08-29 01:01:012012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:182012新春纪念徽章
日期:2012-02-13 15:11:18
15#
发表于 2009-3-22 16:51 | 只看该作者

使用道具 举报

回复
论坛徽章:
0
16#
发表于 2010-7-17 23:54 | 只看该作者

有没有专门讲ruby DSL的书籍呢

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

TOP技术积分榜 社区积分榜 徽章 团队 统计 知识索引树 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 未成年人举报专区 
京ICP备16024965号-8  北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表