我正在试图弄清楚如何为我的Ruby项目创建一种“无类DSL”,类似于在Cucumber步骤定义文件中定义步骤定义或在Sinatra应用程序中定义路由。 / p>
例如,我想要一个文件,其中所有的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
类中进行评估。但我不知道该怎么做。
我希望这是有道理的。任何帮助,经验或建议将不胜感激。
答案 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都是:带有趣名称的方法,通常接受块。