我在 Ruby 中有一个 DSL,其工作原理如下:
desc 'list all todos'
command :list do |c|
c.desc 'show todos in long form'
c.switch :l
c.action do |global,option,args|
# some code that's not relevant to this question
end
end
desc 'make a new todo'
command :new do |c|
# etc.
end
一位开发人员同事建议我增强 DSL,使其不需要将
c
传递到 command
块,因此不需要所有的 c.
里面的方法;据推测,他暗示我可以使以下代码同样工作:
desc 'list all todos'
command :list do
desc 'show todos in long form'
switch :l
action do |global,option,args|
# some code that's not relevant to this question
end
end
desc 'make a new todo'
command :new do
# etc.
end
command
的代码看起来像
def command(*names)
command = make_command_object(..)
yield command
end
我尝试了几件事,但无法让它发挥作用;我不知道如何将
command
块内代码的上下文/绑定更改为与默认值不同。
关于这是否可能以及我如何做到这一点有什么想法吗?
粘贴此代码:
def evaluate(&block)
@self_before_instance_eval = eval "self", block.binding
instance_eval &block
end
def method_missing(method, *args, &block)
@self_before_instance_eval.send method, *args, &block
end
欲了解更多信息,请参阅这篇非常好的文章这里
也许
def command(*names, &blk)
command = make_command_object(..)
command.instance_eval(&blk)
end
可以在命令对象的上下文中评估块。
class CommandDSL
def self.call(&blk)
# Create a new CommandDSL instance, and instance_eval the block to it
instance = new
instance.instance_eval(&blk)
# Now return all of the set instance variables as a Hash
instance.instance_variables.inject({}) { |result_hash, instance_variable|
result_hash[instance_variable] = instance.instance_variable_get(instance_variable)
result_hash # Gotta have the block return the result_hash
}
end
def desc(str); @desc = str; end
def switch(sym); @switch = sym; end
def action(&blk); @action = blk; end
end
def command(name, &blk)
values_set_within_dsl = CommandDSL.call(&blk)
# INSERT CODE HERE
p name
p values_set_within_dsl
end
command :list do
desc 'show todos in long form'
switch :l
action do |global,option,args|
# some code that's not relevant to this question
end
end
将打印:
:list
{:@desc=>"show todos in long form", :@switch=>:l, :@action=>#<Proc:0x2392830@C:/Users/Ryguy/Desktop/tesdt.rb:38>}
我编写了一个类来处理这个确切的问题,并处理 @instance_variable 访问、嵌套等问题。这是另一个问题的写法:
@Jatin Ganhotra 的答案似乎更准确,但需要根据问题进行调整并提供更多信息。
以下是经过调整的解决方案
module DslAble
# It runs the `block` within this object context
# @note if the object misses any method, redirects the method to the
# original evaluate caller.
# Parameters are passed to the block
def evaluate(*args, **kargs, &block)
@self_before_evaluate = eval "self", block.binding
instance_exec(*args, **kargs, &block).tap do
@self_before_evaluate = nil
end
end
# When it's the case, redirect to the original `evaluate` caller
# @see https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
def method_missing(method, *args, **kargs, &block)
super unless @self_before_evaluate
@self_before_evaluate.send(method, *args, **kargs, &block)
end
end
有两项主要修正和一项改进:
nil
method_missing
调用的上下文中时,应以 evaluate
的默认行为为准(这就是我们调用 super
的地方)instance_exec
允许将参数传递给block
。这样我们就可以使用块接收的参数来调用 evaluate
,最终用户可能仍然想使用它们(并且为了与现有定义向后兼容)。在问题的场景中,假设有一个类
Command
。您可以在其中包含此模块:
class Command
include DslAble
def self.make_object(*names); some_logic_here; end
def desc(str); @last_desc = str; end
def switch(sym); @switches << Switch.new(sym).desc(@last_desc) ; end
def action(&blk); @switches.last.action(&blk); end
end
然后,顶层
command
方法将定义如下:
def command(*names, &block)
command = Command.make_object(*names) # omitted in the question
command.evaluate(command, &block)
end
command
作为参数传递给块。 向后兼容与以前的定义,其中command
是明确地称为块的参数。您为
command
创建的新块将隐式引用您的 Command
对象的方法,从而使以下内容按您的预期工作:
desc 'list all todos'
command :list do
desc 'show todos in long form'
switch :l
action do |global,option,args|
# some code that's not relevant to this question
end
end
必须注意的是,当使用
missing_method
时,带有 evaluate
钩子的方法将引用回原始调用者。这意味着在 command
块中,您应该能够引用该块的原始上下文中可用的方法(即 argument
)。但是,如果该方法也存在于您的 Command
对象中,则会调用它:
desc 'list all todos'
command :list do
desc 'the long form'
switch: :l
action { |*args| do_whatever }
# Nested command definition
desc "this desc does not define below's command, but override switch's one :/"
command :to_csv { do_some_stuff }
end
command
将通过missing_method
正确调用(从主上下文),但此方法将无法将最后一个desc
描述链接到嵌套的command
,因为desc
作为Command
存在
方法(并且 command
对象位于其块内 self
)。rpec
那样捕获和解析上下文。在此更改之前,上述情况不会发生。但我想这是使用嵌套 DSL 引用冲突方法的一个正常问题(在本例中是
desc
)。
您可以使用
command
方法的命名参数来解决此问题:
class Command
def my_desc(str = :unused)
return @desc if str == :unused
@desc = str
end
end
def command(*names, desc: nil, &block)
command = Command.make_object(*names)
command.my_desc(desc) if desc
command.evaluate(command, &block)
end
desc 'list all todos'
command :list do
desc 'the long form'
switch: :l
action { |*args| do_whatever }
# Nested command definition
cdesc = "this desc does not define below's command, but override switch's one :/"
command :to_csv, desc: cdesc { do_some_stuff }
end
还有其他选择,但这里不是主题。