Introduction

Finchers Users Guide にようこそ。

Finchers は、非同期性と型安全性を重視したコンポーネント指向のWebフレームワークです。 元々のコンセプトは Scala のライブラリである Finch から着想を得ています。

Finchers では、Endpoint というトレイトを実装したコンポーネントを組み合わせていくことで Web アプリケーションを構築します。 Rust のトレイトに基づく静的なディスパッチにより、これらのコンポーネントを組み合わせることによる実行時コストは多くの場合インライン化されます。

Quickstart

Finchers を用いた Web アプリケーションを試す最も簡単な方法は、プロジェクトのリポジトリをクローンしてサンプルコードを実行することです。 例えば、ToDo アプリの例 (examples/todos)を実行するためには次のようにします。

$ git clone https://github.com/finchers-rs/finchers.git
$ cd finchers
$ cargo +nightly run -p example-todos

More examples are located in the directory examples/.

Finchers では、 futures_api などいくつかの不安定な言語・ライブラリ機能に依存しています。 将来的にこれらが安定化されるまでは、nightly コンパイラを使用する必要があります。 ツールチェインの管理に rustup を使用している場合、次のように nightly コンパイラを使用するよう 設定を上書きしておくことをお勧めします。

$ cd /path/to/user-project
$ rustup override set nightly

Getting Started

WIP

Understanding Endpoint

Finchers では、HTTP アプリケーションにおけるルーティングとリクエストの解析を Endpoint というトレイトにより抽象化しています。 大まかに言うと、このトレイトはクライアントからのリクエストを受け取り、ある型の値を返す(非同期な)関数を表します。

説明のために簡略化した Endpoint の定義を以下に示します。


# #![allow(unused_variables)]
#fn main() {
trait Endpoint<'a> {
    type Output: Tuple;
    type Future: TryFuture<Ok = Self::Output, Error = Error> + 'a;

    fn apply(&'a self, cx: &mut Context<'_>) -> EndpointResult<Self::Future>;
}
#}

多くのものが登場しました。少しずつ見ていきましょう。

このトレイトは、一つのメソッド apply() を持っています。 この中では、クライアントから受信したリクエストの中身を処理し、関連型 Output の値に解決される Future の値を返します。 リクエストの値へのアクセスは Context という構造体を介して行います。 この中には、クライアントからのリクエストの値と同時に Finchers 内部で使用されるパス内の位置情報などのコンテキスト値が格納されています。

関連型 Output はエンドポイント自体の「出力」を表します。 後述するコンビネータを実装する関係で、この関連型はタプル型のみを取るよう制限されています(実際には、この制約は内部トレイト Tuple で表されます)。

Built-in Endpoints

通常、ユーザはこのトレイトの実装を直接記述する必要はありません。 その代わり、あらかじめ組み込まれているコンポーネントを組み合わせていくことで Web アプリケーションを構築していきます。 これは丁度、combine などのパーザコンビネータの働きと類似しています。

path segments:

  • path(segment)
  • param<T>()

body parsing:

  • body::parse::<T>()
  • body::json::<T>()
  • body::urlencoded::<T>()

header:

  • header::required::<T>()
  • header::optional::<T>()

query:

  • query::parse()

Composing Endpoints

上に紹介したエンドポイントは、一般的な Web アプリケーションを構築するために用いることを想定した基本的な機能のみを提供します。 実用的な複雑なアプリケーションを実装するためには、これらのコンポーネントを組み合わせていくことで実現します。 基本的には、次の 3 つの方法でコンポーネントを組み合わせます:

  • 2 つの Endpoint を結合し、それらの結果の直積(タプル)を返す
  • 2 つの Endpoint のうち、リクエストに「よりマッチする」側の出力を返す
  • 結果を別の値に変換する

ライブラリでは、基本的なコンビネータを提供するためのトレイト EndpointExt が用意されており、これをインポートすることで以下のコンビネータが使用可能になります。

Product

コンビネータ and を用いることで、2つのエンドポイントの結果を結合したエンドポイントを作ります。 2つのエンドポイントの結果は、HList という仕組みを用いて単一のタプルに平滑化されます。 これにより、複数のエンドポイントを組み合わせていくことで Output の型が入り組んだものになることを防ぐことが可能になります。


# #![allow(unused_variables)]
#fn main() {
let endpoint1 = path("posts");
let endpoint2 = param::<u32>();

let endpoint = endpoint1.and(endpoint2);
#}

上の例の場合、2 つのエンドポイント(それぞれ (), (u32,)Output に持つ)を組み合わせた結果の型は (u32,) となります。 このような結合を常に可能にするため、Endpoint の関連型 Output が取ることの出来る型がタプル型のみになるような制約が設けられています。

Mapping

結合したエンドポイントの出力は、ビジネスロジックへと渡すことで出力へと「変換」する必要があります。 この変換を実現するためのコンビネータは次のとおりです。 それぞれ

  • e.map(f) - a
  • e.then(f) - a
  • e.and_then(f) - a

and() により平滑化されたタプルからクロージャの引数への変換は、次のように自動的に行われます。


# #![allow(unused_variables)]
#fn main() {
let endpoint = path("posts").and(param()).and(body::parse())
    .map(|id: u32, body: String| {
        format!("id = {}, body = {}", id, body)
    });
#}

Coproduct (or)


# #![allow(unused_variables)]
#fn main() {
let add_post = ...;
let create_post = ...;

let post_api = add_post.or(create_post);
#}

Converting to HTTP Responses

コンビネータにより返された値は、クライアントに送信する前に HTTP レスポンスへと変換する必要があります。 Finchers では、この変換処理を Output というトレイトを用いて抽象化しています。

Output は次のように定義されています。


# #![allow(unused_variables)]
#fn main() {
trait Output {
    type Body;
    type Error;

    fn respond(self, cx: &mut OutputContext<'_>)
        -> Result<Response<Self::Body>, Self::Error>;
}
#}

Error Handling

HttpError

Finchers では、Web アプリケーション内で生じるエラーを HttpError というトレイトを用いて抽象化します。 HttpErrorerror モジュール内で定義されており、そのシグネチャは以下の通りです。


# #![allow(unused_variables)]
#fn main() {
trait HttpError: fmt::Debug + fmt::Display + Send + Sync + 'static {
    fn status_code(&self) -> StatusCode;
    fn headers(&self, h: &mut HeaderMap);
}
#}

このトレイトを実装したエラー値は、同じく error モジュール内にある Error 型へと変換された上でフレームワーク内に保持され、クライアントへのレスポンス構築時にエラーレスポンスへと変換されます。

Recovering

エラー値は通常「例外」としてフレームワーク内で扱われ、レスポンスへの変換は通常自動で行われます。 この挙動をカスタマイズしたい場合、recover というコンビネータを用いることで可能になります。


# #![allow(unused_variables)]
#fn main() {
let endpoint = ...;

endpoint
    .fixed() // ルーティングのエラーを Future として返すようにする
    .recover(|err| -> Result<&'static str, Error> {
        if err.status_code() == StatusCode::NOT_FOUND {
            Ok("not found")
        } else if err.status_code() == StatusCode::BAD_REQUEST {
            Ok("bad request")
        } else {
            Err(err)
        }
    })
#}

recover の返す値は隠蔽されており、その後でユーザ側で使用することは現在禁止しています。

Launching as HTTP Server

WIP