Rustのactix-webで複数選択したチェックボックスを扱う

## 概要

RustのWebフレームワーク「actix-web」で、htmlのcheckboxを使ってフォーム入力し、postしようとしたら上手く動かずにハマった…。その際の解決法をご紹介する。

## やりたいこと（最終目標）

html上で下図のようなチェックボックスによる入力フォームを用意し、postメソッドでチェックした情報を送れるようにしたい。以下の例の場合「"りんご"、"みかん"にチェックされました」という情報を送りたい。

![Screenshot from 2021-07-11 10-12-57_cut.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/89cab707-0849-c5de-0d47-3e69f6321827.png)
htmlは（あまり知識がないが）以下のように記述。
属性値を複数選択する場合は、name属性の値の最後に``[]``をつけて配列にする必要がある。

```html:index.html
<!DOCTYPE html>
<html>

<head>
  <title>top page</title>
</head>

<meta charset="utf-8" />

<body>
好きなくだものを選んでください。<br>
<form method="post" action="show_checked">
  <input type="checkbox" name="fruits[]" value="apple">りんご<br>
  <input type="checkbox" name="fruits[]" value="orange">みかん<br>
  <input type="checkbox" name="fruits[]" value="banana">バナナ<br>
  <input type="submit" name="button" value="Send"><br>
</form>

</body>

</html>
```

http(s)://hostname/index.html でチェックボックス選択サイトを表示して、「送信」ボタンを押すと、選択したチェックボックスの情報を保持しつつ、http(s)://hostname/show_checked に遷移するWebサイトを作りたい。

## 環境情報


* 動作確認したバージョン
    * rustc : 1.51.0
    * cargo : 1.51.0

使うソースの構成は下記。

```
.
├── Cargo.toml
├── src
│   └── main.rs
└── templates
    └── index.html
```

## 手始め：属性値を一つ指定する場合の実装

まずは、ラジオボタンを使って属性値を一つを選択するWebサービスを実装する。

### templates/index.htmlの中身

body部分のみ記載。デフォルトは「りんご」が選択された状態になっている。
（ここではname属性の値には``[]``をつけずにスカラー値にしていることに注意）

```html:templates/index.html
好きなくだものを一つ選んでください。<br>
<form method="post" action="show_checked">
  <input type="radio" name="fruits" checked="checked" value="apple">りんご<br>
  <input type="radio" name="fruits" value="orange">みかん<br>
  <input type="radio" name="fruits" value="banana">バナナ<br>
  <input type="submit" name="button" value="Send"><br>
</form>
```

### Cargo.tomlの中身

```toml:Cargo.toml
[package]
name = "webform_single"
version = "0.1.0"
authors = ["auther"]
edition = "2018"

[dependencies]
actix-web = "3.3.2"
askama = "0.10.5"
serde = "1.0.126"
```

* actix-web
    * Webフレームワークとして利用
* askama
    * ``templates/index.html``をプログラムで読むために利用
    * askamaを使うとPythonのJinja2テンプレートのような感じで、htmlファイル内に変数を指定することも可能だが、今回はその機能は使わない。
* serde
    * Rustのシリアライズ／デシリアライズするツール
    * ラジオボタンやチェックボックスを選択してフォーム送信すると、元のデータは「fruits=apple&button=Send」のようなクエリ文字列になっているが、これを自動でRustの構造体に変換してくれる模様

### src/main.rsの中身

以下ソースは、**属性fruitsの値を一つだけ指定する場合のみ**に動く。

```rs:src/main.rs
use actix_web::{App,HttpServer,get,post,web,HttpResponse};
use askama::Template;
use serde::Deserialize;

// index.htmlのテンプレート定義
#[derive(Template)]
#[template(path = "index.html")]
// htmlテンプレートに変数指定する場合は、構造体メンバーを定義することも可能（今回は使わない）
struct IndexTemplate;

/// index.htmlのbodyを返す
#[get("/index.html")]
async fn index() -> HttpResponse {
  let html = IndexTemplate {};
  let response_body = html.render().unwrap();
  HttpResponse::Ok()
    .content_type("text/html")
    .body(response_body)
}

// 入力フォーム用の構造体定義
// index.htmlのラジオボタンのタグで指定した名前(name=fruits)とメンバ名を合わせる必要あり
#[derive(Deserialize)]
pub struct FormDataCheckBox {
  pub fruits: String,
}

/// チェックボックスのフォームで入力した情報を返す
#[post("/show_checked")]
async fn show_checked(
  form: web::Form<FormDataCheckBox>,
) -> HttpResponse {

  // レスポンスで、指定したチェックボックスのvalueの属性値が返る
  HttpResponse::Ok()
    .content_type("text/html")
    .body(&form.fruits)
}

#[actix_web::main]
async fn main() -> Result<(), actix_web::Error> {
  // Webサーバを起動
  HttpServer::new(move ||
    App::new()
    .service(index)
    .service(show_checked)
  )
  .bind("0.0.0.0:8080")?
  .run()
  .await?;

  Ok(())
}
```

上記ソースをcargo run等で実行後、Webブラウザでindex.htmlにアクセスしてフォーム送信すると、ラジオボタンで選択した果物に相当する値(value属性)がレスポンスで返るのを確認できる。
curlコマンドで再現すると、以下のようになる。

```
$ curl -X POST -d "fruits=apple&button=Send" "http://[ホスト名]:8080/show_checked"
apple
```

## 本題：属性値を配列にする場合の実装

入力フォームの属性値を配列で受け取る場合、先の例でフォームを受け取るのに使った``web::Form<FormDataCheckBox>``構造体のジェネリック部分(FormDataCheckBox)を次のようにfruitsをVec型で定義すれば動きそう！
と最初は思った。

```rs
#[derive(Deserialize)]
pub struct FormDataCheckBox {
  pub fruits: Vec<String>,
}
```

だが実際にやってみるとビルドは通るが、レスポンスが``Parse error``となって動かないことが判明…。調べると、現時点ではserdeのDeserializeが配列タイプの属性値に対応していないのが原因の模様。

参考記事 : https://banatech.net/blog/view/46

そこで、次の手順で解決する。

* 入力フォームのリクエストbodyをactix-webの``web::Payload``型でそのまま受け取る
* 受け取ったbodyを、serde_qsを使ってパースする

ソース構成（再掲）は下記。

```
.
├── Cargo.toml
├── src
│   └── main.rs
└── templates
    └── index.html
```

これらのファイルの中身を以降に示す。

### templates/index.htmlの中身

body部分のみ記載。既に説明済だが、チェックボックスでの複数選択に対応できるようname属性の値の最後に``[]``をつけている。

```html:templates/index.html
好きなくだものを選んでください。<br>
<form method="post" action="show_checked">
  <input type="checkbox" name="fruits[]" value="apple">りんご<br>
  <input type="checkbox" name="fruits[]" value="orange">みかん<br>
  <input type="checkbox" name="fruits[]" value="banana">バナナ<br>
  <input type="submit" name="button" value="Send"><br>
</form>
```

### Cargo.tomlの中身

```toml:Cargo.toml
[package]
name = "webform_multi"
version = "0.1.0"
authors = ["auther"]
edition = "2018"

[dependencies]
actix-web = "3.3.2"
askama = "0.10.5"
serde = "1.0.126"

# 以下を追加
serde_qs = "0.8.4"
futures-util = "0.3.15"
```

* serde_qs
    * URLのクエリ文字列(QueryString)に特化したシリアライズ／デシリアライズツール。
* futures-util
    * リクエストのbodyを連結する際に必要となる模様。（非同期処理に対応するために必要？）

### src/main.rsの中身

```rs:src/main.rs
use actix_web::{App,HttpServer,get,post,web,HttpResponse};
use askama::Template;
use serde::Deserialize;

// 追加パッケージ
use serde_qs;
use futures_util::StreamExt;

// index.htmlのテンプレート定義
#[derive(Template)]
#[template(path = "index.html")]
// htmlテンプレートに変数指定する場合は、構造体メンバーを定義することも可能（今回は使わない）
struct IndexTemplate;

/// index.htmlのbodyを返す
#[get("/index.html")]
async fn index() -> HttpResponse {
  let html = IndexTemplate {};
  let response_body = html.render().unwrap();
  HttpResponse::Ok()
    .content_type("text/html")
    .body(response_body)
}

// 入力フォーム用の構造体定義
// index.htmlのチェックボックスのタグで指定した名前(name=fruits[])とメンバ名を合わせる必要あり
#[derive(Deserialize)]
pub struct FormDataCheckBox {
  pub fruits: Vec<String>,
}

// チェックボックスのフォームで入力した情報を返す
#[post("/show_checked")]
async fn show_checked(
  mut body: web::Payload,
) -> HttpResponse {

  // bodyをbyte型の配列に変換する
  let mut bytes = web::BytesMut::new();
  while let Some(item) = body.next().await {
    bytes.extend_from_slice(&item.unwrap());
  }

  // byte型配列を文字列に変換
  let query_string
    = bytes.iter().map(|&s| s as char).collect::<String>();

  // クエリ文字列からfruitsの属性値の配列を抽出する
  // 括弧はそれぞれ"%5B","%5D"となっているので予め変更しておく
  let sqs = serde_qs::from_str::<FormDataCheckBox>(
    &query_string
    .replace("%5B", "[")
    .replace("%5D", "]")
  );

  let fruits = match &sqs {
    // 一つもチェックされていない場合は空のVec<String>を作る
    Err(_) => Vec::<String>::new(),
    Ok(v) => v.fruits.iter()
             .map(|c| c.to_string())
             .collect::<Vec<String>>()
  };

  // チェックされた属性値を配列として表示
  HttpResponse::Ok()
    .content_type("text/html")
    .body(format!("{:?}", fruits))
}

#[actix_web::main]
async fn main() -> Result<(), actix_web::Error> {
  // Webサーバを起動
  HttpServer::new(move ||
    App::new()
    .service(index)
    .service(show_checked)
  )
  .bind("0.0.0.0:8080")?
  .run()
  .await?;

  Ok(())
}
```

cargo run等で実行後、Webブラウザでindex.htmlにアクセスしてフォーム送信して挙動を確認いただきたい。
curlコマンドでも再現可能で、以下のような挙動になる。

* Webブラウザで「りんご」「バナナ」を選択した場合の再現

```
$ curl -X POST -d "fruits[]=apple&fruits[]=banana&button=Send" "http://[ホスト名]:8080/show_checked" 
["apple", "banana"]
```

* Webブラウザで何も選択しない場合の再現

```
$ curl -X POST -d "button=Send" "http://[ホスト名]:8080/show_checked"
[]
```