Rails3 で JSON 出力時に、日本語が文字参照となってしまうのを防ぐ方法

JSON 出力時に、日本語が文字参照となってしまうのは、
activesupport-3.0.9/lib/active_support/json/encoding.rb の
173 行目にある def encode_json(encoder) のためです。

class String
  def as_json(options = nil) self end #:nodoc:
  def encode_json(encoder) encoder.escape(self) end #:nodoc:
end

これを、下記のように application.rb 等でオーバーライドしてあげれば
文字参照を防いで出力することができるようです。

class String
  def encode_json(encoder)
    '"' + self + '"'
  end
end

Rails3 で Ajax (will_paginate 2.3.15 対応)

以前、http://d.hatena.ne.jp/nedate/20101006/1286341476 にて Rails3 で link_to_remote + will_paginate に対応する方法を書きましたが、will_paginate のバージョンが上がったらこの対応では正常に動作しなくなりました orz

というわけで、will_paginate 2.3.15 にて link_to_remote に対応する方法です。

まずは、app/helpers/remote_link_renderer.rb に下記のような内容を記述します。

class RemoteLinkRenderer < WillPaginate::LinkRenderer
  def page_link(page, text, attributes = {})
    params = @options[:params][:url]
    params[:page] = page

    @template.link_to_remote(
      text,
      {
        :url => params,
        :update => @options[:params][:update],
        :loading => @options[:params][:loading]
      }
    )
  end
end

そして will_paginate を下記のように書いてあげれば動くようになると思います。

<%= will_paginate(
    @users,
    :renderer => RemoteLinkRenderer,
    :params => {
      :url => {:action => 'show'},
    },
    :update => 'detail_zone',
    :loading => "showLoading('small', 'detail_zone')"
) -%>

Rails3 で RDoc ドキュメント生成時に invalid byte sequence in UTF-8 エラーが出た場合の対応

日本語でコメントを記述したプログラムを利用して RDoc ドキュメントを生成するときに、
下記のようなエラーが出て途中で止まってしまうことがあります。

$ rdoc -c UTF-8 -U
Parsing sources...
Before reporting this, could you check that the file you're documenting
compiles cleanly--RDoc is not a full Ruby parser, and gets confused easily if
fed invalid programs.

The internal error was:

        (ArgumentError) invalid byte sequence in UTF-8

uh-oh! RDoc had a problem:
invalid byte sequence in UTF-8

run with --debug for full backtrace

これは、ドキュメントを生成する際に RDoc の内部で 1024byte 毎にファイルを区切っていて、
1024byte の部分がちょうど日本語 UTF-8 の 3byte の中になってしまった場合にエラーになるようです。
実験として、下記のような model を作成してみました。

# coding: utf-8
#
#= エラー再現確認用モデル
#
# 1024byte の部分が日本語だった場合の実験用モデル
#


class Mail < ActiveRecord::Base
  #
  # 1024byte の部分が、日本語 UTF8 の 1 文字の途中だった場合は
  # rdoc -c UTF-8 -U を実行した際にエラーとなって rdoc が生成できないみたい。
  # この実験のために日本語をたくさん書いてエラーを再現してみようと思います。
  # ここまで書いてまだ半分の 512byte まで到達していなくてちょっとびっくり。
  # 意味の無い文書を長く書くことも結構難しいですね。
  # ここで色々なことを書いておけば SEO でヒットするキーワードが多くなるかなとか
  # 適当なことを考えながら淡々と文章を書き続けています。
  #
  # あとちょっとで 1024byte にまで届きそう!
  # がんばれ俺!負けるな俺!
  # この行がちょうど 1024byte になるのでここで打ち止め!
end

そして /usr/local/lib/ruby/gems/1.9.1/gems/rdoc-2.5.11/lib/rdoc/parser.rb の
def self.binary? に下記のような修正を加えてみます。

  def self.binary?(file)
    s = File.read(file, 1024) or return false
    set_encoding(s)

    p s # この行を状況確認用に追加

    if s[0, 2] == Marshal.dump('')[0, 2] then
      true
    elsif file =~ /erb\.rb$/ then
      false
    elsif s.scan(/<%|%>/).length >= 4 || s.index("\x00") then
      true
    elsif 0.respond_to? :fdiv then
      s.count("^ -~\t\r\n").fdiv(s.size) > 0.3
    else # HACK 1.8.6
      (s.count("^ -~\t\r\n").to_f / s.size) > 0.3
    end
  end

そして rdoc -c UTF-8 -U を実行すると下記のようなエラーで止まります。
(読みやすいように改行を加えています)

"# coding: utf-8\n#\n#= エラー再現確認用モデル\n#\n# 1024byte の部分が日本語だった場合の実験用モデル
\n#\n\n\nclass Mail < ActiveRecord::Base\n  #\n  # 1024byte の部分が、日本語 UTF8 の 1 文字の途中だっ
た場合は\n  # rdoc -c UTF-8 -U を実行した際にエラーとなって rdoc が生成できないみたい。\n  # この実験
のために日本語をたくさん書いてエラーを再現してみようと思います。\n  # ここまで書いてまだ半分の
512byte まで到達していなくてちょっとびっくり。\n  # 意味の無い文書を長く書くことも結構難しいですね 。\n
# ここで色々なことを書いておけば SEO でヒットするキーワードが多くなるかなとか\n # 適当なことを考えながら
淡々と文章を 書き続けています。\n  #\n  # あとちょっとで 1024byte にまで届きそう!\n  # がんばれ俺!
負けるな俺!\n  # この行がちょうど 1024byte になるのでここで打ち止\xE3"
Before reporting this, could you check that the file you're documenting
compiles cleanly--RDoc is not a full Ruby parser, and gets confused easily if
fed invalid programs.

The internal error was:

        (ArgumentError) invalid byte sequence in UTF-8

uh-oh! RDoc had a problem:
invalid byte sequence in UTF-8

run with --debug for full backtrace

rdoc 生成時の app/models/mail.rb のファイルサイズは 1034byte です。
なので utf-8 を 1 文字 3byteで最後から逆算していくと、
下記のように "打ち止め!" の "め" の部分がちょうど 1024byte 目から始まってしまっています。

byte       | char
----------------
1015〜1017 | 打
1018〜1020 | ち
1021〜1023 | 止
1024〜1026 | め
1027〜1029 | !
1030       | \n
1031       | e
1032       | n
1033       | d
1034       | \n

というわけで、こういったケースはプログラム内の先頭や途中に適当に半角文字を追加して、
日本語が 1024byte にぶつからないように微調整してあげることでエラーが出ないようになります。

あぁ、日本語ってめんどくさい、、、

Rails3 で Session を Memcache に入れる

Rails3 で Session を Memcache に入れるための手順です。
まずは、memcache-client をインストールするために Gemfile に下記の行を追加します。

gem 'memcache-client'

次に bundle install を実行して memcache-client をインストールします。

$ sudo bundle install
Fetching source index for http://rubygems.org/
   :
Installing memcache-client (1.8.5)
   :
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.

デフォルトでは Session は Cookie に保存するようになっているので、config/initializers/session_store.rb を下記のように修正して Memcache に Session を保存するように修正します。
(memcache_server にて指定している Memcache サーバは、開発環境・本番環境と切り替えたほうが良いと思いますので、config/environments/development.rb 等に適宜逃がしてください。)

Project::Application.config.session_store :mem_cache_store
Project::Application.config.session_options = {
  :cookie_only => false,
  :key => '_session',
  :memcache_server => 'localhost:11211'
}

これで Session は Memcache に保存されるようになります。
ですが、Session ID はまだ Cookie 上に保存されます。
これを URL パラメータとして引き回す場合は jpmobile を使用します。
これも memcache-client と同様に Gemfile に jpmobile の設定を追加して bundle install を実行します。

gem 'memcache-client'
gem 'jpmobile'
$ sudo bundle install
Fetching source index for http://rubygems.org/
   :
Using memcache-client (1.8.5)
Installing jpmobile (0.1.2)
   :
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.

あとは app/controllers/application_controller.rb に trans_sid :always を追記すれば Cookie を使用しないで Session を Memcache に保存できます。

class ApplicationController < ActionController::Base
  trans_sid :always
  protect_from_forgery
end

Rails3 で Ajax (link_to_remote + will_paginate 対応)

Rails3 で Ajax 対応をする場合は下記のような感じで view を書いて、リクエストを受け取る側の Controller 等でも JavaScript を書かないと動かないようです。

<%= link_to('参照',  {:action => 'show'}, :remote => true) %>

可能であればこれを勉強して Rails3 っぽく書きたいところなのですが、とりあえず Rails 2.x の link_to_remote に逃げる方法です。
(Rails3 の Ajax + RJS は後で勉強するつもりです(^^;)

まずは、prototype_legacy_helper を vendor/plugins に配置します。

$ cd vendor/plugins
$ git clone git://github.com/rails/prototype_legacy_helper.git

これで view 上で link_to_remote が動作するようになるので、下記のような感じで記述できます。

<%= link_to_remote(
  '参照',
  {
    :url => {:action => 'show'},
    :update => 'update_target',
    :loading => "showLoading('big', 'update_target')",
  }
) -%>

<div id="update_target">ここを書き換えたい</div>

サーバからのレスポンスを待っている間は Alax っぽくロード中の画像を出したいので、:loading で showLoading を呼び出しています。
これは、下記の JavaScript を public/javascripts/application.js 等に書いておきます。

/*
 * ロード画面を出力する
 */

function showLoading(load_image_size, load_target)
{
  // 画像オブジェクトの作成
  img_element = document.createElement('img');
  img_element.src = load_image_size == 'big' ?
                    '/images/ajax-loader.gif' :
                    '/images/ajax-loader-small.gif';
  img_element.align = 'center';

  // センタリングオブジェクト
  center_element = document.createElement('center');
  center_element.appendChild(img_element);

  // 画像を表示するメソッド
  var output_loading = function(load_image, target_place) {
    target = document.getElementById(target_place);
    while (target.hasChildNodes()) {
      target.removeChild(target.firstChild);
    }
    target.appendChild(load_image);
  }

  // ロード画像を表示する場所が複数か否かの分岐
  if (load_target instanceof Array) {
    for (var i = 0; load_target[i]; i ++) {
      output_loading(center_element, load_target[i]);
    }
  } else {
    output_loading(center_element, load_target);
  }
}

/images/ajax-loader.gif や /images/ajax-loader-small.gif などの画像は Ajaxload - Ajax loading gif generator で自分好みの画像を作成すると良い感じになります。
これで link_to_remote での Ajax リクエストが実行できるようになります。
FORM から Ajax リクエストを実行したい場合は form_remote_tag を使用すれば、同様の動作を行うことが可能です。


ただし、prototype_legacy_helper は will_paginate に対応していないので別途対応が必要になります。
まず、app/helpers/remote_link_renderer.rb に下記のような内容を記述します。
(こちらは、id:donghai821 さんの記事を参考にさせていただきました)

class RemoteLinkRenderer < WillPaginate::LinkRenderer
  private

  def link(text, target, attributes = {})
    if target.is_a? Fixnum
      page = target
      target = url(target)
      target[:url][:page] = page
    end

    @template.link_to_remote(text, target, attributes)
  end

  def url(page)
    @base_url_params ||= begin
      url_params = base_url_params
      merge_optional_params(url_params)
      url_params
    end

    url_params = @base_url_params.dup
    add_current_page_param(url_params, page)

    return url_params
  end
end

次に will_paginate を下記のように記述すると、ページ送りも Ajax で動作するようになります。

<%= will_paginate(
  @users,
  :renderer => RemoteLinkRenderer,
  :params => {
    :url => {:action => 'show'},
    :update => 'update_target',
    :loading => "showLoading('big', 'update_target')",
  },
  :previous_label => '<-',
  :next_label => '次 ->'
) -%>

Rails3 で ActiveRecord にて取得した UTF-8 の日本語を view に表示する

ActiveRecord にて取得した UTF-8 の日本語の情報を view で表示する方法です。
たとえば、Controller を下記のように記述します。

# coding: utf-8

class UserController < ApplicationController
  def index
    User.create({
      :name => 'てすと',
    })
    @user = User.find(:first)
  end
end

そして、view を下記のように記述します。

<h1>日本語表示のテスト</h1>

<%= @user.name %>

すると、下記のようなエラーが出て日本語を表示することが出来ません。

incompatible character encodings: UTF-8 and ASCII-8BI

これは、日本語を ActiveRecord 上では ASCII-8BI で扱い view 上では UTF-8 で扱っているため、違う文字コードの日本語を連結することは不可能であるというエラーのようです。
そのため view に日本語を一切書かなければ正常に表示することが出来ますが、そういうわけにもいきません。
単純に回避するのであれば、下記のように view 上で日本語を UTF-8 に強制的に encode してあげればエラーは出ません。

<h1>日本語表示のテスト</h1>

<%= @user.name.force_encoding('UTF-8') %>

ですが、全てのデータ出力に force_encoding を付けていくのは DRY ではありません。
そこで app/helpers/application_helper.rb に下記のように記述すると force_encoding をしなくても日本語を表示できるようになります。

module ActionView
  class OutputBuffer < ActiveSupport::SafeBuffer
    def <<(value)
      super(value.to_s.force_encoding('UTF-8'))
    end
    alias :append= :<<
  end
end

■お詫び
 ActionView のオーバーライドは最初は config/environment.rb に記述してくださいと書いていましたが、設定を読み込む順番によってはオーバーライドされない現象が確認できました。
 こちらは、app/helpers/application_helper.rb に記述することで動作することが確認できましたので、修正させていただきました。

Rails3 で RSpec + RCov 日本語対応

Rails3 でプロジェクトを作成し、RSpecRCov を日本語で動作させるまでの手順です。
rails (3.0.0) や rspec (2.0.0.beta.22) 等の必要な gem は既にインストールされているものとします。

まずは、普通に Rails プロジェクトを作成して、config/database.yml を開発環境に合わせて設定しておきます。

$ rails new project
      create
      create  README
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
     :
     :

プロジェクト内にて RSpecRCov を使用するために Gemfile に下記を追記します。

gem "rcov"
group :test do
  gem 'rspec-rails', '>= 2.0.0.beta', :group => :development
end

プロジェクトのテストフレームワークRSpec にするために
config/application.rb に下記のような修正を加えます。

module Project
  class Application < Rails::Application
    config.generators do |g|
      g.test_framework :rspec
    end
  end
end

RSpec テスト用の設定ファイルを設置します。

$ ./script/rails generate rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  autotest
      create  autotest/discover.rb

ここまで基本的な準備は完了です。
controller などを作成すると、同時に rspec 用のテストファイルが生成されます。

$ ./script/rails generate controller user index
      create  app/controllers/user_controller.rb
       route  get "user/index"
      invoke  erb
      create    app/views/user
      create    app/views/user/index.html.erb
      invoke  rspec
      invoke  helper
      create    app/helpers/user_helper.rb
      invoke    rspec
      create      spec/helpers/user_helper_spec.rb

これで RSpec を実行する環境はできましたので、下記のようにテストを書いてみます。
1 行目の "# coding: utf-8" を記述しておくことで、テスト内容を日本語 UTF-8 で記述して実行することが可能になります。

# coding: utf-8

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe UserController do
  describe 'index method' do
    it 'GET "index" リクエストが成功すること' do
      get 'index'
      response.should be_success
    end
  end
end

次に RSpec で記述したテストのカバレッジを確認するために RCov を実行したいのですが、
普通に rake spec:rcov を実行すると、下記のようなエラーが出て実行できません。
これは、RSpec テスト内の日本語 UTF-8 が問題で文字を正常に認識できていないために発生する問題のようです。

$ rake spec:rcov
(in /Users/nedate/svn/project)
/usr/local/bin/ruby -S bundle exec rcov --exclude /gems/,/Library/,/usr/,lib/tasks,.bundle,config,/lib/rspec/,/lib/rspec- "./spec/controller/user_spec.rb"
** WARNING: Ruby 1.9 Support is experimental at best. Don't expect correct results! **
/usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/file_statistics.rb:115:in `block in is_code?': invalid byte sequence in US-ASCII (ArgumentError)
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/file_statistics.rb:112:in `each'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/file_statistics.rb:112:in `each_with_index'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/file_statistics.rb:112:in `is_code?'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/file_statistics.rb:234:in `extend_heredocs'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/file_statistics.rb:40:in `initialize'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/formatters/base_formatter.rb:50:in `new'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/formatters/base_formatter.rb:50:in `add_file'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/code_coverage_analyzer.rb:143:in `block (2 levels) in dump_coverage_info'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/code_coverage_analyzer.rb:142:in `each'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/code_coverage_analyzer.rb:142:in `block in dump_coverage_info'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/code_coverage_analyzer.rb:135:in `each'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/lib/rcov/code_coverage_analyzer.rb:135:in `dump_coverage_info'
        from /usr/local/lib/ruby/gems/1.9.1/gems/rcov-0.9.9/bin/rcov:433:in `block in <top (required)>'
rake aborted!
Command failed with status (1): [/usr/local/bin/ruby -S bundle exec rcov --...]

(See full trace by running task with --trace)

これは、spec/spec_helper.rb に下記のように記述することで回避できます。

#
# quick monkey patch for rcov
#
# http://codefluency.com/post/1023734493/a-bandaid-for-rcov-on-ruby-1-9
#
if defined?(Rcov)
  class Rcov::CodeCoverageAnalyzer
    def update_script_lines__
      if '1.9'.respond_to?(:force_encoding)
        SCRIPT_LINES__.each do |k,v|
          v.each { |src| src.force_encoding('utf-8') }
        end
      end
      @script_lines__ = @script_lines__.merge(SCRIPT_LINES__)
    end
  end
end