⚙️

Rust and Cloudflare Workers

2023/10/29に公開

Rust で REST API を実装する方法を調べる。

業務での実践に近い形にするため、Stateful に実装する。

データの保存は、無課金でやりたいので、D1 を使う。Rust + Workers + D1。

Rust の syntax に慣れていないため、Application での利用パターンを繰り返し実装して慣れる。

今回のコードはこちら。

https://github.com/danny-yamamoto/rust-worker

environment

Cloudflare の template を使用する。

https://developers.cloudflare.com/workers/runtime-apis/webassembly/rust/

Stateful

D1 を準備する。
wrangler で local に D1 のデータを準備する。

wrangler d1 execute test-db \
  --local --command "CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, email_address TEXT, created_at INTEGER, deleted INTEGER, settings TEXT);"

npx wrangler d1 execute test-db \
  --local --command "INSERT INTO users (user_id, email_address, created_at, deleted, settings) VALUES (1, 'maria@example.com', 0, 0, '');"
npx wrangler d1 execute test-db \
  --local --command "INSERT INTO users (user_id, email_address, created_at, deleted, settings) VALUES (999, 'admin@example.com', 0, 0, '');"

npx wrangler d1 execute test-db \
  --local --command "select * from users;"

REST API

  • 実装仕様
    /: 全件取得
    /n: user_id を指定する。
    Router::new()
    .get_async("/:id", |_, ctx| async move {
        let id = match ctx.param("id") {
            Some(value) => value,
            None => {
                eprintln!("Error: id parameter not found");
                "default_id"
            }
        };
        let d1 = ctx.env.d1("DB")?;
        let statement = d1.prepare("select * from users where user_id = ?1");
        let query = statement.bind(&[id.into()])?;
        let res = query.first::<Users>(None).await;
        match res {
            Ok(Some(user)) => {
                let json = serde_json::to_string(&user)
                    .map_err(|e| Error::from(e.to_string()))?;
                Response::ok(json)
            },
            Ok(None) => Response::error("Not Found", 404),
            Err(e) => {
                eprintln!("Database error: {:?}", e);
                Response::error("Internal Server Error", 500)
            },
        }
    })
    .get_async("/", |_, ctx| async move {
        let d1 = ctx.env.d1("DB")?;
        let statement = d1.prepare("select * from users");
        let res = statement.all().await?;
        Response::from_json(&res.results::<Users>().unwrap())
    }).run(request, env).await
vscode ➜ /workspaces/rust-worker (main) $ curl localhost:8787
[{"user_id":1,"email_address":"maria@example.com","created_at":0,"deleted":0,"settings":""},{"user_id":999,"email_address":"admin@example.com","created_at":0,"deleted":0,"settings":"
vscode ➜ /workspaces/rust-worker (main) $ curl localhost:8787/1
{"user_id":1,"email_address":"maria@example.com","created_at":0,"deleted":0,"settings":""}
vscode ➜ /workspaces/rust-worker (main) $ 

Impression

  • Rust の書き方で好感を覚えるのは、match
    if で分岐させる必要はないし、簡潔に書くことが可能。
  • まだ、直感的に書けるところまで来ていない。例えば、namespace の解決や関連関数へのアクセスに用いる :: について、迷う。

GPT-4 コード解説

このRustのコードは、WebAssemblyのために書かれたHTTPリクエストに対応するサーバレス関数を実装しています。クラウドフレアのWorkers(あるいは同様のサーバレスプラットフォーム)で動作することを意図していると思われます。このコードは、特定のエンドポイントへのHTTPリクエストを処理し、データベース(おそらくCloudflare WorkersのD1)からユーザー情報を取得してレスポンスを返します。具体的な部分を説明します。

  1. モジュールとクレートのインポート:
    workerクレートとserdeクレートをインポートしています。workerはWebAssemblyベースのサーバレス環境用のAPIを提供し、serdeはRustでのシリアライズ(データ構造をJSONなどの形式に変換)およびデシリアライズ(その逆の操作)に使用されます。
  2. Users構造体の定義:
    Users構造体は、ユーザー情報を表すためのデータ構造です。DeserializeSerializeトレイトが導出されており、これによりJSON形式との変換が可能になっています。serde(rename = "name")アトリビュートは、JSONキー名とRustフィールド名を対応させています。
  3. main関数(非同期):
    main関数は非同期であり、HTTPリクエストを受け取って処理します。この関数はResult<Response>を返します。
  4. ルーティング:
    HTTPエンドポイントごとに異なるハンドラ関数を定義しています。
    - GET /:id: 指定されたIDに対応するユーザーをデータベースから検索して返します。
    - GET /: すべてのユーザーをデータベースから取得して返します。
  5. データベース処理:
    SQLクエリを使って、D1データベース(Cloudflareのデータベースソリューション)からユーザーデータを取得しています。
  6. エラーハンドリング:
    データベースクエリやデータ取得の際にエラーが発生した場合、適切なHTTPステータスコード(404や500など)とエラーメッセージをレスポンスとして返します。

このコードは、RustでクラウドネイティブのWebアプリケーションを開発する際の一例を示しており、WebAssemblyとRustの組み合わせによる高性能かつセキュアなサーバレスアプリケーション開発を実現しています。

Discussion