yamlにRubyのクラス名を入れて設定ファイルとして利用する
Rubyで設定ファイルを作るとき、yamlファイルを利用することがある。そのyamlファイルの中に、ユーザ定義のクラス名を入れて、設定ファイルによって利用するクラスを変更するようなことをしたい。と思った時のためのやり方。
まあ、yamlクラスにあるんですけど。
yaml では、Ruby 向けに以下のローカルタグを扱えます。
http://doc.ruby-lang.org/ja/1.9.3/library/yaml.html
・ !ruby/class: Class オブジェクト
「この記法はRuby独自の方法だろうし、利用するエンジンによって実装が異なるだろうし使いたくないな!」と思っていたのだけど、調べてみるとこの !〜〜 という書き方はYAML1.1以降の正式なユーザ定義タグの仕様に則ってた。
http://www.yaml.org/spec/1.2/spec.html
ただ、この記法を使う場合、yamlファイルをパーズする時点でこのタグに相当するクラスが定義されていないとエラーが起こってしまうので、不便なときがある。
yamlから文字列として取得して、それをクラスにする
yamlファイルにはクラス名を文字列として格納しておいて、利用するときに改めてクラス名として展開する、というのが平和な解決法に思える。
文字列をクラスにする方法はググればそれなりに見つかる。大きく分けると二つ。
- eval する
- const_get する
evalは、与えられた文字列がクラス名だったら良いけれど、yamlファイルに "`sudo rm -rf ~/*`" とか書かれてるだけで死ぬので使えない。
const_getなら使えそうに見えるけど、階層が深くなると…
irb(main):001:0> Object.const_get("Hash") => Hash irb(main):002:0> require 'net/http' => true irb(main):003:0> Net::HTTP => Net::HTTP irb(main):004:0> Object.const_get("Net::HTTP") NameError: wrong constant name Net::HTTP from (irb):4:in `const_get' from (irb):4 from :0
とまあ、こうなる。
Object.const_getが行っているのは文字通り、クラス名という定数(const)をObjectから取り出しているだけ。Objectの中には "Net::HTTP" という定数はないので、失敗する。というからくり。
でも、Object直下に "Net" 自体はあるので、
irb(main):005:0> Object.const_get("Net") => Net irb(main):006:0> Object.const_get("Net").const_get("HTTP") => Net::HTTP
こうやって辿っていくことで、Net::HTTPを取得することができる。
したがって、任意のクラス名をクラスとして解釈するワンライナーは、例えばこう。
"Net::HTTP".split("::").reduce(Object, :const_get) => Net::HTTP
わりと常用しそうなテクニックなのに、誰も書いてない気がしたので記事化してみた。
reduceよりinjectが好きならそちらでどうぞ。