takedaです。前回に引き続き今回もRubyです。Rubyを使い始めて3ヶ月ほどになりますが、Rubyとの接し方にも慣れてきました。
今回は、内部DSLを用いた、設定ファイルの読み込みについて記述します。
端的にまとめると、Rubyのプログラムを設定ファイルのように扱おうということです。
内部DSLとは
DSLとはドメイン特化言語と呼ばれるものです。ある事柄に対して、特化しているプログラミング言語のようなものです。MySQLなどの設定ファイルも、設定項目に対して、代入を行うだけのプログラミング言語とみなすことができるかと思います。
内部というのは、Rubyという言語の内部で特化された言語ということです。
外部DSLは文法などが一から定義されるのに対して、内部DSLでは、Rubyの文法に則って記述します。
何ができるのか
通常のYAMLやJSONなどの設定ファイルの記述と違い何ができるのかを、以下に挙げます。- 関数を設定できる
- 読み込み時にイベントを起こせる(検査など)
- Rubyで許される記述方法がすべて許される
- 間違った設定項目があった場合にエラーが出る(missing_methodでエラーを拾えば自由に対処できる)
実際に作ってみる
以下のようなコードを設定ファイルとします。
1 2 3 4 |
foo 14 bar 15 boo { | val | val > 5 } bar 18 |
以下のように読み取り用のクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Reader def initialize @data = {} end def foo( arg ) @data[__method__] = arg #該当するハッシュに対して値をセット end def bar( *args ) if @data[__method__].is_a? Array # 既に配列であれば追加 @data[__method__] += args else # 配列でなければ代入 @data[__method__] = args end end def boo( &block ) @data[__method__] = block # 関数も保存しておける end def get # 値を返却するためのメソッド @data end end |
以下は実際に設定ファイルから値を読み取るためのコードです。
1 2 3 4 5 6 7 |
require_relative 'reader' config = Reader.new # 読み込んだファイルをinstance_evalに投げるだけ config.instance_eval File.read 'sample1.rb' p config.data # => {:foo=>14, :bar=>[15, 18], :boo=>#<Proc:0x007f88fb8b9af8@(eval):3>} |
このように、とても簡単に値を取得できます。値の代入がメソッドの呼び出しなので、値の加工やバリデーションなども分かりやすいです。
instance_evalとは何なのか
instance_evalは与えたコードの示すselfを、呼び出したインスタンスとして扱うことができるメソッドです。簡単に言うと以下と同じようなことをしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Reader # 上記のメソッド定義..... def execute_code # sample1.rbのコード foo 14 bar 15 boo { | val | val > 5 } bar 18 end end reader = Reader.new reader.execute_code |
selfというのはmethod定義時には省略することができますが、実際は「self.foo 14」のような呼び出しとなり、自身のメソッドが呼ばれます。
なので、self部分をinstance_evalによりインスタンスを束縛することで、そのインスタンスのメソッドで実行したこととなります。
代入にはイコールを使いたい
設定ファイルなので代入はイコールを使いたいと誰もが思うかと思います。しかし以下の様な設定ファイルは作る事ができません。
1 2 3 |
foo = 14 bar = 15 bar = 18 |
前回の開発ブログでも出てきましたが、Rubyでは「def foo=( val )」というような定義により、「reader.foo = 14」で実行できるメソッドが定義できます。
しかし、このメソッドは、同じクラスのメソッド内では呼び出すことができません。
以下のように定義して「execute_code」を呼び出した場合は、fooというローカル変数への代入とみなされます。
1 2 3 4 5 6 7 8 9 |
class Reader def foo=( value ) @foo = value end def execute_code foo = 14 end end |
なのでメソッドの代入を呼び出したいのであれば、以下のように記述することで実現できます。
1 2 3 4 |
# メソッドの呼び出し self.foo = 14 self.bar = 15 self.bar = 18 |
もしくは、一度メソッドのブロック構文で括り、引数として渡されたオブジェクトに対して、代入を行うこともできます。
1 2 3 4 |
far do | val | val.a = 10 val.b = 20 end |
クラス側は以下のようになります。
1 2 3 4 5 6 |
class Reader def far val = Struct.new( :a, :b ).new # 構造体を作って渡す yield val end end |
このように代入文を使用したい場合は、ローカル変数への代入にならないように、
何らかのオブジェクトを示すことで実現できます。
気をつけるべきこと
Rubyの言語として動作しているので、悪意のあるプログラムを埋め込むこともできてしまいます。なので、使用できる場面としては、設定ファイルの内容に責任をもてるような場面に限られるでしょう。最後に
このように、Rubyをそのまま設定ファイルとすることで、記述する方法が自由に決められます。VagrantやChefなどのソフトの設定ファイルもRubyによって記述されます。機会があれば使って、参考にしてみてください。
今回もRubyでした。次回もRubyの予定です。近日中には小ネタで投稿すると思います。