Padrino Frameworkのルーティングの仕組みが変わります

# Padrino Frameworkのルーティングの仕組みが変わります

## はじめに

タイトルのとおり、Padrino 0.13.0からはコアのルーティング実装が新しいものに替わります。
この記事では、0.13.0から追加された新機能や変更について解説します。

ルーティングの実装については、[Padrino Frameworkのルーターを開発した話](http://qiita.com/namusyaka/items/ded96dc44489e2f8adfb)をご覧ください。

## 新機能

### プリコンパイル機能

新しいルーターでは、目的のルートを高速に探索するために、一度全てのルートを正規表現にコンパイルします。
この機能は、その処理を予めサーバ起動時に行っておくことで、初回リクエスト時のオーバーヘッドを減らすことを目的とするものです。
以下に示すコードをアプリケーション直下、あるいは`config/apps.rb`の`configure_apps`ブロック内に追加する事で、この機能が有効になります。

```ruby
set :precompile_routes, true
```

## Sinatraとの互換性の修正

### `*`を含むルートにおいて、`*`に相当する値を取得できない問題の修正

`*`に相当する値を取得する方法がありませんでしたが、ブロックパラメータ、もしくは`params[:splat]`から値を取得することができます。

```ruby
get "/a/*" do |b|
  "#{b}, #{params.inspect}"
end

get "/b/*" do
  params.inspect
end

# 0.12.4
get "/a/123" #=> ArgumentError
get "/b/123" #=> {}
# 0.13.0
get "/a/123" #=> "123, {"splat"=>["123"]}"
get "/b/123" #=> {"splat"=>["2"]}
```

### `*`を含むルートのパスの展開ができない問題の修正

`url`や`url_for`メソッドを使ったパスの展開ができるようになりました。

```ruby
get :splatter, "/a/*" do
  url(:splatter, splat: "123")
end

get :splatter2, "/b/*/*" do |c, d|
  url(:splatter2, splat: [c, d])
end

# 0.12.4
get "/a/123" #=> "/a?splat=123"
# 0.13.0
get "/a/123" #=> "/a/123"
get "/b/123/456" #=> "/c/123/456"
```

### `*`を含むルートに対するリクエストにおける、PATH_INFOの末尾スラッシュの有無による挙動の修正

Sinatraの挙動に合わせることにしました。
**この機能に依存したルーティングは修正が必要になります。**

```ruby
get("/asdf/*"){ "splat" }

# 0.12.4
get "/asdf/" #=> "splat"
get "/asdf" #=> "splat"

# 0.13.0
get "/asdf/" #=> "splat"
get "/asdf" #=> 404
```

### 正規表現ライクなクラスを用いたルートのサポート

なんのこっちゃという感じですが、要はダックタイピングのサポートです。
ただしSinatraのそれとは少し仕様が異なるので注意が必要です。

Sinatraでは、以下に示すようなクラスを用いたルーティングが可能です。

```ruby
class RegexpLookAlike
  class MatchData
    def captures
      ["this", "is", "a", "test"]
    end
  end

  def match(string)
    ::RegexpLookAlike::MatchData.new if string == "/this/is/a/test/"
  end

  def keys
    ["one", "two", "three", "four"]
  end
end

get RegexpLookAlike.new do
  "this is a test"
end
```

Padrinoでは、代わりに以下に示すようなクラスが必要になります。

```ruby
class RegexpLookAlike
  NAMES = ["this", "is", "a", "test"]
  class MatchData
    def captures
      ["this", "is", "a", "test"]
    end

    def names
      NAMES
    end
  end

  def names
    NAMES
  end

  def to_s
    "/this/is/a/test/"
  end

  def match(string)
    ::RegexpLookAlike::MatchData.new if string == "/this/is/a/test/"
  end
end

get RegexpLookAlike do
  "this is a test"
end
```

見てわかるとおり、`MatchData#names`、`RegexpLookAlike#names`、`RegexpLookAlike#to_s`が新たに必要となります。

プリコンパイル機能に関する説明で触れたように、一度ルートを正規表現にコンパイルする必要があるため、
`to_s`はそのための正規表現、あるいは文字列を返す必要があります。

[mustermann](https://github.com/rkh/mustermann)などと組み合わせて作っていくと良いかもしれません。

### `pass` with ブロックが動かない問題の修正

passing自体は有名ですが、ブロックを渡すことでそれを評価した値をレスポンスボディとして扱うことができる、というような機能もあります。

```ruby
get "/" do
  pass { "passed" }
  "index"
end

get "/" #=> "passed"
```

ただし、ブロック付きpassを実行したことで次のルートにマッチし、そのルートが正しくレスポンスを返した場合にはpassに渡されたブロックは無視されます。

```ruby
get "/" do
  pass { "passed" }
  "index"
end
get "/" do
  "index2"
end

get "/" #=> "index2"

get "/" do
  pass { "passed" }
  "index"
end
get "/" do
  pass
  "index2"
end

get "/" #=> "passed"
```

### ルートブロック内で`request`インスタンスが変更された際のルーティングの挙動の修正

ルートブロック内で`request`インスタンスを変更すると、その変更を考慮したルーティングが行われるようになるというものです。
`pass`と組み合わせて使うことが想定されます。

```ruby
get "/foo" do
  request.path_info = "/bar"
  pass
end

get "/bar" do
  "bar"
end

get "/foo" #=> "bar"
```

## 変更点

### `(.:format)` -> `(.:format)?`

`:provides`オプションを使った際に、以前までのPadrinoはパスに対して`(.:format)`というキャプチャ用パラメータを追加していましたが、
0.13.0からは`(.:format)?`という指定に変更されます。
これは、Sinatra 2.0のOptional Groupの挙動を模倣したものであり、Padrinoはこの部分において、Sinatra 2.0を先取りしているといえます。
といっても、`:provides`オプションを介して使っている人はあまり意識する必要はないでしょう。

### ブロックパラメータの数に関するサポートの追加

[Sinatra](https://github.com/sinatra/sinatra/blob/e727b17930f0b3c1a4e666c5901d45d04f68a2d6/lib/sinatra/base.rb#L1608-1610)や[Padrino](https://github.com/padrino/padrino-framework/blob/efdf4fd14cf4de2d8a5caa92e3e7563accf5995c/padrino-core/lib/padrino-core/application/routing.rb#L508-512)はルートに渡すブロックの仮引数の有無で、実引数を渡すか否かを決定します。
今回のアップデートでは、仮引数が存在していた場合に、仮引数の数と実引数の数が一致しなければPadrinoが用意した例外を発生させるように変更されました。

また、これに伴い0.13.0以前のものとブロックパラメータの扱いに関する挙動が変わっているため、解説します。
以下に示すコードは、0.12.4では拡張子が省略されているケースで正常に動作しているように見えますが、拡張子が渡されるとArgumentErrorが発生します。
0.13.0では、拡張子の有無にかかわらず`Padrino::Routing::BlockArityError`が**アプリケーションを起動するタイミングで**発生するようになりました。

```ruby
get "/:param1/:param2", provides: :xml do |param1, param2|
  "#{param1}, #{param2}"
end

# 0.12.4
get "/123/456" #=> "123, 456"
get "/123/456.xml" #=> ArgumentError
# 0.13.0
get "/123/456" #=> Padrino::Routing::BlockArityError
get "/123/456.xml" #=> Padrino::Routing::BlockArityError
```

実際には以下に示すように、仮引数を三つ用意しておくべきです。

```ruby
get "/:param1/:param2", provides: :xml do |param1, param2, format|
  "#{param1}, #{param2}, #{format}"
end

# 0.12.4
get "/123/456" #=> ArgumentError
get "/123/456.xml" #=> "123, 456, xml"
# 0.13.0
get "/123/456" #=> "123, 456, "
get "/123/456.xml" #=> "123, 456, xml"
```

## おわりに

0.13.0を正式にリリースする前に、何度かbetaリリースが行われることになると思います。(0.13.0.rc1 など)。
是非、Padrinoを利用している方はbetaリリース期間中に自分の環境で新しいバージョンをテストしていただき、不具合等があれば報告していただければと思います。
よろしくお願いします。

* [padrino/padrino framework issues](https://github.com/padrino/padrino-framework/issues)
* [Padrino Framework JP idobataチャット](https://idobata.io/organizations/padrino-framework-jp/rooms/public/join_request/5b2dda52-c818-45f2-82d6-04c891cb2350)
* [namusyaka@twitter](https://twitter.com/namusyaka)

**この記事は、0.13.0のリリースまで随時更新していきます。**