レベル: 中級 David Mertz, Ph.D (mertz@gnosis.cx), Author, Gnosis Software, Inc.
2008年 1月 29日 Ranvier は Web アプリケーションのフレームワークに統合できる Python のパッケージであり、受信した URL リクエストをソース・コードにマッピングすることができます。Ranvier はそれを、正規表現に基づいて URL を書き換える一般的な方法とは異なる、委任と利用 (delegation-and-consumption) の機構を使って行います。また Ranvier は Web アプリケーションで扱われるすべての URL の中心的なレジストリーとしても動作し、ページをクロスリンクさせるために必要な URL を Ranvier 自体が生成します。このレジストリー機能によって、Ranvier はリンクの完全性を保証することができ、またカバレッジ分析を自動化することができます。Ranvier はピュア Python コードであり、サードパーティーにはまったく依存しません。そのため (アダプター・コードを少し追加することで)、Python ベースの任意の Web アプリケーション・フレームワークに Ranvier を利用できるはずです。
はじめに
URI (Uniform Resource Identifier) の役割は、世界に 1 つの固有の名前をリソースに付けることです。URI のサブセットとして最も有名なものは URL (Uniform Resource Locator) です。URL には、指定されたリソース (つまり電子文書あるいは電子ストリームを取得する際に使用するネットワーク上の場所やプロトコル) を取得する方法を説明する、という役割が追加されています。一部の URI は、URL ではない URN (Uniform Resource Name) です。つまり、これらの URI はリソースを指定しますが、そのリソースを取得する方法についての詳細は提供しません。
しかし、URI が何を意味するかは状況によって微妙に異なります。記述されるリソースの内容が、コンテキスト (特にリソースを取得する時刻と場所) によって変化することがよくあります。例えば file:///etc/hosts という URI (そして URL) が記述する内容は、読者が自分のローカル・マシンでこのリソースを検証してみると、それぞれの読者によって異なります。さらに、例えば位置情報の機能を持つ Web ベースのサービスを考えてみると、そのサービスは、ある場所のある時刻の天気に関するコンテンツを受信したいと要求側が望んでいることを示すために、http://weather.example.com/today/local/ のような URL を使用するかもしれません。また、URL ではない URN を使って同じコンテンツを表すかもしれません (例えば urn:weather:today:local など)。指定されたコンテンツをどのようなプロトコルあるいは仕組みを利用して取得、あるいは生成するかを決めるのは、おそらくコンテンツを処理するアプリケーションの役割のはずなのです。
原則的には、URI は RFC 3986 に準拠する任意の形式にすることができます。例えば URI は、RFC 4122 で定義される UUID (Universally Unique Identifier) サブセットに従った名前を利用することができます。これを利用してエンコードすると、「本日の天気」を urn:uuid:4930fce0-2eda-4f6b-9eaa-fe426fefc655 のように指定することができます。また、もし URL 形式が必要な場合には、例えば tftp://192.0.2.143/eaecf224-6efc-3499-8cd8-3e50a4f58ec1 のようにすることができます。当然ながらこれらの URI には、人間にとって覚えにくく、わかりにくいという問題があります。(マシンは人間ほど、どう表現するかという問題には影響されません。) 実際には、開発者やユーザーは URI を単なる構文形式とするのではなく、少なくとも何らかのセマンティックな情報を URI に埋め込みたいと思うものです。
階層構造とコンポーネント
最も古く、そして最も一般的な Web サーバーは、リソースをホストするマシンのファイル・システム構造を直接反映した形式の URI を生成しました。この URI スキーマ自体は、URI の階層構造の「パス」コンポーネントを指定します (ただしパスは空かもしれません) が、URI のパス構造とファイル・システムとの間が文字どおりマッピングされている必要があるわけではありません。とはいえ、Web サーバーが URI のベースとして単純に相対ディレクトリー名を使用することは一般的です。このシステムは、(比較的) 静的な文書群を構成するためには非常に適切です。その場合には、ローカルでアクセスされるリソースの階層構造を作成する際とネットワークでアクセスされるリソースの階層構造を作成する際に、同じ構成原則を適用できるからです (これは大まかに言えば、WWW の元々の目的です)。
動的な Web アプリケーションのフレームワークでは、一般的に URI パスの階層構造は、そのページを提供するコードの構造の外にまで広がります。そのため、例えば Rails や Django などによって提供される URI のパス・コンポーネントはクラスやメソッドの相対的なネスティングを表し、フレームワークに特有の、クラスのコンポーネントに対する命名規則をいくつか追加で持っていることがよくあります。
ファイル・システムのマップとコード階層構造のパスに共通することの 1 つとして、それらのマップやパスによってリソース利用側に提供される URI が、Web サーバーを構成する際に下された技術的な決定の範囲を超えて広がっていることが挙げられます。コードに関する決定は URI の表現を見るとわかります。場合によると、この透明性は適切で望ましいものです。例えば、よく OOP (Object Oriented Programming: オブジェクト指向プログラミング) で掲げられる理想の下では、コード・オブジェクトの構造はアプリケーション・ドメインのセマンティクスを直接反映しています。しかしそうした理想以外の場合では、Web サービスを作成する際に関係する階層構造は、そのドメイン自体の「自然な」階層構造とはかなり異なります。その場合には、リソースの利用者は、彼らが取得しようとするリソースの内容を記述していない、単に名前のみを指定する URL で我慢しなければなりません。または少なくとも、こうした URI は、開発者の視点ではなく利用者の視点から作業を始めた場合のように適確にリソースの内容を記述することはできない、ということです。
正規表現による置き換え
内部構造を持つ URI に、セマンティックに表現された URI をマッピングするための最も一般的なツールでは、正規表現に基づいて置き換えが行われます。この方法の例として、Apache の mod_rewrite が最も広く知られています。Rails の場合も同様の方法が採られます。この方法では、内部用の URI セット (例えばサイトを作成する際の技術的判断に従った形式の URI) と、意味のある URI を構成するものは何かについての考え方が必要です。正規表現を使って URL を書き換える方法では、書き換えられるすべての URI に対して、その URI に対応するもの (URI が指す対象を表したもの) が少なくとも 1 つあります。つまり「公開されている」 URI が、Web サーバーによって「プライベートな」 URI にマッピングされます。しかし、プライベートな URI は必ずしも (それどころか普段も) 公開アクセスから保護されているわけではなく、単に公開版の URI はリソースを公開する際に通常公開されるものである、ということにすぎません。
URL を書き換える方法の最も単純で一般的な例として、パスをベースにしたリソース記述を FastCGI (あるいは単なる CGI) 呼び出しにマッピングする場合が挙げられます。つまり Web サーバーの、いわゆるファイル・システムにはパスが存在しない可能性もあるため、「真の」パスをシミュレートするために、書き換えルールは「仮想」パスを返すスクリプトにパスを委任します (この仮想パスは、サーバーのファイル・システムに対する仮想的な相対パスです。つまりすべての URI パス・コンポーネントは実質的にリソース・コンポーネントと等価です)。『The Django Book』の中の一例には、ファイル・システムのパスに存在するすべてのファイルを返し、ファイル・システムのパスにファイルが存在しない場合にはパスを委任するように Apache に命令する方法が説明されています。Ranvier の Web ページにある CGI ベース の Ranvier のデモはそれと似たルールを使っており、私のテスト・システムでも、そのルールを使っています (下記)。
Apache の mod_rewrite: ファイルが存在しない場合にはパスを委任する
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^/(.*)$ /mysite.fcgi/$1 [QSA,L]
|
Ranvier はどのように異なるのか
Ranvier は正規表現を使って公開 URI をプライベート URI に書き換える代わりに、chain-of-responsibility 設計パターンを実装することで、リソースを決定するための判断を関連するコードに委任します。この場合の目標は、大まかに言って、意味のある形でコードを再利用できるようにすることです。パス・コンポーネントがあるごとに、関連するコード・ハンドラーへのディスパッチが行われます。するとコード・ハンドラーは、1 つ以上のパス・コンポーネントを使用するか、もしくはパス・コンポーネントを使用せずに、必要な処理をすべて実行し、また処理状態を保持するコンテキスト・オブジェクトを (必要に応じて) 変更します。この場合、さまざまな URI を処理する際に同じハンドラーを渡すことができるため、一般的な任意の作業を 1 つのハンドラーで容易にコーディングすることができます。
この一連の動作は例を見るとよくわかります。Ranvier に付属しているデモには、あるパスの中に含まれるユーザー名を処理する例が含まれています。例えば http://furius.ca/ranvier/demo/users/mertz/name という URI は (このデモの設計に私の名前は含まれていませんが)、私の「ユーザー・アカウント」についての記述を返します。この例で行われることは、私の苗字 (last name) を大文字にし (訳注: 大文字にするのは、もちろん英語で記述する場合の話です)、このアクションについてのメッセージを返すことのみです。しかしこのデモは、URI をどう利用して任意のユーザーのアカウント情報をデータベースから取得するか、また情報を取得できない場合には URI 解析の一端として計算をどう行うかを、うまく説明しています。
Ranvier アプリケーションの中心となる関数として、root オブジェクトを定義する create_application() 関数があります。root オブジェクトは多くの場合 (そしてこのデモの場合も)、Folder クラスのインスタンスであり、URI パスにネストしたコンポーネントを指すキーワード引数のコレクションによって初期化されます。一部のコードを見ると、これがもっと明確にわかります。以下に示すのは Ranvier のディストリビューションの demoapp.py の中に含まれている内容を要約したものです。
Ranvier での ルート URI ツリーの定義
root = Folder(
_default='home',
home=Home(),
resources=EnumResource(mapper),
users=UsernameRoot(Folder(username=PrintUsername(),
name=PrintName(),
data=UserData())),
)
|
/home という相対 URI は、demoapp.py の中で定義される Home(LeafResource) というクラスによって定義されています。要約すると、これは単純に、拡張したテンプレートを要求側に書き込む方法なのです。このテンプレートに含まれるものの中には、mapurl("@@Home") など、計算された URI があります。Ranvier はさまざまなものをソートしてくれるため、このようにして提供される URI は単に http://mysite.com/home/ そのものです。他の URI も同様にして計算されます。EnumResources は Ranvier に組み込まれており、上記の定義の /resources で利用できるようになります。
非常に興味深い点は、パス・コンポーネント /users の定義です。/users はクラス UsernameRoot(VarDelegatorResource) によって定義されます。では、このコードを見てみましょう。
VarDelegatorResource ハンドラー・クラス
class UsernameRoot(VarDelegatorResource):
def __init__(self, next, **kwds):
VarDelegatorResource.__init__(self, 'username', next, **kwds)
def handle(self, ctxt):
if not re.match('[a-z]+$', ctxt.username):
return ctxt.response.errorNotFound()
ctxt.name = 'Mr or Mrs %s' % ctxt.username.capitalize()
|
属性 .username は、このクラスの .__init__() メソッド内のコンテキスト・オブジェクトに割り当てられています。このコンテキスト・オブジェクトは、その次のパス・コンポーネントも利用します。属性 ctxt.username は、このクラスの .handle() メソッドの中で、主に別のコンテキスト属性 ctxt.name を計算するために利用されます (ここでは些細な計算ですが、データベースにアクセスする際の計算を想像してみてください)。
UsernameRoot が行う作業の大部分が別の Folder を含むための作業であることに注目してください。このネストされたフォルダーは、パス・コンポーネント username、name、そして data を含んでいます。これによって、users/mertz/username や /users/blais/data/keyname、/users/smith/name のような URI パスを提供することができます。次にクラス PrintName を見てください。UsernameRoot は /users/username コンポーネントを利用した後、この PrintName クラスに委任されます。
LeafResource ハンドラー・クラス
class PrintName(LeafResource):
def handle(self, ctxt):
ctxt.page.render_header(ctxt)
ctxt.response.write('''<p>The user\'s name is %(name)s.</p>''' %
{'name': ctxt.name})
ctxt.page.render_footer(ctxt)
|
PrintName が行うことで唯一注目に値することは、コンテキスト属性 ctxt.name を利用していることです。もし完全な Web アプリケーション・フレームワークに統合されたとすると、この値はおそらく、このデモで使われている単純なテンプレートの中ではなく、一般的なテンプレート言語の中で使われることになるでしょう。
デモから抜粋したこの短い例は、Ranvier を使って実現できることの、ごく一部を示しているにすぎません。またハンドラーから URI への逆方向のマッピングについては、ほんの少ししか触れまていません。
概念的な問題
私は、Ranvier の作成者である仲間の Martin Blais が作成したコードのスマートさは気に入っていますが、Ranvier によってマップされる URI の全体的な設計には少し概念上のギャップがあると思っています。その問題を具体的に言うと、RFC 3986 形式の URI と、より「一般的に」慣れ親しんでいる URI のパス・コンポーネントとの間のミスマッチです。
具体的に示すために、Ranvier のドキュメンテーションの中にある例を再掲しましょう。Martin は以下のように単純なURI を取り上げています。
http://example.com/get_balance?user=blais&acc=64
このURI は以下のような関数によって実装することができます。
def get_balance(user, accno):
"Lookup balance based on user and account number"
|
Ranvier を使うと、すべてのパス・コンポーネントが引数として動作する、この URI の別の形式を容易に作成することができます (下記)。
http://example.com/user/blais/acc/64/get_balance/
Martin はドキュメンテーションの中で、この別形式の URI について以下のようにコメントしています。
これによって、より適切な URL が作成されるようになり、ユーザーは彼らがサーバー上で提供するリソースを概念的に階層構造で構成するようになります。クエリー引数はオプションの引数用に優先予約されています。
しかし私の意見では、URI 形式の変換で行われることは、オプションとしての引数と必須の引数とを概念的に分離することではありません。結局のところ Ranvier のハンドラーは、(オプションとしての) パス・コンポーネントの数が変わっても、パス・コンポーネントを完璧に利用できるのです。パス・コンポーネントとクエリー・パラメーターとで実際に異なる部分は、階層構造の引数と不規則に並べられた引数との違いにあります。大まかに言って、Ranvier による、このタイプの URI マッピングでは、非階層構造の引数が階層構造のパスの中に単純に埋め込まれます。これは実際には、& 記号、等号、そして疑問符よりもスラッシュが望ましい、という構文上の好みを表現しているにすぎません。ある種の文字群をスタイルの面から要求することは非常に中立的なことですが、RFC 3986 では既に、パスによる URI 部分とクエリーによる URI 部分という形で、階層構造との引数と不規則に並べられた引数との望ましい区別が記述されているのです。なぜ、ある特定の部分を別の部分に押し込もうとするのでしょうか。一般的に区別が誤解されているという事実があるというだけでは、私には理由として貧弱に思えます。
とは言っても、確かに実際のケースでは、ドメインのリソースがはセマンティックな階層構造になっており、単に実装に使われる開発フレームワークやツールの特別な事情を反映した階層構造ではないことがよくあります。Ranvier を使うことで、URI 処理の機能面を複数の再利用可能なコード・ブロックに割り当てるための構成を柔軟に行えるようになります。
参考文献 学ぶために
製品や技術を入手するために
著者について
記事の評価
|