2012-01-03 11 views
5

私はRubyプロジェクトに「クラスレスDSL」を作成する方法を理解しようとしています。ステップ定義がCucumberのステップ定義ファイルで定義されている方法やSinatraアプリケーションで定義されている方法と似ています。私はそれがある方法の束を持つグローバル(Kernel)名前空間を汚染する悪い習慣だと仮定しRubyでクラスレスDSLを作成するには?

#sample.rb 

when_string_matches /hello (.+)/ do |name| 
    call_another_method(name) 
end 

は例えば、私はすべての私のDSL機能が呼び出されているファイルを持つようにしたいです私のプロジェクトに特有のものですしたがって、方法when_string_matchescall_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クラスで評価するのではなく、そのコンテキストでインスタンスを作成して評価することです。しかし、私はこれを行う方法を知らない。

私はそれが理にかなっていると思います。どんな助け、経験、アドバイスも感謝します。

def when_string_matches(regex) 
    # do whatever is required to produce `my_string` and `name` 
    yield(name) if my_string =~ regex 
end 

答えて

4

あなたのやりたいことをどうやって行うのかについてのパットの答えは、少し難しいです。 Eloquent Rubyという本を見てみることをお勧めします。そこにはおそらく貴重なDSLを扱ういくつかの章があるからです。あなたは、これらの他の図書館が行っていることをどのようにしているかについていくつかの情報を求めてきたので、私は簡単に概要を説明しようと思います。

シナトラ

あなたはシナトラコード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

FYI 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   = ?/ 
+0

それは私の頭の上に少しあるので、そこに消化するための多くがありますが、それはまだ有用です。ありがとう! – Andrew

2

ただ、何でも「文字列」に対してそれをテストし、引数として正規表現を取るwhen_string_matchesと呼ばれる方法を定義しますが、そのブロックにあるものは何でもname渡して、条件付きで収量を話して、としていますこれは本質的にすべてのRuby DSLです:ブロックを受け入れることが多い面白い名前のメソッド。

+1

... Kernel' '上で定義されています。 – Reactormonk

+0

次に、メソッド定義を変更して、後で実行するために与えられたブロックを任意の状態変数とともに保存します。 – meagar

+0

[OK]を、私は私の状況をより良く説明することを望むコードサンプルのトンで私の質問を更新しました。問題は、ファイルの解析と評価と、ブロックが最初に定義された場所では利用できないインスタンスメソッドの呼び出しにあります。 – Andrew

3

モジュールを使用してコードを整理できます。 Module#includeメソッドを使用して、ModuleクラスにDSLメソッドを追加できます。 RSpecがそれを行う方法は次のとおりです。最後の2行はあなたが探しているものです。 DSLをシンプルに保つことについて+1〜@ imagar!

@UncleGeneが指摘するように、RSpecはDSLメソッドでKernelを汚染します。私はそれを回避する方法がわかりません。 describeメソッドを持つ別のDSLがある場合は、どちらが使用されているかを判断するのは難しいでしょう。

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) 
+0

これは非常に役に立ちます、ありがとう! – Andrew

+1

ここではカーネルを汚染しませんか? 'rspec'が必要です。 Kernel.methods.grep/describe/=>を記述します。そして、私は汚染モジュールがそれ以上に優れているとは確信していません(AFAIU OPは公害を避けようとしていました) – UncleGene

+0

あなたは正しいです。私はこの点を追加する答えを編集しています。 – CubaLibre

関連する問題