Rails 我一直觉得 Sunspot#search 的 DSL 很酷,但是以我现在的能力实在是看不懂怎么实现,有没有高人来研究一下

ad583255925 · October 31, 2017 · Last by ad583255925 replied at October 31, 2017 · 1182 hits
MyClass.solr_search do
  any_of do
    with :col1, 1
    with :col2, 2
    all_of do
      with :col3, 3
      without :col4, 4
    end
  end
end

像这样,all_ofany_off可以无限层嵌套,分别代表orand, with代表等于, without代表!等于,在这样的 DSL 里面结合业务逻辑就很清晰,但是现在要使用 ElastivSearch 了,那个封装的就比较浅,没有这样的 DSL 可以用了,我想把这套思路搬出来,于是去看了源码,最深处看到两行有点懵

class ContextBoundDelegate
      class <<self
        def instance_eval_with_context(receiver, &block)
          calling_context = eval('self', block.binding)
          if parent_calling_context = calling_context.instance_eval{@__calling_context__ if defined?(@__calling_context__)}
            calling_context = parent_calling_context
          end
      ...
eval('self', block.binding)

这个什么意思呢, 顺便问一下 block 里面无限嵌套 block 的思路怎么实现

这种风格的核心原理是 obj.instance_exec(&block) 你 google 搜 Ruby DSL 就能看到很多风格 DSL 是怎么做的了

Reply to jasl

话说墙这两天翻不过去。

跟 rails 的 route 差不多,试着撸一个简陋的:

module Search

   def self.ing &blk
     Root.new.tap do |r|
       r.instance_eval(&blk)
       puts r
     end
   end

  def children
    @chidlren ||= []
  end

  def with condition, value
    children << With.new(condition, value)
  end

  def without condition, value
    children << Without.new(condition, value)
  end

  def any_of &blk
    a = Any.new
    a.instance_eval &blk
    children << a
  end

  def all_of &blk
    a = All.new
    a.instance_eval &blk
    children << a
  end

  def children_to_s
    children.map &:to_s
  end

  class Root
    include Search
    def to_s
      children.first.to_s
    end
  end

  class Any
    include Search
    def to_s
      '(' + children_to_s.join(' or ') + ')'
    end
  end

  class All
    include Search
    def to_s
      '(' + children_to_s.join(' and ') + ')'
    end
  end

  class With
    def initialize cond, v
      @cond, @v = cond, v
    end

    def to_s
      "#{@cond} = #{@v}"
    end
  end

  class Without
    def initialize cond, v
      @cond, @v = cond, v
    end

    def to_s
      "#{@cond} != #{@v}"
    end
  end
end

Search.ing do
  any_of do
    with :col1, 1
    with :col2, 2
    all_of do
      with :col3, 3
      without :col4, 4
    end
  end
end

# output
# (col1 = 1 or col2 = 2 or (col3 = 3 and col4 != 4))

我临时写了一个比较符合 ES 的

class MyDsl

  def initialize
    @hash = {}
  end

  def method_missing method_name, k=nil, v=nil, &block
    if block_given?
      new_container = self.class.new
      new_container.instance_eval(&block)
      @hash[method_name] = new_container.to_h
    else
      @hash[method_name] ||= {}
      @hash[method_name][k] = v
    end
  end

  def to_h
    @hash
  end
end
es = MyDsl.new
es.bool do
  must do
    match 'title', 'test'
  end
end

es.to_h
=> {:bool=>{:must=>{:match=>{"title"=>"test"}}}}
You need to Sign in before reply, if you don't have an account, please Sign up first.