web-sys
/js-sys
のラッパーであるglooを用いてFetch APIを使ってみます.axumでテスト用のサーバーを立て,gloo-net用いてjsonの受け渡しを実装し,wasm-packでテストします.コード全体はこちらにあります.
Fetch APIを利用してJsonのGET/POST
gloo-net
ではserde
のSerialize
, Deserialize
を実装した型を用いてjsonとして受け渡しができるため,以下のように受け渡す型を定義しています.それ以外にderiveするトレイトはテストやサーバー側で使います.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
pub struct Client {
pub id: usize,
pub name: String,
pub location: String,
}
またテストの時とそれ以外でurlとcorsの設定が異なる(テストの時はオリジンの異なるサーバーを利用する)ため,以下のように定数を定義しています.
use gloo_net::http::RequestMode;
const API_DOMAIN: &str = if cfg!(test) {
"http://127.0.0.1:8080/api"
} else {
"/api"
};
const CORS_MODE: RequestMode = if cfg!(test) {
RequestMode::Cors
} else {
RequestMode::SameOrigin
};
またテストを分かりやすくするために以下のエラーを定義しています.
#[derive(thiserror::Error, Debug)]
pub enum FetchError {
// gloo-netのエラー(serdeのエラーを含む)
#[error(transparent)]
GlooNetError(#[from] gloo_net::Error),
// Clientが見つからないときのエラー
#[error("Not Found Error")]
NotFoundError,
// Clientが重複する場合のエラー
#[error("Already Exists Error")]
AlreadyExistsError,
}
jsonを取得してパースする関数は以下となります.Rustの他のhttpクライアントのクレートと同じように使えます.
use gloo_net::http::Request;
pub async fn get_client(id: usize) -> Result<Client, FetchError> {
let response = Request::get(&format!("{}/client/{}", API_DOMAIN, id))
.mode(CORS_MODE)
.send()
.await?;
if response.ok() {
let client = response.json::<Client>().await?;
Ok(client)
} else {
Err(FetchError::NotFoundError)
}
}
リクエストポディにjsonを含めて送信する関数は以下となります.
pub async fn post_client(client: Client) -> Result<(), FetchError> {
let res = Request::post(&format!("{}/client", API_DOMAIN))
.mode(CORS_MODE)
.json(&client)?
.send()
.await?;
if res.ok() {
Ok(())
} else {
Err(FetchError::AlreadyExistsError)
}
}
テスト用サーバー
上で定義した関数を利用するためのサーバーを定義します.簡単のためid
が0のClient
のみを取得でき,重複の判定をしています.またwebdriverを用いてテストを行うため,CORSの設定もしています.
use axum::{
extract::{Json, Path},
http::StatusCode,
routing::{get, post},
Router,
};
use tower_http::cors::{Any, CorsLayer};
use wasm_fetch_example::Client;
async fn get_client_handler(Path(id): Path<usize>) -> (StatusCode, Json<Client>) {
if id == 0 {
let client = Client {
id: 0,
name: "John".to_string(),
location: "NewYork".to_string(),
};
(StatusCode::OK, Json(client))
} else {
(StatusCode::NOT_FOUND, Json(Client::default()))
}
}
async fn post_client_handler(Json(client): Json<Client>) -> StatusCode {
if client.id == 0 {
StatusCode::CONFLICT
} else {
StatusCode::OK
}
}
// CORSの設定
let cors_layer = CorsLayer::new()
.allow_methods(Any)
.allow_headers(Any)
.allow_origin(Any);
let test_app: Router<()> = Router::new()
.route("/api/client", post(post_client_handler))
.route("/api/client/:id", get(get_client_handler))
.layer(cors_layer);
axum::Server::bind(&"127.0.0.1:8080".parse().unwrap())
.serve(test_app.into_make_service())
.await
.unwrap();
テスト
wasm_bindgen_testを用いてテストを書いています.先ほど定義したFetchError
を用いてエラーの判定を行っています.
#[cfg(all(test, target_arch = "wasm32"))]
mod test {
use wasm_bindgen_test::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn test_get_client() {
let res_ok = super::get_client(0).await;
assert!(res_ok.is_ok());
let client = res_ok.unwrap();
assert_eq!(
client,
super::Client {
id: 0,
name: "John".to_string(),
location: "NewYork".to_string(),
}
);
let res_err = super::get_client(1).await;
assert!(res_err.is_err());
}
#[wasm_bindgen_test]
async fn test_post_client() {
let client_ok = super::Client {
id: 1,
name: "Ethan".to_string(),
location: "Washington".to_string(),
};
let res_ok = super::post_client(client_ok).await;
assert!(res_ok.is_ok());
let client_err = super::Client {
id: 0,
name: "John".to_string(),
location: "NewYork".to_string(),
};
let res_err = super::post_client(client_err).await;
assert!(res_err.is_err())
}
}
wasm-packを用いてテストを行います.
wasm-pack test --chrome --headless
test wasm_fetch_example::test::test_post_client ... ok
test wasm_fetch_example::test::test_get_client ... ok
gloo-net
ではserde
を用いて簡単にデータの受け渡しができて便利ですね.工夫すればエラーを受け渡すこともできるため,httpリクエストにおいてもRustの型システムのメリットが発揮できそうです.