yamlにRubyのクラス名を入れて設定ファイルとして利用する

Rubyで設定ファイルを作るとき、yamlファイルを利用することがある。そのyamlファイルの中に、ユーザ定義のクラス名を入れて、設定ファイルによって利用するクラスを変更するようなことをしたい。と思った時のためのやり方。

まあ、yamlクラスにあるんですけど。

yaml では、Ruby 向けに以下のローカルタグを扱えます。
・ !ruby/class: Class オブジェクト

http://doc.ruby-lang.org/ja/1.9.3/library/yaml.html

「この記法はRuby独自の方法だろうし、利用するエンジンによって実装が異なるだろうし使いたくないな!」と思っていたのだけど、調べてみるとこの !〜〜 という書き方はYAML1.1以降の正式なユーザ定義タグの仕様に則ってた。
http://www.yaml.org/spec/1.2/spec.html

ただ、この記法を使う場合、yamlファイルをパーズする時点でこのタグに相当するクラスが定義されていないとエラーが起こってしまうので、不便なときがある。

それと、Ruby以外で読めない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が好きならそちらでどうぞ。