Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 中編(翻訳)

概要

MITライセンスに基いて翻訳・公開いたします。


roda.jeremyevans.net/より

長いので3本に分割します。
本記事では、原則としてroutesやroutingは「ルーティング」、rootは「ルート」と表記します。

Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 中編(翻訳)

オプションのセグメント

Rodaではオプションのセグメントをさまざまな方法で扱えます。たとえば、/items/123/items/123/456のどちらも受け付けたいとしましょう(123はitemのid、456は何らかのオプションデータ)。

最も単純な方法は、共有したブランチ(branch: 枝)で2つの独立したルーティングとして扱うことです。

r.on "items", Integer do |item_id|
  # ブランチで使う共有コードがここにあるとする

  # /items/123/456
  r.is Integer do |optional_data|
  end

  # /items/123
  r.is do
  end
end

これは多くのケースで使えますが、これをオプションのセグメントを持つ1つのルート(route)としてどうしても扱いたいということもあります。シンプルな方法の1つとして、オプションセグメントの代わりにパラメータを使う手があります(+/items/123?opt=456+など)。

r.is "items", Integer do |item_id|
  optional_data = r.params['opt'].to_s
end

しかし、オプションセグメントをどうしても使いたい場合、マッチャーを使う方法がほかにいくつもあります。そのひとつが、最後の要素がtrueの配列マッチャーを使うことです。

r.is "items", Integer, [String, true] do |item_id, optional_data|
end

技術的には、オプションセグメントが渡されなかった場合、引数を(2つではなく)1つだけyieldする点にご注意ください。

他の方法として、正規表現で実装する手もあります。

r.is "items", /(\d+)(?:\/(\d+))?/ do |item_id, optional_data|
end

マッチブロックやルートブロックからの戻り値

response.writeを直接呼んだことでレスポンスのbodyが既に書き込まれている場合、マッチブロック(match block)やルートブロック(route block)の戻り値はすべて無視されます。

レスポンスのbodyがまだ書き込まれていない場合、マッチブロックやルートブロックの戻り値がinspectされます。

String
レスポンスのbodyとして使われる
nilfalse
無視される
その他すべて
エラーをraise

プラグインでは、マッチブロックの戻り値やルートブロックの戻り値の追加をサポートします。JSONプラグインを例に取ると、マッチブロックやルートブロックで配列やハッシュを返せるようになり、これらを直接JSONに変換してレスポンスのbodyとして使えます。

ステータスコード

レスポンスを確定する段階で、ステータスコードが手動で設定されておらず、レスポンスに何らかの書き込みが行われている場合、レスポンスのステータスコードには200が使われます。それ以外の場合のステータスコードは404になります。これによって「驚き最小の原則」を実現します。アクションがどこでも扱われていない場合のレスポンスは404が前提になります。

レスポンスのstatus属性を使って、いつでも手動でステータスコードを設定できます。

route do |r|
  r.get "hello" do
    response.status = 200
  end
end

リダイレクトの場合、レスポンスのステータスコードはデフォルトで302になります。このステータスコードはr.redirectに渡す2番目の引数で変更できます。

route do |r|
  r.get "hello" do
    r.redirect "/other", 301 # use 301 Moved Permanently
  end
end

verb(動詞)メソッド

上述のように、RodaにはHTTPリクエストメソッドを元にマッチングを行うr.getメソッドとr.postメソッドがあります。これ以外のHTTPリクエストメソッドとマッチさせたい場合は、all_verbsプラグインを使います。

引数なしで呼ぶと、リクエストに適切なメソッドがある限りマッチしたと判断されます。

 r.get do end

上はあらゆるGETリクエストにマッチします。

r.post do end

上はあらゆるPOSTリクエストにマッチします。

メソッドに引数を渡す場合は、「リクエストメソッドとのマッチ」と「すべての引数とのマッチ」と「パスが完全に引数とマッチ」がすべて満たされる場合にのみマッチと判断されます。

r.post "" do end

上は、現在のパスが/POSTリクエストにだけマッチします。

r.get "a/b" do end

上は、現在のパスが/a/bGETリクエストにだけマッチします。

このように振る舞いを変えてある理由は、引数をまったく渡さないということは「おそらく現在のパスと完全一致するかどうかのテストまでしたくなんかないだろう」と考えたためです。「いやいや、それテストしたいから」というのであれば、引数にtrueを渡してください。

r.on "foo" do
  r.get true do #「GET /foo」にマッチ、「GET /foo/.*」にはマッチしない
  end
end

リクエストメソッドにマッチさせ、かつリクエストパスについては部分マッチだけ行いたい場合は、r.on:methodハッシュマッチャーを使う必要があります。

r.on "foo", method: :get do # Matches GET /foo(/.*)?
end

ルート(root)メソッド

上述のように、r.rootをマッチメソッドとして使うこともできます。このメソッドは、現在のパスが/であるGETリクエストにマッチします。r.rootr.get ""と似ていますが、/の指定に余分な場所を使わないで済む点が異なります。

他のマッチメソッドと異なり、r.rootは引数を取りません。

r.rootは、パスが空の場合にはマッチしない点にご注意ください。その場合はr.get trueを用いるべきです。空のパスと/の両方にマッチさせたい場合は、r.get ["", true]とするか、slash_path_emptyプラグインを使います。

r.rootGETリクエストでないとマッチしない点にご注意ください。POST /リクエストを扱うには、r.post ''を使います。

リクエストとレスポンス

リクエストオブジェクトはrouteブロックにyieldされますが、requestメソッドで取得することもできます。同様に、レスポンスオブジェクトはresponseメソッドで取得できます。

リクエストオブジェクトは、Rack::Requestのサブクラスのインスタンスにいくつかのメソッドを追加したものです。

リクエストオブジェクトやレスポンスオブジェクトを別のモジュールで拡張【チェック】したい場合は、module_includeプラグインを利用できます。

「汚染」について

Rodaでは、さまざまな手をつくしてrouteブロックのスコープの汚染を回避しようとしています。このため、Rodaがアプリのコードで名前空間の問題を引き起こす可能性は小さいはずです。Rodaでは以下を含む対策を取っています。

  • routeブロックのスコープでデフォルトで定義されるインスタンス変数は、@_request@_responseしかありません。Rodaに付属するプラグインがrouteブロックのスコープで用いるインスタンス変数は、すべて冒頭にアンダースコア_が追加されます。
  • Objectのデフォルトメソッドの他に)定義されるメソッドは、次のものしかありません: callenvoptsrequestresponsesession
  • Roda名前空間内の定数は、すべてRodaで始まります(例: Roda::RodaRequest)。

コンポジション

(別のRodaアプリを含む)任意のRackアプリとそのミドルウェアをRodaアプリ内部にマウントしてr.runを利用できます。

class API < Roda
  route do |r|
    r.is do
      # ...
    end
  end
end

class App < Roda
  route do |r|
    r.on "api" do
      r.run API
    end
  end
end

run App.app

これは/apiで始まる任意のパスを取ってAPIに送信します。この例におけるAPIはRodaアプリのことですが、これをSinatraやRailsといった他のRackアプリにするのも簡単です。

r.runを使うと、Rodaは指定のRackアプリを呼び出します(ここではAPI)。そのRackアプリが返すものはすべて、現在のアプリのレスポンスとして返されます。

振り分け先のRackアプリがたくさんあり、かつリクエストパスのプレフィックスを元に振り分けたいのであれば、multi_runプラグインを調べてください。

multi_routeプラグイン

メインのrouteブロックをブランチごとに分けたいだけなら、multi_routeプラグインを用いるべきです。これは現在のrouteブロックのスコープを維持します。

class App < Roda
  plugin :multi_route

  route "api" do |r|
    r.is do
       # ...
    end
  end

  route do |r|
    r.on "api" do
      r.route "api"
    end
  end
end

run App.app

これにより、メインのrouteブロックに1つ以上のインスタンス変数を設定して、api routeブロック内部でもそれらのインスタンス変数にアクセスできるようになります。

テスト

Rodaは、Rack::TestCapybaraを用いて実に簡単にテストできます。デフォルトのRakeタスクはRodaのspecを実行します。

設定

Rodaアプリごとの設定は、optsハッシュ内の設定に保存できます。この設定はサブクラスに継承されます。

Roda.opts[:layout] = "guest"

class Users < Roda; end
class Admin < Roda
  opts[:layout] = "admin"
end

Users.opts[:layout] # => 'guest'
Admin.opts[:layout] # => 'admin'

便利そうなものがあれば、何でも自由に保存できます。ただしサブクラス化した場合、Rodaは設定の「浅いコピー」(shallow clone)しか行わないのでご注意ください。

ネストした構造を保存してそれらをサブクラス内で変更するのであれば、「自己責任」でRoda.inheritedsuperを呼べるようにする)内のネストした構造をdupしてください。サブクラス化後に親クラスを変更してもサブクラスに影響しないように、およびその逆の影響も生じないようにするために、これは必ず行うべきです。

Rodaに付属するプラグインはこれらの設定をfreezeし、プラグインの再読み込みによる設定変更だけを許可します。外部プラグインもこのアプローチに従ってください。

以下のオプションは、デフォルトのライブラリやさまざまなプラグインで考慮されます。

:add_script_name
リクエストのSCRIPT_NAMEをパスの前に追加します。これは、アプリを別のアプリの下のパスとしてマウントする場合に便利です。
:freeze_middleware
ラックアプリのビルド時にすべてのミドルウェアをfreezeするかどうかを指定します。
:root
アプリのルート(root)パスを設定します。デフォルトは、プロセスの現在のワーキングディレクトリになります。

個別のプラグインがサポートするこの他のオプションがある場合は、プラグインのドキュメントで言及します。

レンダリング

Rodaには、レンダリングテンプレートのヘルパーを提供するrenderプラグインが付属します。このプラグインで用いられているTiltというgemは、さまざまなテンプレートエンジンとのインターフェイスとなります。デフォルトではerbエンジンが用いられます。

このプラグインで使うには、Tilt gemと、使いたいテンプレートエンジンをインストールする必要があります。

このプラグインは、テンプレート出力用のrenderメソッドとviewメソッドを追加します。デフォルトでは、viewはデフォルトのレイアウトテンプレート内のテンプレートを出力し、renderはテンプレートを単に出力します。

class App < Roda
  plugin :render

  route do |r|
    @var = '1'

    r.get "render" do
      # views/home.erbテンプレートをレンダリングする
      # このテンプレートはローカル変数のコンテンツと同様に
      # インスタンス変数@varにもアクセスできる
      render("home", locals: {content: "hello, world"})
    end

    r.get "view" do
      @var2 = '1'
      # views/home.erbテンプレートをレンダリングする
      # このテンプレートはインスタンス変数@varと@var2にアクセスでき、
      # その出力を受け取ってviews/layout.erb内部でレンダリングする
      # (これはコンテンツが挿入されるべき箇所でyieldされるはず)
      view("home")
    end
  end
end

プラグインにハッシュを1つ渡すことで、デフォルトのレンダリングオプションをオーバーライドできます。

class App < Roda
  plugin :render,
    escape: true, # Erubiのエスケープサポートでerbテンプレートの出力を自動エスケープする
    views: 'admin_views', # デフォルトのビューディレクトリ
    layout_opts: {template: 'admin_layout', engine: 'html.erb'},    # デフォルトのレイアウトオプション
    template_opts: {default_encoding: 'UTF-8'} # デフォルトのテンプレートオプション
end

セッション

Rodaはデフォルトではセッションをオンにしませんが、多くのユーザーがセッションサポートをオンにしたいと思うでしょう。最もシンプルな方法は、Rackに付属するRack::Session::Cookieミドルウェアを用いることです。

require "roda"

class App < Roda
  use Rack::Session::Cookie, secret: ENV['SECRET']
end

関連記事

Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 前編(翻訳)

Ruby: 認証gem「Rodauth」README(更新翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。