Personal tools
You are here: Home ブログ 井上 Railsで簡単にできそうで、できなかったこと
« December 2010 »
Su Mo Tu We Th Fr Sa
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
Recent entries
Apache2.4のリリース予定は来年(2011年)初め(あくまで予定) inoue 2010-12-23
Herokuの発音 inoue 2010-12-20
雑誌記事「ソフトウェア・テストPRESS Vol.9」の原稿公開 inoue 2010-12-18
IPA未踏のニュース inoue 2010-12-15
労基法とチキンゲーム inoue 2010-12-06
フロントエンドエンジニア inoue 2010-12-03
ASCII.technologies誌にMapReduceの記事を書きました inoue 2010-11-25
技術評論社パーフェクトシリーズ絶賛発売中 inoue 2010-11-24
雑誌連載「Emacsのトラノマキ」の原稿(part8)公開 inoue 2010-11-22
RESTの当惑 inoue 2010-11-22
「プログラマのためのUXチートシート」を作りました inoue 2010-11-19
「ビューティフルコード」を読みました inoue 2010-11-16
Categories
カテゴリなし
 
Document Actions

Railsで簡単にできそうで、できなかったこと

Railsはデフォルトで、http://www.foo.com/my/action のURLでアクセスした時、MyControllerのactionメソッドを実行します。また、http://www.foo.com/my/ のURLでアクセスすると、MyControllerのindexメソッドを実行します。

やりたかったことは、MyControllerのメソッド(仮にメソッド名をdoだとします)をデフォルトアクションにしたい、でした。つまり、http://www.foo.com/my/ にアクセスした時にdoメソッドを実行するように変更したかったのです。

一番安易なのは、indexメソッドとdoメソッドをごっそり差し替える方法(メソッド名を交換)です。命名規約に従うと、ビュー側のindex.html.erbとdo.html.erbのふたつのファイルの中身も交換しなければいけません。この方法でやりたいことが実現できることは分かっていますが、次の理由でやりたくありませんでした。

  • subversionで以下を同時に実行して履歴が狂わないのか自信が無かった
svn mv index.html.erb do.html.erb
svn mv do.html.erb index.html.erb

要は、http://www.foo.com/my/ にアクセスした時にindex以外のメソッドを実行できればいいだけなので、もっと簡単にできそうです。実際、これは簡単でした。次のようにconfig/routes.rbに書き足すだけで実現できました。routes.rbをいじると色々とカオスになりそうなのであまり変更したくありませんが、これぐらいなら許容範囲だと判断しました。

ActionController::Routing::Routes.draw do |map|
  map.connect 'my', :controller => 'my', :action => 'do'   # 追加
  # 以下、デフォルトからあるコード
  map.connect ':controller/:action/:id'      # もし、ここに :action=>'do' を書き足すと、すべてのコントローラのデフォルトメソッドが変わる
  map.connect ':controller/:action/:id.:format'
end

これで一件落着のようですが、残念ながら話は終わりません。

RailsではURL生成のために url_forメソッドを使います。次のようにurl_forメソッドを呼び出すと

url_for :controller => 'my', :action => 'foo'

'http://www.foo.com/my/foo' の文字列を返します。

url_for :controller => 'my', :action => 'index'

は、'http://www.foo.com/my' の文字列を返します。routes.rbに上の設定をした場合、'http://www.foo.com/my/index' を返してくれないと困ります。

結論から言うと、Railsのコードを読んでも、url_forがどこで 'index' を特別扱いして上の結果になるのか分かりませんでした。

結局、次のようにlistメソッドをindexメソッドのalias指定をして回避しています(微妙な解決ですが)。url_for :controller => 'my', :action => 'index' は使わずに、url_for :controller => 'my', :action => 'list' を使うようにしました。

class MyController < FacebookAbstractController
  省略
  alias list index
end

以下、url_forメソッドの中を追った記録です。

url_forメソッドはActionViewモジュールにもありますが、結局、以下のActionControllerモジュールのurl_forメソッドに来ます。

[action_controller/base.rb]
def url_for(options = nil)
  case options || {}
    when String
      options
    when Hash
      @url.rewrite(rewrite_options(options))
    else
      polymorphic_url(options)
  end
end

case文のwhen節にクラス名を書く技法を初めて知りました。半端でもコードリーディングは役に立ちます。

脱線して少し実験(case文の一致のためには === が使われます)。

irb> s = 'x'
irb> s === String  #=> false
irb> String === s  #=> true
# 同値演算子のように見える ===メソッドですが、交換則は成り立ちません
# ここから分かることは、when節に書いた式の評価値に対して === メソッド呼び出しで、評価値が ===メソッドの引数ではないことです

irb> String.method(:===)   #=> "#<Method: Class(Module)#===>"
# String::=== の実体は Module::=== であることが分かります。
# Module::=== のリファレンスを見ると、
#   Returns true if anObject is an instance of mod or one of mod‘s descendents.
# と書いてあります

閑話休題。

@url.rewrite(rewrite_options(options)) から呼ぶ rewrite_optionsメソッドは無視します。@urlは、次のコードがbase.rb内にあるのでUrlRewriterオブジェクトであることが分かります。変数に型があれば、定義部分を見ればいいのに、代わりに代入部分を探して当たりをつけるのが泥臭くて嫌です。

[action_controller/base.rb]
def initialize_current_url
  @url = UrlRewriter.new(request, params.clone)
end

UrlRewriterクラスのrewriteメソッドのコードを見ます。

[action_controller/url_rewriter.rb]
def rewrite(options = {})
  rewrite_url(options)
end

rewrite_urlメソッドの実装を見ると、中でrewrite_pathといういかにもな名前のメソッドを呼んでいます(URLから/indexが省略される動作は、URLの中のパス部分に関係する処理のはずです)。rewrite_pathメソッドの実装を見ると、最後に次のような処理があります。

[action_controller/url_rewriter.rb]
def rewrite_path(options)
  省略 
  Routing::Routes.generate(options, @request.symbolized_path_parameters)
end

と言うわけでRoutingモジュールのコードへ。routing.rbの中の断片を引用すると

[action_controller/routing.rb]
module Routing
  省略
  class RouteSet
    省略
    def generate(options, recall = {}, method=:generate)
      省略
    end
  end
  省略
  Routes = RouteSet.new
end

この技法がどれほど一般的か不明ですが、こうして外部からRouting::Routes.generateのように呼べます。

generateメソッドの途中は省略しますが、メソッド本体の最後が次のようになっています。

[action_controller/routing.rb]
def generate(options, recall = {}, method=:generate)
  省略
  routes = routes_by_controller[controller][action][options.keys.sort_by { |x| x.object_id }]
  routes.each do |route|
    results = route.send!(method, options, merged, expire_on)
    return results if results && (!results.is_a?(Array) || results.first)
  end
end

Routeオブジェクトに処理が委譲されているようです。class Routeの定義はmodule Routing内にあります。だいぶ本丸に近づいている気配です。

しかしここから先が分かりません(メソッドの本体をコード生成しています)。どこに'index'依存があるのか分かりません。class PathSegment は怪しいのですが、つかみきれません。コードリーディングには自信があったのですが、まだまだ修行が足りません。

The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/inoue/rails-hard/tbping

Re:Railsで簡単にできそうで、できなかったこと

Posted by Rust/OGAWA at 2009-01-29 12:21
ちゃんと見れてないのですが,RoutingBuilder あたりでなにかしらやっているようです.
Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.