Rails console で Rails routes を詳しく確認する方法

## Rails routes ともっと仲良く 

Rails routes を探索する初心者RailsプログラマのためのTipsです。

- Lv.0 ```config/routes.rb``` と ```rake routes```
- Lv.1 rails consoleで モデルからどのURLが生成されるか調べる
- Lv.2 rails consoleで URL 文字列からどのコントローラーが応答するのか調べる
- Lv.3 デフォルトのWelcome page
- Lv.4 全てにマッチするroutesを書く
- Lv.5 サブドメイン

### Lv.0 ```config/routes.rb``` と ```rake routes```

```config/routes.rb
Rails.application.routes.draw do
  # comments...
end
```

rails new 直後の ```config/routes.rb``` はろいろ書いてあるけど、全てコメントなので、ルートの定義は何も書いてない状態ですよね。

定義を追加して(```resources :groups```みたいな)確認する方法はターミナルからおなじみの

```
> ./bin/rake routes
```

Lv.0の状態だと、多分 ```rake routes``` が使えるだけなので、ちょっとroutesが混みいってきたり、狙ったroutesになってない！？となった時のデバッグがツライ・・

ここから徐々にレベルを上げていきます！

### Lv.1 rails consoleで モデルからどのURLが生成されるか調べる

説明のため、Groupというモデルがあるとする。

```config/routes.rb
Rails.application.routes.draw do
  resources :groups
end
```

モデルからどのパスが生成されるか知りたい時

```
> ./bin/rails console

irb> group = Group.first
irb> app.polymorphic_path(group)
=> "/groups/1"
```

↓polymorphic_pathでいろいろ調べる方法は以前書いたのでスキップ

[Rails でリンクパスを生成する方法色々・・とRails console で 生成される path を確認したい時 - Qiita](http://qiita.com/mm36/items/f266977e12df9d1dc548)

### Lv.2 rails consoleで URL 文字列からどのコントローラーが応答するのか調べる

Lv.1の逆をやりたい時。

"/groups/1" -> GroupsController#show

が知りたい。

```
> ./bin/rails console

irb> > Rails.application.routes.recognize_path('/groups/1')
=> {:controller=>"groups", :action=>"show", :id=>"1"}
```

[Recognize routes in rails console Session - Stack Overflow](http://stackoverflow.com/questions/2778074/recognize-routes-in-rails-console-session)

### Lv.3 デフォルトのWelcome page

Lv.2の方法で調べてみると、```/``` をどのこが担当してるのかわかる。

```
> ./bin/rails console

irb> > Rails.application.routes.recognize_path('/')
=> {:controller=>"rails/welcome", :action=>"index"}
```

明示的に ``` root to:``` を指定してない場合、 デフォルトのあの赤いページ (Welcome aboard) が出てくる

自分のコントローラーに対応させる場合

```config/routes.rb
Rails.application.routes.draw do
  resources :groups
  root to: 'groups#index'
end
```

```
> ./bin/rails console

irb> > Rails.application.routes.recognize_path('/')
=> {:controller=>"groups", :action=>"index"}
```

**rails/welcomeが応答してくれるのは、routes.rbに記載されたルールのうち、どれもマッチせず終了した場合**

### Lv.4 全てにマッチするroutesを書く

/以外のパスは、routes.rbどれにもマッチせずに応答するコントローラーないやん、となった場合、ルーティングのエラーが出る。

development環境なら、赤いRouting Errorのページ。

これを一旦キャッチして、なんかしたい場合、マッチしなかったルートを担当するメソッドに渡すルールを追加する。

```config/routes.rb
Rails.application.routes.draw do
  resources :groups
  root to: 'groups#index'
  get '*unmatched_route', to: 'application#unmatched'
end
```

**ルーティングのルールは上から順にマッチするので、全部を受け取るようなルールは一番下に書くこと！**

```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # ...
  def unmatched
    # なんかいろいろする
    # リダイレクト先があればリダイレクト
    redirect_to your_redirect_path if need_redirect?
    # ルーティングエラーを出すときはこんな感じ
    raise ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
  end
end
```

上の雑な例では ```need_redirect?``` で何かのルールを判定してるものとする。
どのルーティングでここまで来ちゃったかは、受け取ったコントローラーのパラメータでわかる
``` params[:unmatched_route] ```
(config/routes.rbで書いた```get '*unmatched_route'```と一致）

#### 蛇足

特にコントローラーで判断するべきルールがなくて、一律にリダイレクトすれば良い場合は、routes.rbの記載だけで対応できる。

```config/routes.rb
  get '*unmatched_route' => redirect('/')
```

### Lv.5 サブドメイン

全てのルーティングを ```www``` サブドメインの下で使いたい、場合。

```config/routes.rb
Rails.application.routes.draw do
  constraints subdomain: 'www' do
    resources :groups
    root to: 'groups#index'
  end
  get '*unmatched_route', to: 'application#unmatched'
end
```

自分で作ってる全てのルーティングを ```www``` 下に置いてみました。
```www``` なしでアクセスされた時、```www``` 付きにリダイレクトするようにしてみます。


```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # ...
  def unmatched
    raise_routing_error if request.subdomain.present?
    url_to_redirect = root_url + params['unmatched_route'].to_s
    route_hash = Rails.application.routes.recognize_path(url_to_redirect)
    raise_routing_error if route_hash[:controller] == 'application' && route_hash[:action] == 'unmatched'
    redirect_to url_to_redirect
  end

  private

  def raise_routing_error
    raise ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
  end
end
```

これで、```http://your.domain/groups``` -> ```http://www.your.domain/groups``` にリダイレクトできるようになります。

#### 残る問題点・・

やったーできた〜〜と思ったんですが・・実は問題点が残っています。

```http://your.domain/``` にアクセスすると、亡霊のようにデフォルトの Welcome aboard が残っているのです・・・

Lv.2で得たスキルで確認してみます。

```
> ./bin/rails console

irb> > Rails.application.routes.recognize_path('/')
=> {:controller=>"rails/welcome", :action=>"index"}
```

きゃーー；；
これが出てくるということは、どのルールにもマッチしていない、ということ。

routes.rbの一番最後に書いたはずの、
```get '*unmatched_route', to: 'application#unmatched'```
はどうしちゃったのか・・・


#### 解決策

```get '(*unmatched_route)', to: 'application#unmatched'```

マッチするパスに()を付けて省略可にすると、全てマッチするようになる。

### 補足

```recognize_path``` の使い方を調べている途中で、

> ```recognize_path``` is private API and should not be used in applications.

との記載を発見・・　（[参考](https://github.com/rails/rails/pull/21494)）
あんまり積極的に利用すべきではなさそう・・・

#### 修正してみた

```app/controllers/application_controller.rb
  def unmatched
    raise_routing_error if request.subdomain.present?
    redirect_to root_url + params['unmatched_route'].to_s
  end
```

recognize_pathを使って、リダイレクト後に対応できるコントローラーがいるかどうかのチェックを外してみた。
サブドメインありで```unmatched``` まできた場合は、どこにも転送できないのでエラーに。
それ以外の場合はとりあえず www 付きのURLに転送するように。
（リダイレクト後にまた```unmatched```に来て、404になるという無駄が発生するけど・・）

### 補足2

一律www付きにリダイレクト、とかだと、Nginxとかでやることのが多いのかも。。（なので後半のTipsは参考までに。。）