2012-01-03 44 views
5

我想弄清楚如何为我的Ruby项目创建一种“无类DSL”,类似于如何在Cucumber步骤定义文件中定义步骤定义或路径是在Sinatra应用程序中定义的。如何在Ruby中创建一个无类DSL?

例如,我想有一个地方我所有的DSL函数被调用的文件:

#sample.rb 

when_string_matches /hello (.+)/ do |name| 
    call_another_method(name) 
end 

我认为这是一个不好的做法,污染了一堆的是方法,全局(Kernel)命名空间特定于我的项目。所以方法when_string_matchescall_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课程中评估。但我不知道该怎么做。

我希望这是有道理的。任何帮助,经验或建议,将不胜感激。

回答

4

给你一个关于如何去做你所要求的事情的答案是有点具有挑战性的。我建议你看一下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   = ?/ 
+0

我有很多东西需要消化,因为其中有些东西有点过头,但它仍然有用。谢谢! – Andrew 2012-07-27 05:30:16

2

只是定义了一个名为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:具有有趣名称的方法通常接受块。

+1

...这是在'Kernel'定义。 – Reactormonk 2012-01-03 01:52:46

+0

然后改变你的方法定义来存储它给出的块以及任何状态变量,以便以后执行。 – meagar 2012-01-03 02:17:10

+0

好吧,我用一大堆代码样本更新了我的问题,希望能更好地解释我的情况。问题在于解析和评估文件,并调用实例方法,这些方法在第一次定义块时不可用。 – Andrew 2012-01-03 04:23:33

3

您可以使用模块来组织您的代码。您可以使用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) 
+0

这个很有帮助,谢谢! – Andrew 2012-07-25 17:34:50

+1

这里不扩展是否污染内核?需要'rspec';把Kernel.methods.grep/describe/=>描述。我不确定污染模块是否更好(AFAIU OP试图避免污染) – UncleGene 2012-07-25 19:56:25

+0

@UncleGene你是对的。我正在编辑我的答案以增加这一点。 – CubaLibre 2012-07-31 16:38:20