RubyでBasicObject#instance_eval()
には第2引数にファイル名を渡すことができる。
サンプルコード
例えばConfigクラスがあり、DSLで書かれたコンフィグファイルをロードするようなコードを書いたとする。
config.rb
class Config
def initialize
@config = {}
end
def load
instance_eval File.read('./conf/setting.rb')
end
def set(key, value)
@config[key] = value
end
def fetch(key)
@config[key]
end
end
conf/setting.rb
set :app, 'My App'
set :url, 'http://example.com'
set :log_leve, :info
bin/runner
require_relative '../config'
config = Config.new
config.load
p config.fetch(:url) # => "http://example.com"
サンプルコードの問題点
上のコードにはinstance_evalに関連して以下の問題がある
- conf/setting.rbの中でユーザ(DSLを利用するユーザ)がタイポするとエラーメッセージが分かりづらい
- conf/setting.rbの中でユーザが任意のコードを書き
__FILE__
ディレクティブを使うと動かない
1についてはconf/setting.rbの中でタイポしても、コンソールに表示されるエラーメッセージはconfig.rb:7:in `instance_eval': undefined method `settt' for #<Config:0x007f8c6a88af88 @config={}> (NoMethodError)
こうなり一見どこでタイポしたか分からない。そうではなくタイポしたファイル名 + 行番号 conf/setting.rb:行番号 を表示してほしい
2についてはconf/setting.rbの中で__FILE__
の値はsetting.rb
ではなくinstance_evalをcallしたconfig.rb
になってしまう。
そこでinstance_evalの第2引数にファイル名を渡しておけば良い。
config.rb
class Config
..snip..
def load
path = File.expand_path('./conf/setting.rb')
instance_eval File.read(path), path
end
..snip..
end