Ruby 外部 DSL 与 Gem 打包全解析
1. Ruby 外部 DSL 的魅力与挑战
1.1 Treetop 助力语法解析
Treetop 能让我们清晰地描述语法,避免被解析的细枝末节所困扰。使用 Treetop 时,先将语言描述存储在以
.tt
结尾的文件中,例如
ez_ripper_statement.tt
,然后通过
treetop
编译器运行:
tt ez_ripper_statement.tt
运行此命令后,Treetop 会创建一个名为
ez_ripper_statement.rb
的文件,并在其中生成一个
EzRipperStatementParser
类,该类可用于解析
EzRipper
语句。之后就可以像使用普通 Ruby 代码一样使用它:
require 'treetop'
require 'ez_ripper_statement'
statement = "replace '/document/author' 'Russ Olsen'"
parser = EzRipperStatementParser.new
parse_tree = parser.parse( statement )
运行这段代码,语句会被解析成树形结构,方便我们进行后续处理。
1.2 外部 DSL 与内部 DSL 的对比
外部 DSL 和内部 DSL 的优缺点是相互对立的。内部 DSL 能免费使用 Ruby 的所有功能,如注释、循环、
if
语句和变量等;而外部 DSL 则需要为每个功能付出努力,至少要进行解析。以 HAML 为例,它是一种简洁的 HTML 模板语言,代码如下:
%html
%body
#main
Today is
= Time.new
运行后会生成如下 HTML 代码:
<html>
<body>
<div id='main'>
Today is
2010-09-19 15:10:01 -0400
</div>
</body>
</html>
HAML 的解析依赖正则表达式和一些巧妙的手工代码。以下是 HAML 的
parse_line
方法的部分代码:
def process_line(text, index)
@index = index + 1
case text[0]
when DIV_CLASS; render_div(text)
when DIV_ID
return push_plain(text) if text[1] == ?{
render_div(text)
when ELEMENT; render_tag(text)
when COMMENT; render_comment(text[1..-1].strip)
when SANITIZE
return push_plain(text[3..-1].strip,
:escape_html => true) if text[1..2] == "=="
return push_script(text[2..-1].strip,
:escape_html => true) if text[1] == SCRIPT
return push_flat_script(text[2..-1].strip,
:escape_html => true) if text[1] == FLAT_SCRIPT
return push_plain(text[1..-1].strip,
:escape_html => true) if text[1] == ?\s
push_plain text
# and on and on and on...
end
end
可以看出,编写 HAML 解析器需要付出很大的努力,但结果是值得的。在开始构建外部 DSL 之前,需要问自己:这种语言是否值得花费这些精力?
1.3 内外 DSL 界限模糊
HAML 表明内部 DSL 和外部 DSL 的界限并不清晰。在 HAML 示例中,我们可以直接使用普通的 Ruby 代码,如
Time.new
。在
EzRipper
中也可以添加新命令来执行任意 Ruby 代码,例如:
execute '/document/author' 'puts "the author is #{el.text}"'
只需要在
EzRipper
的
parse_statement
方法中添加几行代码实现:
when /execute\s+'(.*?)'\s+'(.*?)'$/
@ripper.on_path( $1 ) { |el| eval( $2 ) }
这种
execute
语句为外部 DSL 打开了通往内部 Ruby 代码的大门。
1.4 现实中的外部 DSL 案例
1.4.1 ERB 模板引擎
在 HAML 出现之前,大多数 Rails 应用使用 ERB 进行模板处理。例如:
Today is <%= Time.new %>
运行后会得到类似如下的结果:
Today is 2009-10-18 00:25:35 -0400
ERB 使用
String
的
split
方法和正则表达式来分割输入。它定义的正则表达式如下:
SplitRegexp = /(<%%)|(%%>)|(<%=)|(<%#)|(<%)|(%>)|(\n)/
1.4.2 Cucumber 测试工具
Cucumber 结合了外部 DSL 和内部 DSL。它允许我们用结构化的自然语言编写验收测试,例如:
Feature: Count words in a document
In order to be sure that documents hold on to their content
Start with an empty document and add some text to it
and check to see that the text is actually there
Scenario:
Given that we have a document with 1000 words
When I count the words
Then the count should be 1000
我们可以将这种自然语言描述转化为可执行的测试。为此,需要创建“步骤描述”,使用内部 DSL 编写:
Given /^that we have a document with (\d+) words$/ do |n|
@document = Document.new( 'russ', 'a test' )
@document.content = 'crypozoology ' * n.to_i
end
When /^I count the words$/ do
@count = @document.word_count
end
Then /^the count should be (\d+)$/ do |n|
@count.should == n.to_i
end
Cucumber 会使用步骤描述中的正则表达式将其与功能描述结合,最终得到一个既像自然语言规范又可执行的测试。
1.4.3 Treetop 自身
Treetop 是一个具有复杂解析器的外部 DSL 示例。它的解析器使用 Treetop 自身编写,以下是 Treetop 规则的规则:
rule parsing_rule
'rule' space nonterminal space ('do' space)?
parsing_expression space 'end' <ParsingRule>
end
1.5 外部 DSL 的总结
外部 DSL 形式多样,从简单的字符串处理到使用正则表达式,再到借助解析器生成工具(如 Treetop)。无论简单还是复杂,外部 DSL 能摆脱 Ruby 语法的限制,但也失去了免费的 Ruby 解析器。在构建 DSL 时,需要在内部 DSL 的便捷低成本和外部 DSL 的高成本与高自由度之间做出选择。
1.6 外部 DSL 相关要点总结表格
| 要点 | 详细内容 |
|---|---|
| Treetop 使用流程 |
1. 编写
.tt
文件;2. 运行
tt
命令;3. 使用生成的解析器类
|
| 外部 DSL 与内部 DSL 对比 | 内部 DSL 免费使用 Ruby 功能,外部 DSL 需自行解析 |
| HAML 特点 | 简洁的 HTML 模板语言,解析依赖正则表达式和手工代码 |
| 内外 DSL 融合 | 可在外部 DSL 中执行内部 Ruby 代码 |
| 现实案例 | ERB、Cucumber、Treetop |
1.7 外部 DSL 处理流程 mermaid 流程图
graph TD;
A[编写外部 DSL 代码] --> B[选择解析方式];
B --> C{简单处理};
C -- 是 --> D[使用字符串方法];
C -- 否 --> E{正则表达式};
E -- 是 --> F[使用正则解析];
E -- 否 --> G{使用工具};
G -- 是 --> H[Treetop 等工具];
G -- 否 --> I[手工编写解析器];
D --> J[解析完成];
F --> J;
H --> J;
I --> J;
2. Ruby 宝石(Gems)的使用与创建
2.1 宝石的消费
如果你使用 Ruby 编程有一段时间了,很可能已经是宝石的消费者。例如,
ruby - mp3info
宝石可以让你读写 MP3 文件中的信息标签。使用步骤如下:
1. 安装宝石:
gem install ruby - mp3info
在 Unix、Linux 或 OS X 系统上,可能需要使用
sudo
命令:
sudo gem install ruby - mp3info
- 使用宝石:
require 'mp3info'
Mp3Info.open( 'money.mp3' ) do |info|
puts "title: #{info.tag.title}"
puts "artist: #{info.tag.artist}"
puts "album: #{info.tag.album}"
end
2.2 宝石的版本管理
宝石系统的一个重要特性是完整的版本管理支持。每个宝石都有版本号,大多数宝石存在多个版本。可以使用
gem list
命令查看某个宝石的可用版本:
gem list -a --remote ruby - mp3info
安装宝石时,默认会安装最新版本。如果需要指定版本,可以使用
--version
选项:
gem install --version 0.4 ruby - mp3info
系统可以同时安装同一个宝石的多个版本。在代码中,如果需要指定特定版本的宝石,可以使用
gem
方法:
gem 'ruby - mp3info', '=0.5'
require 'mp3info'
gem
方法的版本号参数还支持更通用的表达式,如
'>0.4'
或
'<=0.5'
。
2.3 宝石的技术原理
RubyGems 的技术原理相对简单。宝石开发者将代码打包成一个标准化的存档文件,其中不仅包含代码,还包含大量有用的元数据,如宝石版本号和依赖的其他宝石。打包好的宝石文件上传到知名的仓库,
gem install
命令会从这里获取宝石。
宝石文件实际上是一个 TAR 文件,打开后会发现里面包含两个压缩的 TAR 文件。第一个内部 TAR 文件包含宝石的实际内容,如
README
文件、Ruby 源文件和可执行文件;第二个内部 TAR 文件包含元数据。
安装宝石时,Ruby 会将其解压到一个指定的目录,之后在执行
require
或
load
时会搜索该目录。
2.4 宝石的创建
创建宝石主要需要完成两件关键的事情:
2.4.1 组织项目目录
项目目录结构需要遵循标准的宝石布局。以
document
宝石为例,目录结构如下:
document/
├── Rakefile
├── README
├── document.gemspec
├── lib/
│ └── document.rb
└── spec/
└── document_spec.rb
- 顶级目录名与宝石名相同。
-
README文件用于提供宝石的说明信息。 -
lib目录用于存放 Ruby 代码,通常主 Ruby 文件的名称与宝石名相同,方便用户使用require 'document'引入宝石。 -
spec目录用于存放单元测试文件。
如果宝石比较复杂,包含多个源文件,应在
lib
目录下创建一个与宝石名相同的子目录,并将代码放在该子目录中。例如
text
宝石的目录结构:
text/
├── Rakefile
├── README.rdoc
├── text.rb
├── lib/
│ └── text/
│ ├── metaphone.rb
│ ├── soundex.rb
│ └── ...
└── test/
├── test_metaphone.rb
├── test_soundex.rb
└── ...
lib
目录下的
text.rb
文件通常会引入子目录中的文件:
require 'text/util'
require 'text/double_metaphone'
require 'text/levenshtein'
require 'text/metaphone'
require 'text/porter_stemming'
require 'text/soundex'
require 'text/version'
2.4.2 创建宝石规格文件(gemspec)
宝石规格文件是一个包含 Ruby 代码的文件,用于创建
Gem::Specification
类的实例。以下是
document
宝石的
gemspec
文件示例:
Gem::Specification.new do |s|
s.name = "document"
s.version = "1.0.1"
s.authors = ["Russ Olsen"]
s.date = %q{2010 - 01 - 01}
s.description = 'Document - Simple document class'
s.summary = s.description
s.email = 'russ@russolsen.com'
s.files = ['README', 'lib/document.rb','spec/document_spec.rb']
s.homepage = 'http://www.russolsen.com'
s.has_rdoc = true
s.rubyforge_project = 'simple_document'
end
如果宝石依赖其他宝石,可以在
gemspec
文件中添加依赖信息,例如:
s.add_dependency('text')
2.5 宝石创建步骤总结列表
- 组织项目目录,遵循标准宝石布局。
-
创建
gemspec文件,定义宝石的元数据和依赖信息。
2.6 宝石相关要点总结表格
| 要点 | 详细内容 |
|---|---|
| 宝石消费 |
1. 安装宝石;2. 使用
require
引入宝石
|
| 宝石版本管理 |
使用
gem list
查看版本,
gem install --version
指定版本安装,
gem
方法指定版本使用
|
| 宝石技术原理 | 打包成 TAR 文件,包含代码和元数据,上传到仓库 |
| 宝石创建 |
1. 组织项目目录;2. 创建
gemspec
文件
|
2.7 宝石创建流程 mermaid 流程图
graph TD;
A[确定宝石名称] --> B[创建项目目录];
B --> C[编写 Ruby 代码];
C --> D[编写单元测试];
D --> E[创建 gemspec 文件];
E --> F[定义元数据和依赖];
F --> G[打包宝石];
G --> H[上传到仓库];
综上所述,无论是 Ruby 外部 DSL 还是宝石(Gems),都为 Ruby 开发者提供了强大的工具和灵活的选择。在实际开发中,我们可以根据具体需求选择合适的技术方案,充分发挥 Ruby 的优势。
超级会员免费看
5369

被折叠的 条评论
为什么被折叠?



