[Rust] WASMでFetch API

`web-sys`/`js-sys`のラッパーである[gloo](https://github.com/rustwasm/gloo)を用いてFetch APIを使ってみます．[axum](https://github.com/tokio-rs/axum)でテスト用のサーバーを立て，[gloo-net](https://github.com/rustwasm/gloo/tree/master/crates/net)用いてjsonの受け渡しを実装し，wasm-packでテストします．コード全体は[こちら](https://github.com/deepgreenAN/wasm-fetch-example)にあります．

## Fetch APIを利用してJsonのGET/POST
`gloo-net`では`serde`の`Serialize`, `Deserialize`を実装した型を用いてjsonとして受け渡しができるため，以下のように受け渡す型を定義しています．それ以外にderiveするトレイトはテストやサーバー側で使います．
```rust
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の設定が異なる(テストの時はオリジンの異なるサーバーを利用する)ため，以下のように定数を定義しています．
```rust
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
};
```
またテストを分かりやすくするために以下のエラーを定義しています．
```rust
#[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クライアントのクレートと同じように使えます．
```rust
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を含めて送信する関数は以下となります．
```rust
 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の設定もしています．
```rust:bin/test_server.rs
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](https://github.com/rustwasm/wasm-bindgen)を用いてテストを書いています．先ほど定義した`FetchError`を用いてエラーの判定を行っています．
```rust
#[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の型システムのメリットが発揮できそうです．