コンテンツにスキップ

ルーティング

ルーティングは Web アプリケーションを開発する上で基本となる要素のうちの一つです.ルーティングは,クライアントがリクエストした URI を解釈し,それに見合う処理を探す役割をします.ここではまず,サーバーサイドではクライアントからのリクエストが本質的にどのように見えるのかを説明します.そしてなぜルーティングの概念が発生するのかについて述べます.次に,bamboo ではルーティングをどのように実行しているのかについて述べ,URI 設定時に重要となるロケーションという概念について説明します.

Note

このドキュメントでは以降 HTTP での通信を仮定します.

クライアントからのリクエスト

ルーティングを理解するためには,まずクライアントからのリクエストがサーバーサイドでどのように見えているのかを知っておく必要があります.あなたがこのページの URI である

https://jjj999.github.io/bamboo/tutorials/routing/

にアクセスしたとしましょう.するとあなたのリクエストは github.io というドメインに対応する IP アドレスに向けて転送されます.その行き先にはこのページを管理しているサーバーが待ち構えています.そしてそのサーバーは HTTP に則った以下のデータを受け取ります:

GET /bamboo/tutorials/routing/ HTTP/1.0
以下 ヘッダフィールド --------

これがクライアントが実際に送信するリクエストデータです.リクエストは通常

  • リクエスト行
  • ヘッダフィールド
  • ボディ

で構成され,上の例の1行目はリクエスト行にあたります.リクエスト行は

  • HTTP メソッド
  • URI のパス
  • HTTP バージョン

を空白区切りで記入したもので,1行のみで構成されます.これこそがサーバーサイドから見たクライアントのリクエストの姿です.

ルーティングの概念

上の説明は少々違和感があります.なぜなら,クライアントは

https://jjj999.github.io/bamboo/tutorials/routing/

という URI にアクセスしたはずなのに,実際にそのリクエストを受け取ったのは github.io のサーバーなのです.それではどうやって残りの bamboo/tutorials/routing/ までを特定しているのでしょうか.実はそれを行うのはサーバーの仕事なのです.

サーバーが bamboo/tutorials/routing/ のような受け取った URI のパスをどのように解釈してレスポンスを返すかは基本的には自由です.bamboo/tutorials/routing/ を指定されたからと言って,ルーティングに関係するチュートリアルのドキュメントが書かれた HTML ファイルを送る必要はありません.ただ,クライアントはルーティングに関係するチュートリアルのドキュメントを求めてその URI にアクセスするわけですから,それに相応するレスポンスを返すべきです.

サーバーへのリクエストは何も HTML ファイルを送れという命令だけではありません.例えば,ブラウザやスマートフォンでメールアドレスとパスワードを入力してログインをする場合を考えてみましょう.あなたが入力したデータは何によって検証され,何によってログインを許可されたり再入力を求められたりするのでしょうか.その正体もやはりサーバーです.あなたがログインボタンを押すと水面下では特定の URI を指定してサーバー側にリクエストが送信されているのです.そしてサーバーはあなたが入力したメールアドレスとパスワードを見て,それが本当に正しいものなのかを内部で検証し,その結果をレスポンスとして返しているのです.そのときサーバーはログイン完了後のサイトを HTML ファイルとして送信するかもしれませんし,単にログイン完了を示す小さなテキストデータのみを送るかもしれません.

ここまで来ればルーティングの概念が理解できるようになっていることでしょう.ずばりルーティングとは,クライアントから受け取った URI のパスを解析して,どういうレスポンスを返す処理を行うかを特定することです.あなたがこれから作ろうとするサーバーにはたくさんの機能が盛り込まれるかもしれません.しかし,クライアントがその機能にアクセスするためには1つ1つの機能に対応した URI が必要です.その URI (のパス) の定義はあなた自身が行うことになります.

HTTP メソッド

さきほど,サーバーは URI のパスから行うべき処理を決定することが出来る述べました.しかし実際には URI のパスの他にもう一つ必要な情報があります.それが HTTP メソッド です.URI のパスがリクエストを処理する場所を表すのであれば,HTTP メソッドが表すのはそのアクションです.たとえば,あなたが銀行に行ったとしましょう.その銀行には〇〇支店などという店名が付けられていることでしょう.それは URI のパスと似た発想です.一方で,銀行では出来ることはいくつかあります.口座から現金の引き出し,預け入れ,他口座への振込,等々です.これらはあなたが銀行に行った際に行えるアクションであり,HTTP メソッドに似た概念です.

注意すべきことは HTTP メソッドには限られたアクションしかないということです.あなたが銀行ではなく郵便局に行けば,あなたの行えるアクションは変わりますが,HTTP メソッドは URI のパスが変わろうとその選べるアクションは変わりません.現在 bamboo でサポートされている HTTP メソッドには以下のようなものがあります:

  • GET
  • HEAD
  • POST
  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH

Tip

HTTP メソッドにはそれぞれアクションの意味があります (参考).意味を理解して上手く使い分けて設計すると,システムが美しくなり見通しが良くなります.

bamboo でのルーティング

bamboo のアプリケーションオブジェクトには既にルーティング機能が実装されています.さきほど述べたように,レスポンスを返す処理は URI のパスと HTTP メソッドによって特定されるので,それを予め定義しておく必要があります.bamboo では以下の例のように定義します.

from bamboo import WSGIApp, WSGIEndpoint

app = WSGIApp()

# URI のパスの定義
@app.route("hello", "client")
class HelloEndpoint(WSGIEndpoint):

    # 対応する HTTP メソッドの定義
    def do_GET(self) -> None:
        self.send_body(b"Hello, Client!")

上の例では次のリクエスト行を持つリクエストが送られてくると,do_GET() メソッドが呼び出されることになります:

GET /hello/client HTTP/1.1

上の例からも垣間見えるように,bamboo でのルーティングは2段階で行われます:

  1. リクエスト行の URI のパスを解析し,エンドポイントを特定する
  2. 特定されたエンドポイントがリクエスト行の HTTP メソッドに対応するレスポンスメソッド (上の例では do_GET()) 持っているか調べる

エンドポイントと URI のパスを紐付ける処理を行うのがアプリケーションオブジェクトの route() デコレータであり,そのエンドポイントが対応している HTTP メソッドは do_[METHOD]() の命名規則を持つメソッドが定義されているかどうかで判断されます.ちなみに,エンドポイントクラス定義時ではなく別の場所で URI のパスを紐づけたい場合は以下のように route() メソッドを呼び出す必要があります:

app.route("hello", "client")(HelloEndpoint)

Note

エンドポイントという言葉はよくこの URI のパスという意味で使われます.また,パスという言葉は元来リソースの在り処を指す言葉として使用されてきました.一方で,bamboo におけるエンドポイントはこの URI のパスと非常に密接な関係を持つ概念です.bamboo のエンドポイントはリクエストの行き着く場所であり,レスポンスが返される場所でもあります.つまり,常にお金が出入りする銀行の支店のようなものです.URI のパスというのは一般のクライアントがそこにアクセスできるように付けた住所であり,bamboo のエンドポイントにとっては数ある属性のうちの一つの属性に過ぎません.

ロケーション

URI のパスには様々なバリエーションがあります.例えば,Github では URI のパスは

<アカウント名>/<リポジトリ名>/...

のように定義されています.これはユーザーのリソースを扱う場合に非常に有用なパスの定義です.しかし,これを先程の例のように bamboo で定義するのはほぼ不可能です.なぜなら,ソースコードを書いた状態ではどんなユーザー名を持つユーザーがどんなリポジトリ名を持つリポジトリを作成するかわからないからです.つまり Github の URI パターンは,アクセス出来る URI が動的に追加されているのです.

bamboo ではこのような URI のパスの定義をサポートするために,ロケーション という概念を導入しています.ロケーションとは URI のパスを分解したときの1つ1つの要素のことです.例えば /hello/client というパスは hello というロケーションと client というロケーションに分解されます.さらに,ロケーションには次の2つの種類があります:

  • 静的ロケーション
  • フレキシブルロケーション

静的ロケーションとは開発時に固定されるロケーションのことで,helloclient は静的ロケーションの一種です.静的ロケーションは str オブジェクトです.一方,フレキシブルロケーションは静的ロケーションのように固定されず,ある規則に則ったロケーションを許容します.フレキシブルロケーションは bamboo.FlexibleLocation クラスのサブクラスのオブジェクトで表現されます.一例として bamboo には AnyStringLocation というフレキシブルロケーションの実装クラスがあります.このクラスはどんな文字列でも許容するロケーションで,オプションとして許容する文字数の最大値を指定できます.これを使って以下のようにさきほどの HelloEndpoint の URI のパスを再設定してみます:

from bamboo import AnyStringLocation

app.route("hello", AnyStringLocation())(HelloEndpoint)

こうすると,さきほどまでは /hello/client しか受け付けなかったエンドポイントが,hello/worldhello/bamboo のようなパスも受け付けられるようになります.冒頭の Github の URI のパスもこのようなフレキシブルロケーションを利用すると簡単に定義することが可能になります.

Note

フレキシブルロケーションの登場により,開発時に設定するロケーションの組み合わせと実際にクライアントから受け取るロケーションの組み合わせは正確には一致しなくなりました.例えば開発時に ("hello", AnyStringLocation()) と設定しても,実際に受け取るのは ("hello", "client") だったり ("hello", "world") だったりします.クライアントから受け取るロケーションの組み合わせを「URI のパス」と呼ぶ分には曖昧さは発生しませんが,フレキシブルロケーションを含むロケーションの組み合わせを「URI のパス」と呼ぶのはふさわしくありません.このような理由から,bamboo では開発時にエンドポイントに紐付けるロケーションの組み合わせのことを URI パターン と呼んでいます.

フレキシブルロケーションの注意点

フレキシブルロケーションは上述したように静的ロケーションではカバーできない表現を可能にします.しかし,その柔軟さゆえに危険性もはらんでいます.まずは以下の例を見てください:

```python
app.route("superuser", "setting")(SuperUserSettingEndpoint)
app.route(AnyStringLocation(max=64), "setting")(UserSettingEndpoint)
```

上記のコードは,管理者と一般ユーザーの設定の変更を行うエンドポイントに差別化を図ろうとしています.しかし,上記のコードはあるパスが指定された場合には正常に動作しない可能性があります.そのパスとは superuser/setting です.なぜならこのパスは一般ユーザー用のロケーションの組み合わせとしても有効だからです.したがって,2つのエンドポイントに対して有効なパスとなってしまい,意図したエンドポイントに到達しない可能性があります.

このような問題に対処するために,bamboo ではアプリケーション実行時に以下のような規則でアプリケーションオブジェクトに登録された全ての異なる2つの URI パターンを検証します:

  1. URI パターンに含まれるロケーションの数が異なる場合は有効
  2. URI パターンに含まれるロケーションの数が同じだが,内部の同位置にある静的ロケーションが異なる場合は有効
  3. 1, 2 に当てはまらない場合は無効

この規則は何だか難しげではありますが,特に注意すべき点は URI パターンに含まれるロケーションの数が同じ URI パターンに対してのみです.このような場合に,上記の 2 番を満たしていれば,正常な URI パターンとして処理されます.以下にいくつか例を挙げますので参考にしてみてください.

# 有効 (ロケーションの数が異なる)
app.route(AnyStringLocation(), AnyStringLocation())(Endpoint1)
app.route(AnyStringLocation(), AnyStringLocation(), AnyStringLocation())(Endpoint2)

# 有効 (同位置の静的ロケーションが異なる)
app.route(AnyStringLocation(), "client")(Endpoint1)
app.route(AnyStringLocation(), "server")(Endpoint2)

# 無効 (同位置に静的ロケーションが存在しない)
app.route(AnyStringLocation(), AnyStringLocation())(Endpoint1)
app.route(AnyStringLocation(), "server")(Endpoint2)

# 無効 (同位置の静的ロケーションが同じ)
app.route(AnyStringLocation(), "client")(Endpoint1)
app.route(AnyStringLocation(), "client")(Endpoint2)

# 有効 (同位置の静的ロケーションが異なる)
app.route("superuser", "setting", "name")(Endpoint1)
app.route(AnyStringLocation(), "setting", "email")(Endpoint2)

# 無効 (同位置の静的ロケーションが同じ)
app.route("superuser", "setting", AnyStringLocation())(Endpoint1)
app.route(AnyStringLocation(), "setting", AnyStringLocation())(Endpoint2)

Tip

URI パターンの設計は少々神経質になる作業かもしれません.上手く URI パターンを設計するコツは,無効にならないように設計するのではなく,有効になるように設計することです.つまり神経質になるのはロケーションの数が同じ時だけで,確認すべきは静的ロケーションの位置とその内容だけです.ちなみに,無効な2つの URI パターン検出されると bamboo は DuplicatedUriRegisteredError を送出します.このエラーが発生したとき,無効と判断された2つの URI パターンが出力されるので,その内容を元に必要な部分を修正してください.

独自のフレキシブルロケーションの実装

フレキシブルロケーションは bamboo.FlexibleLocation クラスのサブクラスのオブジェクトです.つまり,FlexibleLocation のサブクラスを実装することで,独自のフレキシブルロケーションを実装できます.実装すべきメソッドは is_valid() メソッドただ1つです.このメソッドは FlexibleLocation では抽象メソッドとして実装されており,クライアントから送られてきたロケーションを引数として受け取り,自身のロケーションとしてふさわしいものかを検証します.さきほど紹介した AnyStringLocation は文字数が最大値を超えていないかのみを検証するロケーションです (文字数の最大値の設定はオプションなので,設定されていなければ何も検証しないロケーションになります).

今回は実装例としてアルファベットの小文字のみを許容するフレキシブルロケーションを定義してみましょう.検証は以下の手順で行います:

  1. パーセントエンコーディングされたパスをデコードする
  2. 文字列がアルファベットかつ小文字であることを検証する

実際に実装したフレキシブルロケーションは以下のようになります:

from urllib.parse import unquote
from bamboo import FlexibleLocation

class LowercaseAlphaLocation(FlexibleLocation):

    # loc にクライアントがリクエストした URI の
    # ロケーションが渡される
    def is_valid(self, loc: str) -> bool:
        loc = unquote(loc)
        return loc.isalpha() and loc.islower()

この作成したフレキシブルロケーションはエンドポイントの URI パターンを設定する際にそのまま使用できます:

app.route("test", LowercaseAlphaLocation())(SampleEndpoint)

さきほど実装したフレキシブルロケーションに,文字数の最大値を設定したい場合は,__init__() メソッドを実装することが出来ます (簡易的な実装です) :

class LowercaseAlphaLocation(FlexibleLocation):

    def __init__(self, max: int) -> None:
        self._max = max

    def is_valid(self, loc: str) -> bool:
        loc = unquote(loc)
        return loc.isalpha() and loc.islower() and len(loc) <= self._max

この場合は,URI パターンの設定は以下のようになります:

app.route("test", LowercaseAlphaLocation(5))(SampleEndpoint)
Back to top