如何在Ruby中创建无类DSL?

时间:2012-01-03 01:33:03

标签: ruby

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

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

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

3 个答案:

答案 0 :(得分:4)

给你一个关于如何做你要做的事情的答案是有点挑战性的。我建议你看一下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

<强>黄瓜

黄瓜使用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         = ?/

答案 1 :(得分:3)

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

同样@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)

答案 2 :(得分: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都是:带有趣名称的方法,通常接受块。