Rust による外部 REST サービスとの非同期通信実装

JSONPlaceholder(https://jsonplaceholder.typicode.com/)は、RESTful インターフェースの動作検証に特化した無料のモック API サービスです。本稿では、Rust を用いてこのサービスと連携し、CRUD 操作を非同期で実行する方法を解説します。

対象エンドポイントとデータ構造

以下のエンドポイントを対象とします:

  • GET /posts/1 — 単一投稿の取得
  • GET /posts — 投稿一覧の取得(上限2件)
  • POST /posts — 新規投稿の作成
  • PUT /posts/1 — 投稿の完全更新
  • DELETE /posts/1 — 投稿の削除

レスポンスはすべて JSON 形式で、各投稿オブジェクトは以下のようなスキーマに従います:

{
  "userId": 1,
  "id": 1,
  "title": "example title",
  "body": "example content"
}

依存関係の設定

Cargo.toml に以下の依存を追加します:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
tokio = { version = "1.0", features = ["full"] }

型定義とクライアント実装

データのシリアライズ/デシリアライズに対応した構造体を定義します:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Article {
    #[serde(rename = "userId")]
    author_id: u32,
    id: u32,
    title: String,
    content: String,
}

HTTP 通信を担う非同期関数群を実装します。各関数はエラーを anyhow::Error で一元管理し、より堅牢なエラーハンドリングを提供します:

use anyhow::Result;
use reqwest::Client;

const API_ROOT: &str = "https://jsonplaceholder.typicode.com";

async fn fetch_single_article() -> Result<String> {
    let response = Client::new()
        .get(format!("{}/posts/1", API_ROOT))
        .send()
        .await?;
    Ok(response.text().await?)
}

async fn fetch_article_object() -> Result<Article> {
    let response = Client::new()
        .get(format!("{}/posts/1", API_ROOT))
        .send()
        .await?;
    Ok(response.json().await?)
}

async fn fetch_first_two_articles() -> Result<Vec<Article>> {
    let response = Client::new()
        .get(format!("{}/posts", API_ROOT))
        .send()
        .await?;
    let all: Vec<Article> = response.json().await?;
    Ok(all.into_iter().take(2).collect())
}

async fn submit_new_article() -> Result<String> {
    let client = Client::new();
    let payload = Article {
        author_id: 10,
        id: 0,
        title: "Rust Integration Test".to_owned(),
        content: "This post was generated via reqwest.".to_owned(),
    };
    
    let response = client
        .post(format!("{}/posts", API_ROOT))
        .json(&payload)
        .send()
        .await?;
    
    Ok(response.text().await?)
}

async fn replace_article() -> Result<String> {
    let client = Client::new();
    let updated = Article {
        author_id: 1,
        id: 1,
        title: "Updated Title".to_owned(),
        content: "Revised content using Rust.".to_owned(),
    };
    
    let response = client
        .put(format!("{}/posts/1", API_ROOT))
        .json(&updated)
        .send()
        .await?;
    
    Ok(response.text().await?)
}

async fn remove_article() -> Result<String> {
    let client = Client::new();
    let response = client
        .delete(format!("{}/posts/1", API_ROOT))
        .send()
        .await?;
    
    Ok(response.text().await?)
}

#[tokio::main]
async fn main() -> Result<()> {
    println!("=== Raw Response ===");
    println!("{}", fetch_single_article().await?);

    println!("\n=== Deserialized Object ===");
    println!("{:#?}", fetch_article_object().await?);

    println!("\n=== First Two Articles ===");
    println!("{:#?}", fetch_first_two_articles().await?);

    println!("\n=== New Article Creation ===");
    println!("{}", submit_new_article().await?);

    println!("\n=== Article Update ===");
    println!("{}", replace_article().await?);

    println!("\n=== Deletion Confirmation ===");
    println!("{}", remove_article().await?);

    Ok(())
}

上記コードでは、reqwest::Client の再利用や anyhow を用いたエラー抽象化、tokio::main マクロによるランタイム統合を採用しています。また、rustls-tls 機能を有効化することで、OpenSSL に依存しない安全な TLS 通信が可能になります。

タグ: rust reqwest serde tokio jsonplaceholder

7月21日 21:38 投稿