Rustのaxumを使う
Rust熱が高まってきたのでaxumで遊んだ.やってることはactix-webの記事とほぼ同じ.
準備
ファイルの監視と自動コンパイル
必須ではないがコマンドwatchexec
(https://github.com/watchexec/watchexec) を導入しておくとプログラムを書き換えたときにサーバを自動で再起動してもらえるようになって便利である.
watchexec
はRust環境があればcargo
でインストールできる:
cargo install --locked watchexec-cli
以下のように使用する:
watchexec -r -- cargo run
APIアクセスのテスト
curlコマンドとか使うのも良いけど, Insomnia (https://insomnia.rest/download) というアプリケーションを使うとすごく楽になった.
使用するクレート
cargo new
でプロジェクトを作ったらCargo.toml
の[dependencies]
に以下のクレートを書く:
uuid = { version = "1", features = ["v4", "serde"] }
axum = { version = "0.8", features = ["macros"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1", features = ["derive"] }
image = "0.25"
Hello, axum
axumでサーバを立てる最小限のプログラム.cargo run
で実行し,ブラウザから http://localhost:3031 にアクセスすると「It works!」の文字列が表示される.
use axum::{
routing::get,
response::{Html, IntoResponse},
Router,
};
use tokio::net::TcpListener;
async fn index() -> impl IntoResponse {
Html("<h1>It works!</h1>")
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index));
let listener = TcpListener::bind("localhost:3031").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
JSONをPOSTできるAPI
URLにユーザ名がPOSTされたらユーザを作成し,作成したユーザのIDと名前を返すAPIの例.
use axum::{
routing::{get, post},
response::{Html, Json, IntoResponse},
Router,
};
use tokio::net::TcpListener;
use serde::{Serialize, Deserialize};
use uuid::Uuid;
#[derive(Deserialize)]
struct CreateUser {
username: String,
}
#[derive(Serialize, Clone)]
struct User {
id: Uuid,
username: String,
}
async fn create_user(
Json(payload): Json<CreateUser>
) -> impl IntoResponse {
let user = User { id: Uuid::new_v4(), username: payload.username };
Json(user)
}
async fn index() -> impl IntoResponse {
Html("<h1>It works!</h1>").into_response()
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index))
.route("/users", post(create_user));
let listener = TcpListener::bind("localhost:3031").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
例えばJSON:
{
"username": "example_user"
}
を http://localhost:3031/users にPOSTすると以下のJSONが返ってくる.
{
"id": "ae1e7bae-41dd-4f63-a32b-30df1eba9263",
"username": "example_user"
}
画像を返すAPI
URL /image/{red}
にアクセスが来たら画像を返すようなAPIを書く.画像を返すにはPngEncoder
によってPNG形式にエンコードした画像データをベクタVec<u8>
に入れて返す.
MIME Type image/png
も忘れずつける
use axum::{
routing::get,
extract::Path,
http::{StatusCode, header},
response::{Html, IntoResponse},
Router,
};
use tokio::net::TcpListener;
use image::{
codecs::png::PngEncoder,
ImageEncoder, ColorType, RgbImage
};
async fn image(Path(r): Path<u8>) -> impl IntoResponse {
let mut img = RgbImage::new(512, 512);
for x in 0..img.width() {
for y in 0..img.height() {
let g = 255 - (x as f32 / img.width() as f32 * 255.0) as u8;
let b = (y as f32 / img.width() as f32 * 255.0) as u8;
img.put_pixel(x, y, image::Rgb([r, g, b]));
}
}
let mut buf = std::io::Cursor::new(Vec::<u8>::new());
let encoder = PngEncoder::new(&mut buf);
encoder.write_image(&img, img.width(), img.height(), ColorType::Rgb8.into()).unwrap();
let image_content = buf.into_inner();
(
StatusCode::OK,
[(header::CONTENT_TYPE, "image/png")],
image_content
)
}
async fn index() -> impl IntoResponse {
Html("<h1>It works!</h1>")
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index))
.route("/image/{red}", get(image));
let listener = TcpListener::bind("localhost:3031").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
URL http://localhost:3031/image/128 にアクセスすると以下の画像が返ってくる.
おわり
axumは全体的に記述がすっきりしてて良いですね