- 前記事: Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 前編(翻訳)
- 次記事: Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 後編(翻訳)
概要
MITライセンスに基いて翻訳・公開いたします。
- リポジトリ: jeremyevans/roda
- 原文更新日: 2018/02/01
- 著者: Jeremy Evans
- サイト: http://roda.jeremyevans.net/
- API: http://roda.jeremyevans.net/rdoc/index.html
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として使われる
nil
、false
- 無視される
- その他すべて
- エラーを
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/b
のGET
リクエストにだけマッチします。
このように振る舞いを変えてある理由は、引数をまったく渡さないということは「おそらく現在のパスと完全一致するかどうかのテストまでしたくなんかないだろう」と考えたためです。「いやいや、それテストしたいから」というのであれば、引数に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.root
はr.get ""
と似ていますが、/
の指定に余分な場所を使わないで済む点が異なります。
他のマッチメソッドと異なり、r.root
は引数を取りません。
r.root
は、パスが空の場合にはマッチしない点にご注意ください。その場合はr.get true
を用いるべきです。空のパスと/
の両方にマッチさせたい場合は、r.get ["", true]
とするか、slash_path_emptyプラグインを使います。
r.root
はGET
リクエストでないとマッチしない点にご注意ください。POST /
リクエストを扱うには、r.post ''
を使います。
リクエストとレスポンス
リクエストオブジェクトはroute
ブロックにyieldされますが、request
メソッドで取得することもできます。同様に、レスポンスオブジェクトはresponse
メソッドで取得できます。
リクエストオブジェクトは、Rack::Request
のサブクラスのインスタンスにいくつかのメソッドを追加したものです。
リクエストオブジェクトやレスポンスオブジェクトを別のモジュールで拡張【チェック】したい場合は、module_includeプラグインを利用できます。
「汚染」について
Rodaでは、さまざまな手をつくしてroute
ブロックのスコープの汚染を回避しようとしています。このため、Rodaがアプリのコードで名前空間の問題を引き起こす可能性は小さいはずです。Rodaでは以下を含む対策を取っています。
route
ブロックのスコープでデフォルトで定義されるインスタンス変数は、@_request
と@_response
しかありません。Rodaに付属するプラグインがroute
ブロックのスコープで用いるインスタンス変数は、すべて冒頭にアンダースコア_
が追加されます。- (
Object
のデフォルトメソッドの他に)定義されるメソッドは、次のものしかありません:call
、env
、opts
、request
、response
、session
- 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::TestやCapybaraを用いて実に簡単にテストできます。デフォルトの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.inherited
(super
を呼べるようにする)内のネストした構造を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「Roda」README: 後編(翻訳)