(jpmobile の) mobile_filter hankaku => true は controller.response.body が freeze されてるとエラーになる

そもそも response.body が freeze された後に、文字変換すんなよというのは置いておいて。。。

例えば、

class HogeController < ApplicationController
  mobile_filter :hankaku => true
  around_filter :my_around_filter
  # mobile_filter は around_filter なので、filterの実行順は
  #   mobile_filter(before) --> my_around_filter(before) --> action(index) -->
  #   my_around_filter(after) --> mobile_filter(after)
  #
  # (mobile_filterは、正確にはオプション指定によるが、around_filter が1〜4つだが、
  #  ここではまとめて1つとして記述している)

  def index
  end

  private
  def my_around_filter
    # before filter では何もしない
    yield
    # after filter で、response.body を freeze
    response.body.freeze
  end
end

これを実行すると、「can't modify frozen string」でエラーになる

エラー箇所は、「vendor/plugins/jpmobile/lib/jpmobile/filter.rb:103:in `gsub!'」

周辺のコードは、次のようになっている。

def filter(str, from, to)
  str = str.clone
  from.each_with_index do |int, i|
    str.gsub!(int, to[i])            # line. 103
  end
  str
end

str に response.body が入ってくるのだが、
freezeされているものを gsub! で(全角カナ --> 半角カナ に)破壊的に変更しようとしてエラーとなっている。


ちなみに、hankaku => true 無しだと、問題なく動作する

class HogeController < ApplicationController
  mobile_filter                        # 全角・半角変換無し
  around_filter :my_around_filter

  def index
  end

  private
  def my_around_filter
    # before
    yield
    # after
    response.body.freeze
  end
end

これであっても、freeze 後に文字コードを変えているのだが、なぜこっちではエラーにならないのか。


結論から言うと、文字コード変更などでは、response.body そのものを変更しているのではなく、
response.body の文字列をコピーしたものを変更して、それをresponse.body に入れているからだと思われる。


frozen? でresponse.body の状態を出力させてみると

class HogeController < ApplicationController
  around_filter :my_around_filter1     # 確認用
  mobile_filter                        # 全角・半角変換無し
  around_filter :my_around_filter2
  # 実行順序は、
  #    my_around_filter1(before) --> mobile_filter(before) --> my_around_filter2(before) -->
  #    action(index) -->
  #    my_around_filter2(after) --> mobile_filter(after) --> my_around_filter1(after)

  def index
  end

  private
  def my_around_filter1
    # before
    yield
    # after
    logger.debug("### #{response.body.frozen?}")
  end

  def my_around_filter2
    # before
    yield
    # after
    response.body.freeze
    logger.debug("### #{response.body.frozen?}")
  end

# ログは以下のようになり、mobile_filter で response.body の freeze 状態が解除されているのが確認できる
#   ### true
#   ### false
end


で、全角・半角変換有りの話しに戻って、気になるのが、エラーが出ていた103行目の2行上にある「str = str.clone」
これは一体何のために存在しているのか。


もし、freezeされていない文字列のコピーを作りたいという考えであるとすれば、
cloneでは意図する動作になっていない。

プログラミング言語 Ruby リファレンスマニュアル の clone, dup の説明に書いてあるとおり、
clone では、freeze状態までコピーしてしまう。


試しに、101行目のcloneをdupに変更したら、問題なく動作した。


これがjpmobileの意図する通りの挙動なのか、バグなのかは分からないが、
全角・半角変換だけがfreezeされた場合にエラーになるのは、個人的には微妙な挙動だと思う。
response.bodyがfreezeされていた時にエラーにするなら、文字コード変換などでもエラーを出力し、
freezeされていてもfreezeを解除して実行してしまうのであれば、
全角・半角変換でもエラーにならないようになっていると、
mobile_filter全体として一貫性がある気がする。


ただ、文字コード変更や、絵文字のキャリア出し分けみたいな変換は、外見を変えるものではないのに対し、
全角・半角変換は外見からして変えるものだから、こういう挙動にしているんだと言われたら、
それはそれで納得できそうな気もする。