- 次記事: 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とは、Rubyで高速かつメンテナンス性の高いWebアプリを構築するためのルーティングツリーWebツールキットです。
インストール
$ gem install roda
リソース
- Webサイト
- http://roda.jeremyevans.net
- ソースコード
- https://github.com/jeremyevans/roda
- バグ
- https://github.com/jeremyevans/roda/issues
- Google Group
- https://groups.google.com/forum/#!forum/ruby-roda
- IRCチャット
- irc://chat.freenode.net/#roda
目指すもの
- シンプル
- 高信頼性
- 高拡張性
- ハイパフォーマンス
シンプル
Rodaは、内部外部のいずれもがシンプルになるように設計されています。「ルーティングツリー」を採用したことで、従来よりもシンプルかつDRYなコードを書けます。
高信頼性
Rodaは「イミュータブル」をサポートおよび促進します。Rodaアプリはproductionでfrozenされるように設計されており、スレッド安全性の問題が発生する可能性を排除しています。
さらにRodaでは、アプリで使われるインスタンス変数や定数やメソッドとの名前衝突を避ける目的で、Rodaで使われるインスタンス変数や定数やメソッドの個数を抑えています。
高拡張性
Rodaは完全にプラグインベースで構成されるため、拡張性が極めて高くなっています。Rodaのどんな部分でも、自由自在にオーバーライドしたりsuper
を呼んでデフォルトの振る舞いを得たりできます。
ハイパフォーマンス
Rodaではリクエストごとのオーバーヘッドを低く抑えており、ルーティングツリーや、内部データ構造のインテリジェントキャシングによって、よく知られている他のRuby製Webフレームワークよりも著しく高速に動作します。
使い方
ルーティングツリーの動作を示すシンプルなアプリです。
# cat config.ru
require 'roda'
class App < Roda
route do |r|
# GET / request
r.root do
r.redirect '/hello'
end
# /hello branch
r.on 'hello' do
# /helloブランチのすべてのルーティングで使う変数を設定
@greeting = 'Hello'
# GET /hello/world request
r.get 'world' do
"#{@greeting} world!"
end
# /hello request
r.is do
# GET /hello request
r.get do
"#{@greeting}!"
end
# POST /hello request
r.post do
puts "Someone said #{@greeting}!"
r.redirect
end
end
end
end
end
run App.freeze.app
上で行われている内容をブロックごとに小分けにして説明します。
route
ブロックは、新しいリクエストを受け取ったときに必ず呼ばれます。ここではRack::Request
のサブクラスにルーティングマッチ用のメソッドが若干追加された、そのサブクラスのインスタンスが生成されます。このブロックの引数名は慣習に則ってr
とすべきです。
Rodaでのルーティングのマッチは、主にr.on
、r.is
、r.root
、r.get``r.post
を呼ぶことで行います。これらの「ルーティングメソッド」はどれも「マッチブロック」を1つ取れます。
各ルーティングメソッドは1つ以上の引数(マッチャー)を受け取り、現在のリクエストとマッチするかどうかを順に試行します。メソッドの引数がすべてマッチするとマッチブロックをyield
し、1つでもマッチしないとブロックをスキップして次に進みます。
r.on
: 引数がすべてマッチするとマッチと判定しますr.is
: 引数がすべてマッチし、かつマッチした部分より先のエントリがパスにない場合にマッチと判定しますr.get
: 引数なしで呼び出されると、あらゆるGET
をマッチと判定しますr.get
:(1つ以上の引数で呼び出されると)、現在のリクエストがGET
で、かつマッチした部分より先のエントリがパスにない場合にのみマッチと判定しますr.root
: 現在のパスが/
になっているGET
リクエストのみをマッチと判定します
Rodaは、ルーティングメソッドがマッチして制御がマッチブロックにyield
されると、マッチブロックから戻るときに必ずRackレスポンスの配列(ステータス、ヘッダー、bodyを含む)を呼び出し元に返します。
マッチブロックが文字列を返し、レスポンスのbodyがまだそこに書き込まれていない場合は、ブロックの戻り値をレスポンスのbodyとして解釈します。どのルーティングメソッドともマッチせず、route
ブロックが文字列を返す場合は、その文字列をレスポンスのbodyとして解釈します。
r.redirect
はレスポンスを即座に返すので、r.redirect(path) if 条件
のような書き方ができます。r.redirect
が引数なしで呼ばれ、現在のリクエストメソッドがGET
ではない場合、現在のパスにリダイレクトします。
末尾に.freeze.app
をオプションで追加できます。アプリをfreeze
すると、アプリレベルの設定を変更しようとしたときにエラーがraiseされるので、アプリのスレッド安全性問題の可能性を事前に警告できます。.app
は、リクエストごとのメソッド呼び出しを若干節約する、一種の最適化です。
ルーティングツリー
Rodaは「ルーティングツリーWebツールキット」と呼ばれていますが、その理由は、ルーティングが(サイトのURL構造に基いて)ツリーを形成するかたちでほとんどのサイトが構造化されるためです。一般に次が成り立ちます。
r.on
: ツリーを異なる枝(branch)に枝分かれするのに用いますr.is
ルーティングパスの終端を定めますr.get
とr.post
: 特定のリクエストメソッドを扱います
したがって、シンプルなルーティングツリーは次のような感じになります。
r.on 'a' do # /a branch
r.on 'b' do # /a/b branch
r.is 'c' do # /a/b/c request
r.get {} # GET /a/b/c request
r.post {} # POST /a/b/c request
end
r.get 'd' do end # GET /a/b/d request
r.post 'e' do end # POST /a/b/e request
end
end
(異なる枝が)同じリクエストを扱うこともできますが、リクエストメソッドの最初の枝分かれでルーティングツリーが構造化されます。
r.get do # GET
r.on 'a' do # GET /a branch
r.on 'b' do # GET /a/b branch
r.is 'c' do end # GET /a/b/c request
r.is 'd' do end # GET /a/b/d request
end
end
end
r.post do # POST
r.on 'a' do # POST /a branch
r.on 'b' do # POST /a/b branch
r.is 'c' do end # POST /a/b/c request
r.is 'e' do end # POST /a/b/e request
end
end
end
このようになっていることで、GET
リクエストの扱いとPOST
リクエストの扱いを簡単に分離できます。扱うPOST
リクエストのURLが少なく、GET
リクエストのURLが多い場合はさらに簡単です。
ただし、パスによるルーティングを冒頭に配置し、リクエストメソッドによるルーティングを末尾に配置する方が、シンプルでDRYなコードになりやすくなるでしょう。このようなことが可能なのは、ルーティング中のどの時点でもリクエストを扱えるからです。たとえば、/a
ブランチではすべてのリクエストでA
というパーミッションが必要で、/a/b
ブランチではB
というパーミッションが必要だとすると、次のようにこれらを簡単にルーティングツリーで扱えます。
r.on 'a' do # /a branch
check_perm(:A)
r.on 'b' do # /a/b branch
check_perm(:B)
r.is 'c' do # /a/b/c request
r.get {} # GET /a/b/c request
r.post {} # POST /a/b/c request
end
r.get 'd' do end # GET /a/b/d request
r.post 'e' do end # POST /a/b/e request
end
end
ルーティング中の任意の時点でリクエストを自由に操作できる点が、Rodaの大きな強みのひとつです。
マッチャー
r.root
を除くあらゆるルーティングメソッドは、1つまたは複数のマッチャーを引数として取ることができます。マッチャーがすべてマッチすると、そのルーティングメソッドはマッチブロックをyield
します。以下はさまざまなマッチャーの動作を示すコード例です。
class App < Roda
route do |r|
# GET /
r.root do
'Home'
end
# GET /about
r.get 'about' do
'About'
end
# GET /post/2011/02/16/hello
r.get 'post', Integer, Integer, Integer, String do |year, month, day, slug|
"#{year}-#{month}-#{day} #{slug}" #=> "2011-02-16 hello"
end
# GET /username/foobar branch
r.on 'username', String, method: :get do |username|
user = User.find_by_username(username)
# GET /username/foobar/posts
r.is 'posts' do
# ブロックはクロージャなので、ここでユーザーにアクセスしてもよい
"Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
end
# GET /username/foobar/following
r.is 'following' do
user.following.size.to_s #=> "1301"
end
end
# /search?q=barbaz
r.get 'search' do
"Searched for #{r.params['q']}" #=> "Searched for barbaz"
end
r.is 'login' do
# GET /login
r.get do
'Login'
end
# POST /login?user=foo&password=baz
r.post do
"#{r.params['user']}:#{r.params['password']}" #=> "foo:baz"
end
end
end
end
個別のマッチャーについて以下で解説します。文中の「セグメント」とは、/
で始まるパスの一部を指します。つまり、/foo/bar//baz
には/foo
、/bar
、/
、/baz
という4つのセグメントがあります。3番目の/
は空のセグメントと見なされます。
文字列
文字列にスラッシュ/
が含まれていない場合、/
で始まり、その文字列のテキストを含む1つのセグメントにマッチします。
"" # "/"にマッチ
"foo" # "/foo"にマッチ
"foo" # "/food"にはマッチしない
文字列にスラッシュ/
が1つ以上含まれている場合は、/
で区切られた1つの追加セグメントにマッチします。
"foo/bar" # "/foo/bar"にマッチ
"foo/bar" # "/foo/bard"にはマッチしない
正規表現
正規表現は、スラッシュ/
で始まり、/
またはパスの終端で終わるパターンを検索することで、1つまたは複数のセグメントにマッチします。
/foo\w+/ # "/foobar"にマッチ
/foo\w+/ # "/foo/bar"にはマッチしない
/foo/i # "/foo"や"/Foo/"にマッチ
/foo/i # "/food"にはマッチしない
いずれかのパターンが正規表現でキャプチャされると、yield
されます。
/foo\w+/ # "/foobar"にマッチ(yieldされない)
/foo(\w+)/ # "/foobar"にマッチ("bar"をyield)
クラス
マッチャーはString
とInteger
でサポートされます。
String
- 空でない任意のセグメントにマッチし、
/
で始まる場合を除いてそのセグメントをyield
する Integer
0
–9
の任意のセグメントにマッチし、マッチした値を整数で返す
任意のセグメントを扱う場合は、String
やInteger
の利用をおすすめします。
String # "/foo"にマッチし、"foo"をyield
String # "/1"にマッチし、"1"をyield
String # "/"にはマッチしない
Integer # "/foo"にはマッチしない
Integer # "/1"にマッチし、1をyield
Integer # "/"にはマッチしない
シンボル
シンボルは、空でない任意のセグメントにマッチし、先頭の/
を除いた部分をyield
します。
:id # matches "/foo" yields "foo"
:id # does not match "/"
シンボルマッチャーによる操作はString
クラスのマッチャーと同じであり、かつて任意のセグメントマッチを行う方法として使われていました。新しいコードではString
クラスのマッチャーを使うことをおすすめします(直感的に書けるので)。
proc
procは、(false
かnil
を返すものを除き)あらゆるものにマッチします。
proc{true} # あらゆるものにマッチ
proc{false} # どれにもマッチしない
procはデフォルトではキャプチャを行いません。しかしキャプチャしたテキストをr.captures
に追加すれば可能です。
配列
配列は、その要素のどれか1つでもマッチした場合にマッチしたと判断されます。複数のマッチャーをr.on
に渡す場合は、そのすべてにマッチしなければマッチしたと判断されません(AND条件)が、マッチャーの配列を渡す場合は、その中のどれか1つがマッチする必要があります(OR条件)。条件の評価は、マッチャーが最初にマッチした時点で終了します。
さらに、マッチしたオブジェクトがString
の場合、その文字列がyield
されます。これを用いれば、正規表現を使わずに複数の文字列マッチを簡単に取り扱えます。
['page1', 'page2'] # "/page1"か"/page2"にマッチ
[] # どれにもマッチしない
ハッシュ
ハッシュを使うと、リクエストで特殊なマッチメソッドを簡単に呼び出せます。Rodaでデフォルトで使える登録済みマッチャーについては後述します。一部のプラグインはハッシュマッチャーを追加します。hash_matcher
プラグインを使うと、独自のハッシュマッチャーを簡単に定義できます。
class App < Roda
plugin :hash_matcher
hash_matcher(:foo) do |v|
# ...
end
route do |r|
r.on foo: 'bar' do
# ...
end
end
end
:all
:all
は、渡された配列のすべてのエントリとマッチした場合にマッチしたと判断します。
r.on all: [String, String] do
# ...
end
つまり、上のコードは下と同等です。
r.on String, String do
# ...
end
:all
がハッシュマッチャーとしても存在している理由は、配列マッチャーの中でも使えるようにするためです。
r.on ['foo', {all: ['foos', Integer]}] do
end
上のコードは、/foo
や/foos/10
とはマッチしますが、/foos
とはマッチしません。
:method
:method
マッチャーは、リクエストメソッド(訳注: GET
やPOST
などのHTTPメソッド)とマッチします。複数のリクエストメソッドを配列として渡すと、そのいずれかとマッチします。
{method: :post} # POSTとマッチ
{method: ['post', 'patch']} # POSTかPATCHにマッチ
false
とnil
マッチャーにfalse
やnil
を直接渡すと、あらゆるものにマッチしなくなります。
その他
これ以外の場合はエラーをraise
します。ただし、プラグインが別種のマッチャーを追加するなどで特定のサポートが追加された場合は、この限りではありません。
- 次記事: Ruby: 高速/高性能ルーティングエンジンgem「Roda」README: 中編(翻訳)