pumaに学ぶrubyのDSLの作り方
調べごとがあって、pumaのソースを読んだ。
そこでpumaの設定ファイルに書いてあるDSLの適用の処理が勉強になったので書く。
(puma: v2.11.0 時点)
pumaのソース
puma/dsl.rb at master · joe-re/puma · GitHub
ここでやっている。
主要な処理だけ抜き出した
主要な処理だけ抜き出して、少しだけ修正して分かりやすくした。
これを実行すると以下になる。
[~/src/sandbox/ruby_dsl]$ ruby dsl.rb blockも渡せる #<DSL:0x007fc39a1fc740 @options={:foo=>"test", :boolean_value=>true}>
config.rbで設定した値が適用されたのが分かる。
いいところ
configファイルを間違って記述したときに、分かりやすい形でエラーを返してくれる。
config.rbを書き換える。
foo 'test' block_test do puts 'blockも渡せる' end # コメントは無視される # boolean_test boolean_toast # typo!
実行する
[~/src/sandbox/ruby_dsl]$ ruby dsl.rb blockも渡せる config.rb:10:in `_load_from': undefined local variable or method `boolean_toast' for #<DSL:0x007f91c106c570> (NameError) ...
config.rbの10行目のboolean_toastが違うよ!と言ってくれる。賢い。
Rubyで書ける
DSLとは言え、コメントとかブロック書くときにRubyのルールで書けるのはうれしい。
実装が簡単
分かりやすいメタプログラミング。
解説
DSLクラスの_load_formメソッド内のinstance_evalに注目する。
instance_evalはrubyのBasicObjectに定義されているので、全てのオブジェクトで使えるインスタンスメソッドだ。
evalと同様、文字列が渡されたときはそれをRubyのコードとして評価するのだけど、違うのはinstance_evalのレシーバが評価のコンテキストになるということ。
この場合、レシーバを書かずにインスタンスメソッド内で実行しているので、レシーバはselfになる。
下と同じ。
self.instance_eval(File.read(path), path) if path
第1引数には、File.read(path)でファイルを読み込んだ文字列が渡されている。DSLクラスのインスタンスをコンテキストにして、この文字列が評価される。よって、config.rbに書いたDSLと同名のDSLクラスのインスタンスメソッドが呼ばれる。
instance_evalメソッドの第2引数にはファイルパスを渡していて、これのおかげでエラーが分かりやすくなる。
渡したファイルパスで実行されているようにスタックトレースを差し替えてくれるのだ。
instance method BasicObject#instance_eval
終わりに
unicornも読んでみたらほぼ同じ仕組みだった。
デザインパターンなのか?今日学んだパターンの名前を僕はまだ知らない。