用Ruby构建"半自然语言"DSL

Joh*_*tta 5 ruby regex dsl parsing

我有兴趣在Ruby中构建一个用于解析微博更新的DSL.具体来说,我认为我可以将文本转换为Ruby字符串,就像Rails gem允许"4.days.ago"一样.我已经有了将翻译文本的正则表达式代码

@USER_A: give X points to @USER_B for accomplishing some task
@USER_B: take Y points from @USER_A for not giving me enough points
Run Code Online (Sandbox Code Playgroud)

变成类似的东西

Scorekeeper.new.give(x).to("USER_B").for("accomplishing some task").giver("USER_A")
Scorekeeper.new.take(x).from("USER_A").for("not giving me enough points").giver("USER_B")
Run Code Online (Sandbox Code Playgroud)

我可以正式确定更新的语法,以便只提供和解析标准化文本,从而可以巧妙地处理更新.因此,似乎更多的是如何实现DSL类的问题.我有以下存根类(删除所有错误检查并用注释替换一些以最小化粘贴):

class Scorekeeper

  attr_accessor :score, :user, :reason, :sender

  def give(num)
    # Can 'give 4' or can 'give a -5'; ensure 'to' called
    self.score = num
    self
  end

  def take(num)
    # ensure negative and 'from' called
    self.score = num < 0 ? num : num * -1
    self
  end

  def plus
    self.score > 0
  end

  def to (str)
    self.user = str
    self
  end

  def from(str)
    self.user = str
    self
  end

  def for(str)
    self.reason = str
    self
  end

  def giver(str)
    self.sender = str
    self
  end

  def command
    str = plus ? "giving @#{user} #{score} points" : "taking #{score * -1} points from @#{user}"
    "@#{sender} is #{str} for #{reason}"
  end

end
Run Code Online (Sandbox Code Playgroud)

运行以下命令:

t = eval('Scorekeeper.new.take(4).from("USER_A").for("not giving me enough points").giver("USER_B")')
p t.command
p t.inspect
Run Code Online (Sandbox Code Playgroud)

产生预期结果:

"@USER_B is taking 4 points from @USER_A for not giving me enough points"
"#<Scorekeeper:0x100152010 @reason=\"not giving me enough points\", @user=\"USER_A\", @score=4, @sender=\"USER_B\">"
Run Code Online (Sandbox Code Playgroud)

所以我的问题主要是,我是否正在通过构建这个实现来做自己的事情?有没有人有任何改进DSL类本身的例子或任何警告?

顺便说一下,为了获得eval字符串,我主要使用sub/gsub和regex,我认为这是最简单的方法,但我可能错了.

Dav*_* J. 5

我是否正确理解您:您想从用户那里获取一个字符串并导致它触发某些行为?

根据您列出的两个示例,您可以使用正则表达式.

例如,要解析此示例:

@USER_A: give X points to @USER_B for accomplishing some task
Run Code Online (Sandbox Code Playgroud)

使用Ruby:

input = "@abe: give 2 points to @bob for writing clean code"
PATTERN = /^@(.+?): give ([0-9]+) points to @(.+?) for (.+?)$/
input =~ PATTERN
user_a = $~[1] # => "abe"
x      = $~[2] # => "2"
user_b = $~[3] # => "bob"
why    = $~[4] # => "writing clean code"
Run Code Online (Sandbox Code Playgroud)

但是如果存在更多复杂性,那么在某些时候您可能会发现使用真正的解析器更容易,更易于维护.如果你想要一个适用于Ruby的解析器,我推荐Treetop:http://treetop.rubyforge.org/

采用字符串并将其转换为代码以使其被唤醒的想法让我感到紧张.使用eval是一个很大的风险,如果可能应该避免使用.还有其他方法可以实现您的目标.如果你愿意,我会很乐意提供一些想法.

关于您建议使用的DSL的问题:您是否会在应用程序的另一部分本地使用它?或者只是计划将它作为过程的一部分,将字符串转换为您想要的行为?我不确定什么是最好的而不知道更多,但如果你只是解析字符串,你可能不需要DSL.


Joh*_*tta 0

基于 @David_James 的回答,我提出了一个仅使用正则表达式的解决方案,因为我实际上并没有在其他任何地方使用 DSL 来构建分数,而只是向用户解析分数。我有两种用于搜索的模式:

SEARCH_STRING = "@Scorekeeper give a healthy 4 to the great @USER_A for doing something 
really cool.Then give the friendly @USER_B a healthy five points for working on this. 
Then take seven points from the jerk @USER_C."

PATTERN_A = /\b(give|take)[\s\w]*([+-]?[0-9]|one|two|three|four|five|six|seven|eight|nine|ten)[\s\w]*\b(to|from)[\s\w]*@([a-zA-Z0-9_]*)\b/i

PATTERN_B = /\bgive[\s\w]*@([a-zA-Z0-9_]*)\b[\s\w]*([+-]?[0-9]|one|two|three|four|five|six|seven|eight|nine|ten)/i

SEARCH_STRING.scan(PATTERN_A) # => [["give", "4", "to", "USER_A"],
                              #     ["take", "seven", "from", "USER_C"]]
SEARCH_STRING.scan(PATTERN_B) # => [["USER_B", "five"]]
Run Code Online (Sandbox Code Playgroud)

正则表达式可能会被清理一下,但这使我能够拥有允许一些有趣的形容词的语法,同时仍然使用“名称->点”和“点->名称”语法来提取核心信息。它不允许我抓住原因,但这太复杂了,所以现在我将只存储整个更新,因为在除异常情况之外的所有情况下,整个更新无论如何都将与每个分数的上下文相关。获取“给予者”用户名也可以在其他地方完成。

我也写了这些表达式的描述,希望其他人可能会觉得有用(这样我就可以回过头来记住那一长串官样文章的含义:)