『Webアプリ開発で学ぶRust言語入門』を読む
『Webアプリ開発で学ぶRust言語入門』を買ったので、読みながら学んだことをアウトプットする
HTTPサーバーを立ち上げる
axumというWebアプリケーションフレームワークを使ってHTTPサーバーを立ち上げる。
Node.js + Expressと同じように、以下のような流れでサーバーを立ち上げる。
- ルーティングの設定
- サーバーを立ち上げる(listenする)
use axum::{routing::{get}, Router};
use std::net::SocketAddr;
let app = Router::new().router("/", get(handler));
let addr = SocketAddr::from([0, 0, 0, 0], 3000);
axum.Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
#[tokio::main]
という属性マクロを使うことで簡単にマルチスレッドランタイムを構築できる。
#[tokio::main]
async fn main() {
...
}
テストを書く
ユニットテストをするとき、大体#[cfg(test)]
という注釈を使う
#[cfg(test)]
が付いている場合、cargo test
が走ったときにだけコンパイルされ、プロダクションではコンパイルされない
#[cfg(test)]
mod tests {
use super::*;
...
}
tests
はmod
と宣言されている通り、モジュールである
しかし内部モジュールであるため、use super::*;
で、外部モジュールで定義したものをテストでも使えるようにしている
スレッドセーフ
複数のスレッドで処理を行う際、同じリソースを扱っても正しい結果が得られるようにしなくてはならない。
そのような状態をスレッドセーフと呼ぶ。
Rustでスレッドセーフな処理を行うには、std::sync::Arc
を用いる。
Arc
で包んだオブジェクトは複数から参照されてもスレッドセーフになる。
本書ではTodoRepositoryForMemory
はデータベースの代わりを果たし複数から参照されうるので、Arc
でラップしてあげる。
より正確に言うと、Arc<RwLock<>>
でラップしている。RwLock
は複数スレッドからのread及び1つのスレッドからのwriteを許可する(つまり複数から一度にwriteされることはない)。安全に書き込みを行うことができる。
pub struct TodoRepositoryForMemory {
store: Arc<RwLock<TodoDatas>>,
}
ファイル分割
すべてのコードをmain.rs
に記述することは現実的ではない。
なのでモジュールを使ってコードを分割する。
同じファイル内でコードを分割する
mod module_a {
pub fn hello_world {
println!("Hello world!");
}
}
fn main() {
module_a::hello_world();
}
mod
キーワードを使ってモジュールを宣言する。
module_a
内の関数hello_world
にpub
キーワードをつけないとアクセスできない。
Rustでは「あらゆる要素は標準で非公開」というルールになっている。
別ファイルにコードを分割する
こんな感じでファイルを分割する。
root
├── main.rs
└── module_a.rs
先ほどのコードを2つのファイルに分ける。
mod module_a; // ここがポイント!
fn main() {
module_a::hello_world();
}
mod module_a {
pub fn hello_world {
println!("Hello world!");
}
}
ポイントはmain.rs
でmod module_a;
と宣言すること。これによりmodule_a
がmain.rs
内で有効になる。
Rustでは、自分で定義したモジュールはルートファイル(main.rs
とか)で宣言してあげないと使うことができない。
useキーワード
毎回module_a::hello_world()
ってやるのしんどい。
use
キーワードを使うことで、宣言済みのモジュールをスコープに持ち込むことができる。
mod module_a;
use module_a::hello_world;
fn main() {
hello_world();
}
use
キーワードを使っても、自作モジュールをルートファイルで宣言する必要はある。
また、パスの指定もできる。
// 絶対パスでの指定
// crateがルートを表す
use crate::front_of_house::hosting::add_to_waitlist;
// 相対パスでの指定
use front_of_house::hosting::add_to_waitlist;
参照
Desctructuring
JSやTSで言うところの分割(Destructuring)代入みたいなこと
JSやTSと同様に、タプルやスライス、構造体などで分割代入できる
(a, b) = (3, 4);
[a, b] = [3, 4];
Struct { x: a, y: b } = Struct { x: 3, y: 4};
// これらは以下の糖衣構文
{
let (_a, _b) = (3, 4);
a = _a;
b = _b;
}
{
let [_a, _b] = [3, 4];
a = _a;
b = _b;
}
{
let Struct { x: _a, y: _b } = Struct { x: 3, y: 4};
a = _a;
b = _b;
}
ネストされててもデストラクチャできる。
let (a, b, c);
((a, b), c) = ((1, 2), 3);
// これは以下の糖衣構文
let (a, b, c);
{
let ((_a, _b), _c) = ((1, 2), 3);
a = _a;
b = _b;
c = _c;
};
関数の引数でデストラクチャを行うことができる。
// タプル構造体のパターン
struct IpV4Address(u8, u8, u8, u8);
// タプル構造体を引数でデストラクチャ
fn print_ipv4addr(IpV4Address(o1, o2, o3, o4): &IpV4Address) {
println!("{}.{}.{}.{}", o1, o2, o3, o4);
}
// これと同じ
fn print_ipv4addr(address: &IpV4Address) {
let o1 = address.0;
let o2 = address.1;
let o3 = address.2;
let o4 = address.3;
println!("{}.{}.{}.{}", o1, o2, o3, o4);
}
fn main() {
let addr = IpV4Address(127, 0, 0, 1);
print_ipv4addr(&addr);
}
Serdeについて
SerdeはRustのデータ構造をSerialize/Deserializeする。
- Serialize : プログラムのデータ構造 → 文字列・バイト列
- Deserialize : 文字列・バイト列 → プログラムのデータ構造
Serdeを使うことによってStruct, HashMap, etc⇔JSON, YML, etcのデータ変換を行うことができる。
httpリクエストでJSONをreq/resしたりするときに便利。
use serde::{Serialize, Deserialize};
// deriveを使うことで簡単にSeriarive/Deserializeできる
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let point = Point { x: 1, y: 2 };
// Point構造体をJSONに変換する
let serialized = serde_json::to_string(&point).unwrap();
println!("serialized = {}", serialized);
// JSONをPoint構造体に変換する
let deserialized: Point = serde_json::from_str(&serialized).unwrap();
println!("deserialized = {:?}", deserialized);
}
実行結果
serialized = {"x":1,"y":2}
deserialized = Point { x: 1, y: 2 }
serde_json::to_string()
はSeriarize
トレイトを実装した型を受け取り、String
のResult
を返す。
serde_json::from_str()
は文字列を受け取り、Seriarize
トレイトを実装した型のResult
を返す。
if let と while let
match
を使う際、すべてのパターンを列挙する必要がある。
if let
記法を使うと1つのパターンにマッチする場合の処理のみを記載できるので、冗長性を排除できる。
if let Some(3) = some_u8_value {
println!("three");
}
// これと同じ
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
同じようなことがwhile
式でもできる。
あるパターンにマッチするときループに入る、みたいなことを書ける。
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// stack.pop()がNoneじゃない限りループに入れる
while let Some(top) = stack.pop() {
// pop変数を使える。分割代入的なイメージ。
println!("{}", top);
}
axumに関して
axumはRustのweb applicationフレームワーク、tokioやhyperを裏で使っている
routingモジュール
ルーティングの設定を行う。
use axum::{Router, routing::get};
let app = Router::new()
.route("/", get(root))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));
handlerモジュール
ハンドラの設定を行う。
ハンドラはリクエストを処理するための非同期関数(async function)。
0個以上のextractorを引数に取り、レスポンス(に変換できるもの)を返り値にする。
extractorsモジュール
受け取ったリクエストを処理してハンドラに渡す役割を果たす。
FromRequest
もしくはFromRequestPart
を実装した型・トレイト。
例として、Json
はextractorである。
(リクエストボディを受け取ってJSONとしてdeserializeしハンドラに渡すので)
use axum::extract::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
email: String,
password: String,
}
async fn create_user(Json(payload): Json<CreateUser>) {
// ...
}
responsesモジュール
レスポンスを扱う。
axumにおいてレスポンスはIntoResponse
が実装されハンドラから返されるものと言える。
IntoResponse
は大体の型に対し実装されているのでほとんどレスポンスとして扱える。
use axum::{
body::Body,
routing::get,
response::Json,
Router,
};
use serde_json::{Value, json};
async fn json() -> Json<Value> {
Json(json!({ "data": 42 }))
}
let app = Router::new()
.route("/plain_text", get(plain_text))
.route("/json", get(json));