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

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

概要

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


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.onr.isr.rootr.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.getr.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)

クラス

マッチャーはStringIntegerでサポートされます。

String
空でない任意のセグメントにマッチし、/で始まる場合を除いてそのセグメントをyieldする
Integer
0-9の任意のセグメントにマッチし、マッチした値を整数で返す

任意のセグメントを扱う場合は、StringIntegerの利用をおすすめします。

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は、(falsenilを返すものを除き)あらゆるものにマッチします。

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マッチャーは、リクエストメソッド(訳注: GETPOSTなどのHTTPメソッド)とマッチします。複数のリクエストメソッドを配列として渡すと、そのいずれかとマッチします。

{method: :post}               # POSTとマッチ
{method: ['post', 'patch']}   # POSTかPATCHにマッチ

falsenil

マッチャーにfalsenilを直接渡すと、あらゆるものにマッチしなくなります。

その他

これ以外の場合はエラーをraiseします。ただし、プラグインが別種のマッチャーを追加するなどで特定のサポートが追加された場合は、この限りではありません。


関連記事

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

Railsのルーティングを極める(前編)

Railsのルーティングを極める (後編)


CONTACT

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