如何在Ruby中创建一个无类DSL?
我想弄清楚如何为我的Ruby项目创建一种“无类DSL”,类似于如何在Cucumber步骤定义文件中定义步骤定义或路径是在Sinatra应用程序中定义的。如何在Ruby中创建一个无类DSL?
例如,我想有一个地方我所有的DSL函数被调用的文件:
#sample.rb
when_string_matches /hello (.+)/ do |name|
call_another_method(name)
end
我认为这是一个不好的做法,污染了一堆的是方法,全局(Kernel
)命名空间特定于我的项目。所以方法when_string_matches
和call_another_method
将在我的库中定义,并且sample.rb
文件将在我的DSL方法的上下文中进行评估。
更新:下面是这些DSL方法目前如何定义一个例子:
方法是在被子类一类中定义的DSL(我想找到一种方法,再利用之间的这些方法简单的DSL和类实例):
module MyMod
class Action
def call_another_method(value)
puts value
end
def handle(text)
# a subclass would be expected to define
# this method (as an alternative to the
# simple DSL approach)
end
end
end
在某些时候
然后,我的程序的初始化过程中,我想解析sample.rb
文件和存储这些动作以后执行的:
module MyMod
class Parser
# parse the file, saving the blocks and regular expressions to call later
def parse_it
file_contents = File.read('sample.rb')
instance_eval file_contents
end
# doesnt seem like this belongs here, but it won't work if it's not
def self.when_string_matches(regex, &block)
MyMod.blocks_for_executing_later << { regex: regex, block: block }
end
end
end
# Later...
module MyMod
class Runner
def run
string = 'hello Andrew'
MyMod.blocks_for_executing_later.each do |action|
if string =~ action[:regex]
args = action[:regex].match(string).captures
action[:block].call(args)
end
end
end
end
end
到目前为止我所拥有的问题(以及我尝试过的以上没有提到的各种问题)是在文件中定义了块时,实例方法不可用(我知道它现在处于不同的阶级)。但是我想要做的更像是在这种情况下创建一个实例并进行评估,而不是在Parser
课程中评估。但我不知道该怎么做。
我希望这是有道理的。任何帮助,经验或建议,将不胜感激。
给你一个关于如何去做你所要求的事情的答案是有点具有挑战性的。我建议你看一下Eloquent Ruby这本书,因为那里有几个章节讨论DSL,这对你可能是有价值的。你确实需要了解这些其他库如何执行它们的一些信息,所以我可以简单地尝试给你一个概述。
西纳特拉
如果你看看西纳特拉代码sinatra/main.rb你会看到,它延伸到Sinatra::Delegator
代码的主线。 Delegator是非常有趣..
它设置了所有它想委托
delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout,
:before, :after, :error, :not_found, :configure, :set, :mime_type,
:enable, :disable, :use, :development?, :test?, :production?,
:helpers, :settings
的方法和树立类委托给一个类变量,以便它可以根据需要被重写..
self.target = Application
和委托方法很好地允许您使用respond_to?
来覆盖这些方法,或者调用该target
类如果没有定义的方法..
def self.delegate(*methods)
methods.each do |method_name|
define_method(method_name) do |*args, &block|
return super(*args, &block) if respond_to? method_name
Delegator.target.send(method_name, *args, &block)
end
private method_name
end
end
黄瓜
黄瓜使用treetop language library。这是一个强大的(也是复杂的,即不平凡的学习)工具,用于构建DSL。如果你预计你的DSL增长很多,那么你可能想要投资学习使用这个“大枪”。这里描述太多了。
HAML
你并没有问HAML,但它只是为“手动”实施的另一DSL,即它不使用树梢。基本上(在这里过于简单化),它会读取HAML文件和处理每行with a case statement ...
def process_line(text, index)
@index = index + 1
case text[0]
when DIV_CLASS; push div(text)
when DIV_ID
return push plain(text) if text[1] == ?{
push div(text)
when ELEMENT; push tag(text)
when COMMENT; push comment(text[1..-1].strip)
...
我认为它用于直接调用出来的方法,但现在它的预处理文件,并推命令到堆栈各种各样的。例如the plain
method
FYI的definition of the constants看起来是这样的..
# Designates an XHTML/XML element.
ELEMENT = ?%
# Designates a `<div>` element with the given class.
DIV_CLASS = ?.
# Designates a `<div>` element with the given id.
DIV_ID = ?#
# Designates an XHTML/XML comment.
COMMENT = ?/
只是定义了一个名为when_string_matches
方法,这需要一个正则表达式作为参数,测试它反对任何“字符串”你在说什么,并有条件的产量,通过任何name
是其块:
def when_string_matches(regex)
# do whatever is required to produce `my_string` and `name`
yield(name) if my_string =~ regex
end
这基本上都是Ruby DSL:具有有趣名称的方法通常接受块。
...这是在'Kernel'定义。 – Reactormonk 2012-01-03 01:52:46
然后改变你的方法定义来存储它给出的块以及任何状态变量,以便以后执行。 – meagar 2012-01-03 02:17:10
好吧,我用一大堆代码样本更新了我的问题,希望能更好地解释我的情况。问题在于解析和评估文件,并调用实例方法,这些方法在第一次定义块时不可用。 – Andrew 2012-01-03 04:23:33
您可以使用模块来组织您的代码。您可以使用Module#include
方法将您的DSL方法添加到Module
类。 RSpec如何做到这一点。最后两行是你可能正在寻找的。 @meagar关于保持DSL的简单!
同样@UncleGene指出,RSpec会使用DSL方法污染Kernel。我不知道如何解决这个问题。如果存在另一种带有describe
方法的DSL,则很难确定使用哪一个describe
。
module RSpec
module Core
# Adds the `describe` method to the top-level namespace.
module DSL
# Generates a subclass of {ExampleGroup}
#
# ## Examples:
#
# describe "something" do
# it "does something" do
# # example code goes here
# end
# end
#
# @see ExampleGroup
# @see ExampleGroup.describe
def describe(*args, &example_group_block)
RSpec::Core::ExampleGroup.describe(*args, &example_group_block).register
end
end
end
end
extend RSpec::Core::DSL
Module.send(:include, RSpec::Core::DSL)
我有很多东西需要消化,因为其中有些东西有点过头,但它仍然有用。谢谢! – Andrew 2012-07-27 05:30:16