RustのWebアプリケーションフレームワーク「actix-web」の入門として、基本的なAPIの作成方法を学びましょう。この記事では、actix-webを使ってREST APIを実装する基本的なステップを説明します。まず、依存関係の設定から始め、GETリクエストとPOSTリクエストの処理方法を見ていきます。
依存関係に「actix-web」を追加する
Rustのプロジェクトでactix-webを使用するには、まずCargo.tomlに必要な依存関係を追加する必要があります。serdeとserde_jsonは、Rustでデータのシリアライズとデシリアライズを扱うためのクレートです。これらは、JSON形式でのデータの送受信に必要です。
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
main関数
actix-webを使ったアプリケーションのエントリーポイントはmain関数です。以下のコードを参考に、HttpServerを作成し、特定のアドレスとポートにバインドしてサービスを開始します。
use actix_web::{web, get, post, App, HttpResponse, HttpServer, Responder};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            //ここにAPI関数の一覧を定義していく
            .service(get_todos)
            .service(get_todo)
            .service(post_todo)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
GETリクエストのAPIを実装する
actix-webでは、パラメータなしのGETリクエスト、クエリパラメータを受け取るGETリクエスト、およびパスパラメータを受け取るGETリクエストを簡単に実装できます。ここでは、それぞれのタイプに対するサンプル実装を見ていきましょう。
属性を使ってルートを定義する
actix-webでは、#[get("path")]という属性を使って、特定のパスにGETリクエストがあった際に呼び出される関数を簡単に定義できます。これにより、ルーティングの設定が直感的に行えます。
例えば、/todosのパスにGETリクエストがあった場合に全てのToDo項目を返すAPIエンドポイントを作成するには、以下のようにします。
#[get("/todos")]
async fn get_todos() -> impl Responder {
    "全てのToDoアイテムを返す"
}
クエリパラメータを受け取る
クエリパラメータを受け取る方法として、次の2つのアプローチがあります。
HashMapとして受けとる
以下の例では、/todos?keyword=xxx&complete=yといったURLからクエリパラメータを取得します。
#[get("/todos")]
async fn get_todos_with_query(info: web::Query<HashMap<String, String>>) -> impl Responder {
    // 「」
    println!("keyword={}", info.get("keyword").unwrap_or(&String::from("")));
    println!("complete={}", info.get("complete").unwrap_or(&String::from("")));
    "条件にマッチするToDoアイテムを返す"
}
構造体にパースして受け取る
クエリパラメータを構造体に直接パースする方法もあります。これにより、パラメータの処理がより型安全になり、コードの可読性も向上します。
#[derive(Deserialize)]
struct Info {
    keyword: String,
    complete: String,
}
#[get("/todos")]
async fn get_todos_with_query(info: web::Query<Info>) -> impl Responder {
    println!("keyword={}", &info.keyword);
    println!("complete={}", &info.complete);
    "条件にマッチするToDoアイテムを返す"
}
パスパラメータを受ける
パスパラメータを受け取ることで、URLの一部を動的に指定し、それに応じた処理を行うことができます。
actix-webでは、パスパラメータを関数の引数として簡単に取得できます。
Tupleとして受け取る
以下の関数は、/todos/{id}/{action}のようなパスからidとactionパラメータを取得します。
{}内は動的な部分で、URLのこの部分にどんな値が来ても、その値が対応する関数の引数に渡されます。
#[get("/todos/{id}/{action}")]
async fn get_todo_by_id(path: web::Path<(u32, String)>) -> impl Responder {
    let (id, action) = path.into_inner();
    format!("id: {0}, action: {1}", id, action)
}
構造体にパースして受け取る
パスパラメータの値を、構造体にパースして取得方法もあります。
構造体にパースして受け取る方法は、値の取得を型安全に行い、コードの整理に役立ちます。
#[derive(Deserialize)]
struct PathInfo {
    id: u32,
    action: String,
}
#[get("/todos/{id}/{action}")]
async fn get_todo_by_id(path: web::Path<PathInfo>) -> impl Responder {
    format!("id: {0}, action: {1}", path.id, path.action)
}
POSTリクエストのAPIを実装する
actix-webでは、POSTパラメータもシンプルなコードで受け取れます。
単一のパラメータ受け取る
単一のパラメータを受け取るには、リクエストのボディから直接値を抽出します。以下は、クライアントから送信されたテキストデータを受け取るシンプルな例です。
#[post("/echo")]
async fn echo(body: String) -> impl Responder {
    format!("Received: {}", body)
}
キーと名前のURL-Encoded Forms形式のパラメータを受けとる
フォームから送信されたデータを受け取るには、web::Formを使用します。これにより、FORMデータが構造体にマッピングされ、アクセスが容易になります。
#[derive(Deserialize, Debug)]
struct Todo {
    id: u32,
    text: String,
    completed: bool,
}
#[post("/todos")]
async fn post_todo(form: web::Form<Todo>) -> impl Responder {
    format!("Welcome {:?}!", form)
}
JSON形式パラメータを受けとる
JSON形式で送信されたデータを扱う場合、web::Jsonを使用すると便利です。これを使うと、送信されたJSONデータが直接構造体にデシリアライズされます。
#[post("/todos")]
async fn post_todo(json: web::Json<Todo>) -> impl Responder {
    format!("Welcome {:?}!", json)
}
ファイルを受けとる
ファイルのアップロードを処理するには、actix_multipart::Multipartを用いることで、フォームデータとして送信されたファイルを受け取れます。この例では、アップロードされたファイルを処理する基本的な方法を示します。
まず「actix_multipart」と「futures-util 」の依存関係をCargo.tomlに追加します。
[dependencies]
actix-multipart = "0.6.1"
futures-util = "0.3"
ファイルのアップロードを処理するには、actix_multipart::Multipartを用いることで、フォームデータとして送信されたファイルを受け取れます。
以下のコードは、アップロードされたファイルを処理する基本的な方法です。
use actix_multipart::Multipart;
use futures_util::StreamExt as _;
#[post("/upload")]
async fn upload(mut payload: Multipart) -> Result<impl Responder, Error> {
    let save_path = Path::new("/Users/tera/Temp");
    while let Some(item) = payload.next().await {
        let mut field = item?;
        // Content-Dispositionヘッダーからファイル名を取得
        let content_disposition = field.content_disposition();
        let filename = content_disposition.get_filename().unwrap();
        let filepath = save_path.join(filename);
        // ファイルオープン(新規作成または上書き)
        let mut f = File::create(filepath).unwrap();
        // フィールドのデータをファイルに書き込み
        while let Some(chunk) = field.next().await {
            let data = chunk.unwrap();
            f.write_all(&data).unwrap();
        }
    }
    Ok("uploaded")
}
上記APIにファイルをアップロードするCURLコマンドは次のとおりです。
curl -X POST http://localhost:8080/upload \
     -H "Content-Type: multipart/form-data" \
     -F "my_file=@/path/to/file;type=application/octet-stream"
レスポンスを返す
他の一般的なWEBフレームワークと同様に、actix-webを使ったWebアプリでは、クライアントに対してさまざまな形式のレスポンスを返すことができます。
シンプルな文字列のレスポンスを返す
最も基本的なレスポンスタイプは、単純な文字列です。以下の例では、クライアントに対して単一の文字列を返しています。
#[get("/hello2")]
async fn hello2() -> impl Responder {
    "初めてのレスポンス"
}
JSON形式のレスポンスを返す
Web APIを構築する際によく利用されるのが、JSON形式のレスポンスです。actix-webではweb::Jsonを使用して、Rustの構造体をJSONにシリアライズし、クライアントに返すことができます。
#[derive(Serialize)]
struct MyObj {
    name: String,
    number: i32,
}
async fn get_json() -> impl Responder {
    web::Json(MyObj {
        name: "John Doe".to_string(),
        number: 123,
    })
}
この関数は、MyObj構造体をJSON形式でシリアライズし、それをHTTPレスポンスとして返します。
ファイル(ダウンロード用)をレスポンスを返す
ファイルをクライアントにダウンロードさせたい場合、actix-filesのNamedFileを利用して簡単に実現できます。この機能を使うことで、サーバー上のファイルをHTTPレスポンスとして送信し、クライアントにダウンロードさせることが可能です。
まず「actix-files」の依存関係をCargo.tomlに追加します。
[dependencies]
actix-files = "0.6.5"
以下は、WEBサーバー上のファイルを、ダウンロードとしてクライアントに提供する例です。
content-typeなどは、ファイルの拡張子から自動判断してレスポンスヘッダに指定してくれるため、非常にシンプルな実装でファイルダウンロードを実装できます。
#[get("/download_file")]
async fn download_file() -> actix_web::Result<NamedFile> {
    Ok(NamedFile::open("/path/to/file")?)
}
まとめ
actix-webの基本的な使用方法を紹介しました。
メモリ安全・省メモリが特徴のRustは、今後API開発でもその進化を発揮するでしょう。

0 件のコメント:
コメントを投稿