フォームパラメータの取得

このままでは固定のデータを表示しているだけなので、フォームパラメータを取り扱ってみましょう。

serdeの導入

まず、 serde と呼ばれるライブラリを導入します。このライブラリはシリアライズ/デシリアライズを行うことができるライブラリです。

Note: 構造体などのデータ構造を文字列に変換することをシリアライズ、 その逆で文字列をデータ構造に変換することをデシリアライズといいます。

Cargo.toml
[package]
name = "rustwi"
version = "0.1.0"
edition = "2018"

[dependencies]
axum = "0.3"
tokio = { version = "1.0", features = ["full"] }
askama = "0.10"
tower-http = { version = "0.1", features = ["fs"] }
serde = { version = "1.0", feature = ["derive"] }  # 追加

フォームデータ用の構造体の作成

リクエストからフォームデータを取得するために、フォームデータを格納する構造体を作成します。

use serde::Deserialize;

#[derive(Deserialize)]
struct TweetForm {
    name: String,
    message: String,
}

serde::Deserialize トレイトを継承しているため、文字列データからこの構造体を生成する標準的な方法が実装されます。

フォームデータの取り扱い

これでフォームデータを受け取る準備ができました。フォームデータを受け取る方法は簡単で、ハンドラ関数の引数で Form<T> 型の引数を取るように変更するだけです。

use axum::extract::Form;

async fn post_tweets(form: Form<TweetForm>) -> impl IntoResponse {  // Form<T>のTに作成した構造体型を指定する
    HtmlTemplate(IndexView {
        tweets: vec![TweetView {
            name: form.name.clone(),
            message: form.message.clone(),
            posted_at: "2020-01-01 12:34".into(),
        }],
    })
}

これで、フォームデータの値を利用して IndexView を変化させることができるようになりました。

ビューの修正

フォームを作成し、フォームから投稿した内容が IndexView に表示されることを確認してみましょう。

templates/post-tweet.html
<form action="/tweets/new" method="post" class="form">
  <table>
    <tr>
      <td>名前</td>
      <td><input name="name" /></td>
    </tr>
    <tr>
      <td>メッセージ</td>
      <td><input name="message" /></td>
    </tr>
    <tr>
      <td colspan="2"><button type="submit">送信</button></td>
    </tr>
  </table>
</form>
templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Rustwi</title>
  <link rel="stylesheet" href="/static/css/index.css">
</head>
<body>

{% include "post_tweet.html" %}

{% import "tweet.html" as tweet %}
{% for t in tweets %}
{% call tweet::render(t) %}
{% endfor %}

</body>
</html>
src/main.rs
use askama::Template;
use axum::error_handling::HandleErrorExt;
use axum::extract::Form;
use axum::{
    body::{Bytes, Full},
    http::{Response, StatusCode},
    response::{Html, IntoResponse},
    routing::{get, post, service_method_routing},
    Router,
};
use serde::Deserialize;
use std::{convert::Infallible, net::SocketAddr};
use tower_http::services::ServeDir;

#[tokio::main]
async fn main() {
    let app = Router::new()
        .nest("/static", static_contents())
        .route("/", get(index))
        .route("/tweets/new", post(post_tweets));
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn index() -> impl IntoResponse {
    HtmlTemplate(IndexView {
        tweets: vec![
            TweetView {
                name: "太郎".into(),
                message: "こんにちは!".into(),
                posted_at: "2020-01-01 12:34".into(),
            },
            TweetView {
                name: "次郎".into(),
                message: "こんにちは!こんにちは!!".into(),
                posted_at: "2020-01-02 12:34".into(),
            },
        ],
    })
}

async fn post_tweets(form: Form<TweetForm>) -> impl IntoResponse {
    HtmlTemplate(IndexView {
        tweets: vec![TweetView {
            name: form.name.clone(),
            message: form.message.clone(),
            posted_at: "2020-01-01 12:34".into(),
        }],
    })
}

fn static_contents() -> Router {
    Router::new().nest(
        "/",
        service_method_routing::get(ServeDir::new("static")).handle_error(
            |error: std::io::Error| {
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    format!("Unhandled internal error: {}", error),
                )
            },
        ),
    )
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexView {
    tweets: Vec<TweetView>,
}

struct TweetView {
    name: String,
    message: String,
    posted_at: String,
}

#[derive(Deserialize)]
struct TweetForm {
    name: String,
    message: String,
}

struct HtmlTemplate<T>(T);

impl<T> IntoResponse for HtmlTemplate<T>
where
    T: Template,
{
    type Body = Full<Bytes>;
    type BodyError = Infallible;

    fn into_response(self) -> Response<Self::Body> {
        match self.0.render() {
            Ok(html) => Html(html).into_response(),
            Err(err) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Full::from(format!(
                    "Failed to render template. Error: {}",
                    err
                )))
                .unwrap(),
        }
    }
}
static/css/index.css
.form {
  margin-bottom: 24px;
}

.tweet {
  border: 1px solid gray;
}

.tweet:not(:last-child) {
  margin-bottom: 12px;
}

.name {
  font-weight: bold;
  border-bottom: 1px solid gray;
  border-right: 1px solid gray;
}

.posted_at {
  font-size: 80%;
  color: gray;
}