2
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-07-12

概要

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

やりたいこと(最終目標)

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

Screenshot from 2021-07-11 10-12-57_cut.png
htmlは(あまり知識がないが)以下のように記述。
属性値を複数選択する場合は、name属性の値の最後に[]をつけて配列にする必要がある。

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属性の値には[]をつけずにスカラー値にしていることに注意)

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の中身

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の値を一つだけ指定する場合のみに動く。

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型で定義すれば動きそう!
と最初は思った。

#[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属性の値の最後に[]をつけている。

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の中身

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の中身

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"
[]
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1