使用lambdas创建灵活的ruby DSL



我正在尝试创建一个灵活的dsl。我已经有DSL模块了,比如module DSL。DSL用户可以将其作为一个类创建衍生产品。DSL的要点是允许用户使用自定义渲染方法创建Feature对象。有很多丑陋且非DRY的代码支持Feature,因此进行了抽象,但用户需要对该功能的呈现方式进行大量控制,而我的元编程无法胜任这项任务。让我给你看看它是怎么设置的。

DSL看起来像这样:

module DSL
  module ClassMethods
      attr_accessor :features
      def column(name, *args)
        arguments = args.pop || {}
        self.features = [] if self.features.nil?
        self.features << Feature.new(name, arguments)
      end
    end
    def self.included(base)
      base.extend ClassMethods
    end
  end
end

它的实现看起来像这样:

class DSLSpinOff
  include DSL
  feature :one
  feature :two, render_with: :predefined_render
  feature :three, render_with: :user_defined_render
  feature :four, render_with: lambda {
    puts "Go nuts, user!"
    puts "Do as you please!"
  }
  def user_defined_render
    #...
  end
end

最后,特性类本身位于DSL中,如下所示:

module DSL
  #...
private
  class Feature
    attr_accessor :name, :render_with
    def initialize(name, *args)
      self.name = name
      attributes = args.pop || {}
      # somehow delegate attributes[:render_with] to the render function, handling defaults, lamdbas, function string names, etc
      self.render_with = attributes.fetch(:render_with, :default_render)
    end
  private
    def default_render
      #...
    end
    def predefined_render
      #...
    end
  end
end

我一直在寻找的魔法:define_singleton_method

module DSL
  #...
private
  class Feature
    attr_accessor :name
    def initialize(name, args)
      #...
      define_singleton_method :render do
        if self.render_with.kind_of? Symbol
          content = self.send(self.render_with)
        else
          content = self.render_with.call
        end
        content.present? ? content.to_s.html_safe : '&ndash;'
      end
    end
  end
end

现在,在DSL中,我可以迭代所有功能并呈现它们。它将向自己发送:default_render或其他:predefined_render,或者使用提供的块。但是,这不允许用户在DSLSpinOff中定义方法并将其传入,因为这些方法将被委派给DSLSpinOff类,而不是DSLSpinOff::Column类。

我怀疑他们会做一些类似的事情:

feature :three, render_with: self.method(:user_defined_render)
def user_defined_render
  #...
end

编辑:

我找到了一种干净的方法来允许使用默认方法、用户定义的lambda、预定义的方法和用户定义的方法:

module DSL
  #...
  class Feature
    attr_accessor :name, render_with
    def initialize(name, *args)
      self.name = name
      self.render_with = args.has_key?(:render_with) ? args[:render_with] : :default_render
      define_singleton_method :render do |object|
        render_method = self.render_with.is_a?(Proc) ? renderer : method(renderer) 
        render_method.call
      end
    end
  private
    def default_render
      #...
    end
    def predefined_render
      #...
    end
  end
end

如果用户传入lambda或proc,这将获取它们,否则,它将使用method方法返回对Feature上定义的方法的引用。call方法适用于这三种情况。

要支持用户定义的呈现方法,只需在初始化器中打开该类,即可通过method方法找到它:

#initializers/custom_dsl_renders.rb
class DSL::Feature
  def user_defined_render
    #...
  end
end

最新更新