【问题标题】:How to create a class-less DSL in Ruby?如何在 Ruby 中创建无类 DSL?
【发布时间】:2012-01-03 01:33:02
【问题描述】:

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

例如,我想要一个文件,其中我的所有 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 类中进行评估。但我不知道该怎么做。

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

【问题讨论】:

    标签: ruby


    【解决方案1】:

    给你一个关于如何做你要求做的事情的简单答案有点挑战性。我建议您看一下Eloquent Ruby 这本书,因为其中有几章涉及 DSL,这可能对您很有价值。您确实要求提供有关这些其他库如何做的一些信息,所以我可以简要地尝试给您一个概述。

    辛纳特拉

    如果您查看 sinatra 代码 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
    

    黄瓜

    Cucumber 使用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

    仅供参考,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         = ?/
    

    【讨论】:

    • 那里有很多内容需要我消化,因为其中一些内容有点超出我的想象,但它仍然很有用。谢谢!
    【解决方案2】:

    您可以使用模块来组织您的代码。您可以使用Module#include 方法将您的DSL 方法添加到Module 类。以下是 RSpec 的做法。最后两行可能是您正在寻找的。 +1 @meagar 关于保持 DSL 的简单!

    正如@UncleGene 指出的那样,RSpec 确实使用 DSL 方法污染了内核。我不知道如何解决这个问题。如果有另一个具有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)
    

    【讨论】:

    • 这里不扩展会污染内核吗?需要'rspec';将 Kernel.methods.grep /describe/ =>describe。而且我不确定污染模块是否更好(AFAIU OP 试图避免污染)
    • @UncleGene 你是对的。我正在编辑我的答案以添加这一点。
    【解决方案3】:

    只需定义一个名为 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 上定义。
    • 然后更改您的方法定义以存储它给出的块以及任何状态变量,以供以后执行。
    • 好的,我用大量代码示例更新了我的问题,希望能更好地解释我的情况。问题在于解析和评估文件以及调用第一次定义块时不可用的实例方法。
    • @Andrew 什么?你正在以完全错误的方式解决这个问题。不要解析或评估文件!它是一个 Ruby 文件,只是普通的 Ruby。您只需require 它,Ruby 就会解析并执行文件的内容。 Ruby DSL 是 Ruby,而不是您必须担心解析的伪语言。
    • 我不能简单地 require 它,因为 when_string_matches 方法没有全局定义,所以你得到未定义的方法错误。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-17
    • 1970-01-01
    • 2011-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多