pockestrap

Programmer's memo

mikutter から ActiveRecord の validates を使う

mikutter とは、Ruby で書かれた Linux 向け Twitter クライアントです。
というのは表向きの顔で、実際は Plugin で高度に拡張可能な Twitter アプリケーションフレームワークです。

ともなれば、mikutter で Web アプリケーションを作りたくなるのは必然です。
Web アプリケーションを作るには、データベースを用意することが殆どでしょう。
そして、Ruby で DB と言えば ActiveRecord が有名です。
mikutter から ActiveRecord を使いたくなりますね。

問題

しかし、mikutter で ActiveRecord を使用する上で、一つ問題が発生しました。
なんと、uniqueness なバリデーションを指定したところ、例外が発生して mikutter が起動しなくなってしまったのです。

エラー発生コード例

require 'active_record'

module DB;end

class DB::User < ActiveRecord::Base
  validates :screen_name, uniqueness: true
end

スタックトレース

$ mikutter
/opt/mikutter/core/utils.rb:367:in `[]': no implicit conversion of Symbol into Integer (TypeError)
        from /opt/mikutter/core/utils.rb:367:in `block in convert_key'
        from /opt/mikutter/core/utils.rb:366:in `each_pair'
        from /opt/mikutter/core/utils.rb:366:in `convert_key'
        from /home/pocke/.gem/ruby/2.2.0/gems/activesupport-4.2.4/lib/active_support/core_ext/hash/slice.rb:32:in `block in slice!'
        from /home/pocke/.gem/ruby/2.2.0/gems/activesupport-4.2.4/lib/active_support/core_ext/hash/slice.rb:32:in `map!'
        from /home/pocke/.gem/ruby/2.2.0/gems/activesupport-4.2.4/lib/active_support/core_ext/hash/slice.rb:32:in `slice!'
        from /home/pocke/.gem/ruby/2.2.0/gems/activemodel-4.2.4/lib/active_model/validations/validates.rb:106:in `validates'
        from /home/pocke/.mikutter/plugin/ar_test.rb:6:in `<class:User>'
        from /home/pocke/.mikutter/plugin/ar_test.rb:5:in `<top (required)>'
        from /opt/mikutter/core/miquire_plugin.rb:138:in `load'
        from /opt/mikutter/core/miquire_plugin.rb:138:in `load'
        from /opt/mikutter/core/miquire_plugin.rb:97:in `block in load_all'
        from /opt/mikutter/core/miquire_plugin.rb:37:in `block in each_spec'
        from /opt/mikutter/core/miquire_plugin.rb:32:in `each'
        from /opt/mikutter/core/miquire_plugin.rb:32:in `each'
        from /opt/mikutter/core/miquire_plugin.rb:35:in `each_spec'
        from /opt/mikutter/core/miquire_plugin.rb:95:in `load_all'
        from /opt/mikutter/core/boot/load_plugin.rb:10:in `<top (required)>'
        from /opt/mikutter/core/miquire.rb:98:in `require'
        from /opt/mikutter/core/miquire.rb:98:in `miquire_original_require'
        from /opt/mikutter/core/miquire.rb:95:in `file_or_directory_require'
        from /opt/mikutter/core/miquire.rb:76:in `block in miquire'
        from /opt/mikutter/core/miquire.rb:75:in `each'
        from /opt/mikutter/core/miquire.rb:75:in `miquire'
        from /opt/mikutter/core/miquire.rb:18:in `miquire'
        from /opt/mikutter/mikutter.rb:38:in `<main>'

配列にSymbolでアクセスしようとしているような気がする例外ですね。

調査

このスタックトレースで注目すべき点ははつあります。
上記の順番通りに抜き出してみました。

  • from /opt/mikutter/core/utils.rb:366:in `convert_key'
  • from /home/pocke/.gem/ruby/2.2.0/gems/activesupport-4.2.4/lib/active_support/core_ext/hash/slice.rb:32:in `block in slice!'
  • from /home/pocke/.mikutter/plugin/ar_test.rb:6:in `<class:User>'

実行順序順に、下から見ていきましょう。

from /home/pocke/.mikutter/plugin/ar_test.rb:6:in `<class:User>'

一番下は、先ほど示したコードのvalidates :screen_name, uniqueness: trueの部分です。
ここで ActiveRecordvalidatesメソッドを読んでいることがわかります。

from /home/pocke/.gem/ruby/2.2.0/gems/activesupport-4.2.4/lib/active_support/core_ext/hash/slice.rb:32:in `block in slice!'

真ん中は、ActiveRecord で定義されているメソッドのうち、最後の部分ですね。
スタックトレースのこれより上の部分では、何故か mikutter が定義するメソッドが呼ばれているようです。

下記がこの部分のソースを抜粋したものです。

def slice!(*keys)
  keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)  # この行
  omit = slice(*self.keys - keys)
  hash = slice(*keys)
  hash.default      = default
  hash.default_proc = default_proc if default_proc
  replace(hash)
  omit
end

どうやら、Hashクラスがrespond_to?メソッドを受け付ける場合とそうでない場合で処理を分岐しているようです。

from /opt/mikutter/core/utils.rb:366:in `convert_key'

一番上は、mikutter のコアのコード内です。

class Hash
  # キーの名前を変換する。
  def convert_key(rule = nil)
    result = {}
    self.each_pair { |key, val|     # この行
      if rule[key]
        result[rule[key]] = val
      else
        result[key.to_sym] = val end }
    result end

どうやら、mikutter がHashクラスを拡張してconvert_keyを定義しているようです。
しかし、それと ActiveRecord が期待するconvert_keyの意図が違うため、例外を吐いているようです。

解決策

convert_keyメソッドの有無によって処理を分岐しているため、このメソッドを消してしまうか、respond_to?を書き換えてしまうのがよさそうです。
今回は、前者の方法をとってみました。

最初に例示したコードを以下のように書き換えます。

require 'active_record'

module DB;end

class DB::User < ActiveRecord::Base
  # convert_key メソッドを別名に逃し、定義を消す
  class ::Hash
    alias_method :_convert_key, :convert_key
    undef convert_key
  end

  validates :screen_name, uniqueness: true

  # convert_key メソッドを復活させ、別名を消す。
  class ::Hash
    alias_method :convert_key, :_convert_key
    undef _convert_key
  end
end

validatesメソッドを呼び出す前にconvert_keyメソッドを消し、validatesメソッドを呼び出した後に復活させています。
これで無事 mikutter を起動できるようになります。

もしvalidates複数箇所で使いたい場合などは、convert_keyの定義をいじっている部分をブロックを受け付けるメソッドにして対応すべきでしょう。

結論

モンキーパッチは悲しみしか生まない。
しかし、その悲しみから人々を救い出すのもまたモンキーパッチなのだ。