RustでWebアプリを初めて開発する
Rustプログラミング入門の第5章に沿って入門。第1章〜第4章は、ざっと目を通したレベルで着手。
実施日: 2020年12月25日
Rust version: 1.48
[ 20-12-25 17:39 ] ~/workspace/srictf
osada@mbp20i% cargo add actix-web actix-rt
error: no such subcommand: `add`
Did you mean `b`?
素直にやると、cargo addがないと怒られる。
対応として、cargo-editをインストールする(これは2章に書いてある内容)
[ 20-12-25 17:45 ] ~/workspace/srictf
osada@mbp20i% cargo install cargo-edit
Updating crates.io index
Downloaded cargo-edit v0.7.0
(省略)
Installed package `cargo-edit v0.7.0` (executables `cargo-add`, `cargo-rm`, `cargo-upgrade`)
こうすると、cargo addなどが使えるようになる。
[ 20-12-25 17:48 ] ~/workspace/srictf
osada@mbp20i% cargo add actix-web actix-rt
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding actix-web v3.3.2 to dependencies
Adding actix-rt v1.1.1 to dependencies
セミコロンの使い方
Wikipediaによると、以下とのこと。
ブロックコード内の命令文のセパレータにはセミコロン(;)を用いるが、C言語のそれと異なりRustのセミコロンは直前の命令文がブロックコードで括られる式の途中式であることを宣言するためのものである。セミコロンを末尾に置かない命令文はブロックコードの最終的な評価式として扱われ、その式の結果がブロックコードの外へ戻り値として返される[30]。
なるほど。セミコロンがないと、その関数の戻り値となるのね。
ちょっとrubyに似ている?
HTTPサーバのサンプルコード
use actix_web::{get, App, HttpResponse, HttpServer};
#[get("/")]
async fn index() -> Result<HttpResponse, actix_web::Error> {
let response_body = "Hello world!";
Ok(HttpResponse::Ok().body(response_body))
}
#[actix_rt::main]
async fn main() -> Result<(), actix_web::Error> {
HttpServer::new(move || App::new().service(index))
.bind("0.0.0.0:8080")?
.run()
.await?;
Ok(())
}
IDE(IntelliJのplugin)で書くと楽チン。
[ 20-12-25 19:09 ] ~/workspace/srictf
osada@mbp20i% cargo run
Compiling srictf v0.1.0 (/Users/osada/workspace/srictf)
Finished dev [unoptimized + debuginfo] target(s) in 6.45s
Running `target/debug/srictf`
初回起動時は、色々なものがインストールされる。
Cargoのすごいところなんだろう(よくわからん)
IDE IntelliJ Rust
Standard libraryがおかしいと言われる。よくわからない。。
きっと、コード補完やハイライトがおかしいのはこのせいなんだろうな。
10:23 Cargo project update failed:
corrupted standard library: /Users/osada/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src
色々調べてたが、Answer1では解決しなかった。(via rustupが, IntelliJ 2019.3ではない模様)
シンプルなViewテンプレート
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SRI CTF</title>
</head>
<body>
<h1>SRI CTF</h1>
<div>
{% for entry in entries %}
<div>
<div>id: {{ entry.id }}, text: {{ entry.text }}</div>
<form action="/answer" method="post">
<input type="hidden" name="id" value="{{ entry.id }}">
Flag:
<input type="text" name="answer">
<button>Answer</button>
</form>
</div>
{% endfor %}
</div>
<hr>
<h2>Add Question</h2>
<form action="/add" method="post">
<div>
Question: <input type="text" name="text">
</div>
<div>
Flag: <input type="text" name="flag">
</div>
<div>
<button>Add Question</button>
</div>
</form>
</body>
</html>
これを後ろ側で動かす仕組み
use actix_web::{get, http::header, post, web, App, HttpResponse, HttpServer, ResponseError};
use askama::Template;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;
use serde::Deserialize;
use thiserror::Error;
#[derive(Deserialize)]
struct AddParams {
text: String,
flag: String,
}
#[derive(Deserialize)]
struct AnswerParams {
id: u32,
text: String,
}
// text -> flag? or explain?
struct QuestionEntry {
id: u32,
text: String,
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
entries: Vec<QuestionEntry>,
}
#[derive(Error, Debug)]
enum MyError {
#[error("failed to render HTML")]
AskamaError(#[from] askama::Error),
#[error("failed to get connection")]
ConnectionPoolError(#[from] r2d2::Error),
#[error("failed SQL execution")]
SQLiteError(#[from] rusqlite::Error),
}
impl ResponseError for MyError {}
// ----------
#[post("/add")]
async fn add_question(
params: web::Form<AddParams>,
db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
let conn = db.get()?;
conn.execute("INSERT INTO question (text, flag) VALUES (?1, ?2)", &[¶ms.text, ¶ms.flag])?;
Ok(HttpResponse::SeeOther()
.header(header::LOCATION, "/")
.finish())
}
#[post("/answer")]
async fn answer_question(
params: web::Form<AnswerParams>,
db: web::Data<r2d2::Pool<SqliteConnectionManager>>,
) -> Result<HttpResponse, MyError> {
let conn = db.get()?;
// Todo
// let mut statement = conn.prepare("SELECT COUNT(*) FROM question WHERE id = ? AND flag = ?",
// &[¶ms.id], &[¶ms.text])?;
// Do nothing
Ok(HttpResponse::SeeOther()
.header(header::LOCATION, "/")
.finish())
}
#[get("/")]
async fn index(db: web::Data<Pool<SqliteConnectionManager>>) -> Result<HttpResponse, MyError> {
// DB接続
let conn = db.get()?;
let mut statement = conn.prepare("SELECT id, text FROM question")?;
// DBクエリ結果をrowsに収納。ラベル付けも同時にする。
let rows = statement.query_map(params![], |row| {
let id = row.get(0)?;
let text = row.get(1)?;
Ok(QuestionEntry {id, text})
})?;
let mut entries = Vec::new();
for row in rows {
entries.push(row?);
}
// htmlテンプレートにentriesを渡す。
let html = IndexTemplate { entries };
let response_body = html.render()?;
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(response_body))
}
#[actix_rt::main]
async fn main() -> Result<(), actix_web::Error> {
let manager = SqliteConnectionManager::file("srictf.db");
let pool = Pool::new(manager).expect("Failed to initialize the connection pool.");
let conn = pool
.get()
.expect("Failed to get the connection from the pool");
conn.execute(
"CREATE TABLE IF NOT EXISTS question (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
flag TEXT NOT NULL
)",
params![],)
.expect("Failed to create a table `question`.");
HttpServer::new(move || {
App::new()
.service(index)
.service(add_question)
.service(answer_question)
.data(pool.clone())
})
.bind("0.0.0.0:8080")?
.run()
.await?;
Ok(())
}
Answer部分はこれから実装。
rusqliteは、このサイトを参考にした。
ToDo 2020-12-27
- デバッグ方法
- rusqliteの状態(特にselectの検索とRustの内部処理のバランス)
- Dockerイメージ化
- IntelliJのエラー対応
Errorハンドリング
サンプルコードの解説(書店サイト)
サンプルコードのgithub.comこれ、DBをメモリにしか持たないので、rusqliteを組み込もう。
コンソールでのdebugは、安直に以下で良いぽい。
println!("hello");
dbg!("This is debug");
こんな感じ。
hello
[src/main.rs:55] "This is debug" = "This is debug"
世の中のチュートリアル
本だけではよくわからない。というわけで、評価されているサイトを巡回中。
入門。rusqliteではないけど、わかりやすい。
上記サイトでオススメしていた公式の実装例
DIESEL?(DB操作のライブラリ)をチュートリアル。
Parse Errorの対応方法
- templateのformで渡す変数名と、paramsで受け取る変数名があっているか確認しよう。
てなわけで、rusqliteで行くことにする。
executeやquery_mapで引数の渡し方も、なんとなくわかってきた。
(仕組みは説明できないけど)
プレースホルダ利用時のquery
プレースホルダに変数を渡すときは、すべてを型指定しておくこと。
中途半端に指定すると、第一引数にすべて型推論されて型不一致時にコンパイルエラーになる。
let mut cached_statement = conn.prepare_cached(
"SELECT id, text, flag FROM question WHERE id = ?1 AND flag = ?2")?;
// これはコンパイルできる
let rows = cached_statement.query_map(&[¶ms.id.to_string(), ¶ms.flag.to_string()], |row| {Ok(1)})?;
// これはコンパイルできない。
// 第2
// let rows = statement.query_map(&[¶ms.id, ¶ms.flag.to_string()], |row| {Ok(1)})?;
コンパイルエラーのときの出力メッセージ
77 | let rows = cached_statement.query_map(&[¶ms.id, ¶ms.flag.to_string()], |row| {Ok(1)})?;
| ^^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found struct `std::string::String`
第一引数がu32で指定しているから、第二引数もu32(本当はString型)でチェックが入る模様。
・・・すごい・・・。
IntelliJ Rust
1.48.0でどうしてもこれが出続ける。
Cargo project update failed: corrupted standard library
これを読むと、雰囲気で、
lib/rustlib/src/rust/library
が正しいらしい。
lib/rustlib/src/rust/src
(llvm-project/libunwindがあるだけ)
ではないらしい。
が、IntelliJ 側で設定を変えられない。なぜだ。
アップデートしてからまだ1ヶ月も立っていなので、このあたりの変更はプラグインが追いついていないような気もする。今日は、深追いは一旦やめる。入門者にコードアシストツールがないのは、なかなかつらいところ。昔のようなプログラミング学習の時代を体験しているようで、ありといえばあり・・・と前向きに捉えることにする。