at posts/single.html

tDiary の1.8と1.9の互換性を向上したい

2年前にtDiaryの1.8と1.9の混在を試した時に、Stringの仕様変更に対応して以下のようなコードを書いた。

class String
  def method_missing(name, *args, &block)
    each_line.__send__(name, *args, &block)
  end
end

これはruby 1.9でStringがEnumerableでなくなった (eachメソッドを持たなくなった) ことに対応するため。

# ruby1.8.7
irb(main):001:0> "aaa\nbbb".map
=> ["aaa\n", "bbb"]
# ruby1.9.1
irb(main):001:0> "aaa\nbbb".map
NoMethodError: undefined method `map' for "aaa\nbbb":String
        from (irb):1
        from /opt/local/bin/irb1.9:12:in `<main>'

既存コードに手を入れたくなかったので、String#mapなどのEnumerable系のメソッドが呼ばれた場合に、method_missingを使ってeach_line経由で呼び出すようにしてた。

いま見てもこれはひどいコードなので、もう少し互換性をあげようと思う。

method_missingで反応するメソッドを絞り込む

元のコードはすべての存在しないメソッドに対してeach_line経由に変換してる。 このままだと "string".mapp のようなtypoにも反応してしまってバグの温床になる。 そこで、Enumerable系のメソッドだけに反応するようにして、それ以外はsuperを呼び出してちゃんと例外を返すようにした。

class String
        def method_missing(name, *args, &block)
                if each_line.respond_to?(name)
                        each_line.__send__(name, *args, &block)
                else
                        super
                end
        end
end

1.8系でもString#linesメソッドを使うようにする

これまでのように

body.map { ... }

とするのではなく、

body.lines.map { ... }

と書くようにする。 2年前にリリースされたruby1.8.7ではString#linesが追加されているので、1.8.7と1.9.1だと互換性が保たれている。 もちろん、1.8.6以前でも動くように compatible.rb に String#lines を定義しておく。

unless "".respond_to?('lines')
        class String
                alias_method :lines, :to_a
        end
end

String#eachが呼び出されるとログに記録する

linesを使うようにするため、String#eachが呼ばれるたびにログに呼び出し元を記録するようにした。

class String
        def method_missing(name, *args, &block)
                if each_line.respond_to?(name)
                        require 'logger'
                        log_file = "deprecated.log"
                        @@tdiary_compatibile_logger ||= Logger.new(log_file)
                        @@tdiary_compatibile_logger.warn("called deprecated method: String##{name}")
                        @@tdiary_compatibile_logger.warn(caller.join("\n"))
                        each_line.__send__(name, *args, &block)
                else
                        super
                end
        end
end

これを埋め込んでおいて、ログに出力された箇所をlinesを使うように置き換えていけばOK(本当は、テストコードで検出できるのが一番いいんだけどね)。 …と思って、早速ここの日記に埋め込んでみたけど、まだ1つもログが出力されない。 もしかして、もうmethod_missingを使ったモンキーパッチは不要なのかも。

進展があったら、GitHubのブランチでちまちま作業していく予定。

関連する日記