TL;DR
- ハンドラの引数の順番が大事(
Path
,State1
,State2
, ... ,StateN
,Body
) - デバッグするときは
#[debug_handler]
※ただし型引数ありだと正常に動作しないので適当ななしバージョンで試す - ドキュメント、ちゃんと読もうな!
コード
この記事でつかうコード
この記事を書いた動機
- データ操作を扱うAPIをAxumで扱うコードの書き方が公式であまり見当たらない&自分の知ってる書き方がAxumのバージョンが低い
- 適当にコード書いたら原因がわかりづいらエラーに遭遇した→備忘録を残す
事前知識
API
Application Programming Interfaceの略、ユーザーとアプリ間のやり取りを助けたりしているのはUI:User Interfaceで、APIはソフトウェアとプログラムのやり取りのためのツールだったりを指す(厳密な定義とかは各自調べてください)今回はWeb APIをAPIと呼称する。APIにはリクエストとレスポンスがあり、例えば/ping
にリクエストを送ったときにpong!
がレスポンスとして帰ってきたりする手続き。バックエンドとかいう領域はAPIを作ることが多い。多分。
ジェネリクス
任意の型に対して振る舞いを決めるときに便利。
fn get_length_i32(vec: Vec<i32>) -> usize {
vec.len()
}
fn get_length_f64(vec: Vec<f64>) -> usize {
vec.len()
}
このように「やることは同じだけど型が違う」ような手続きやデータ構造に対して、
fn get_length<T>(vec: Vec<T>) -> usize {
vec.len()
}
let vec_i32 = vec![1, 2, 3];
let vec_f64 = vec![1.0, 2.0, 3.0];
println!("{}", get_length(vec_i32)); // 3
println!("{}", get_length(vec_f64)); // 3
の様に書くことができ、.len()
を実装している型であれば(この場合はベクターなど)実行ができる。
トレイト
他の言語だとinterface
が近い概念。 Rustにおけるジェネリクスの1種。共通の振る舞いを特定の型に持たせることができる。
pub trait Animal {
fn sound(&self) -> &str;
}
pub struct Dog;
pub struct Cat;
impl Animal for Dog {
fn sound(&self) -> &str {
"woof"
}
}
impl Animal for Cat {
fn sound(&self) -> &str {
"meow"
}
}
fn make_sound<T: Animal>(animal: T) {
println!("{}", animal.sound());
}
fn main() {
make_sound(Dog);// "woof"
make_sound(Cat);// "meow"
}
Dog
、Cat
はそれぞれAnimal
トレイトで定義されたsound()
を実装する。
make_sound
の型注釈にAnimal
トレイトが用いられているが、これはT
という型は必ずAnimal
トレイトを満たす実装がなくてはならないという制約になる。この書き方であれば、全くデータ構造が異なるようなものに対して同じトレイトが実装されていれば、同じ関数を利用できる。
型引数
ジェネリクスの構文の一つ。関数に対してhoge::<T>
のように書く。
trait Printable {
fn print(&self);
}
impl Printable for i32 {
fn print(&self) {
println!("Integer: {}", self);
}
}
impl Printable for String {
fn print(&self) {
println!("String: {}", self);
}
}
fn print_value<T: Printable>(value: T) {
value.print();
}
fn main() {
let my_int: i32 = 10;
let my_string: String = String::from("Hello, world!");
print_value::<i32>(my_int);
print_value::<String>(my_string);
}
関数が特定のトレイトを実装した型を受け取る場合、関数を使用する際に何もないと具体的な型がわからない。
関数を扱う側で、明示的に型注釈を加える。
本題: サーバーを立てる
今記事では、特定のデータセットの操作、読み込みを行うAPIを定義し、それをHTTP通信で実行できるサーバーを作るのが目的。DB操作でもメモリ上での操作でもこの入り口の書き方はあまり変わらない (はず)
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::{
extract::State,
routing::get,
Router,
response::IntoResponse,
};
use std::net::SocketAddr;
pub trait OperationData: Clone + Send + Sync + 'static {
fn get(&self) -> usize;
fn increment(&self);
fn decrement(&self);
}
struct SharedData {
counter: AtomicUsize
}
impl SharedData {
fn new() -> Self {
Self {
counter: AtomicUsize::new(0),
}
}
}
impl Clone for SharedData {
fn clone(&self) -> Self {
Self {
counter: AtomicUsize::new(self.counter.load(Ordering::Relaxed)),
}
}
}
impl OperationData for SharedData {
fn get(&self) -> usize {
self.counter.load(Ordering::Relaxed)
}
fn increment(&self) {
self.counter.fetch_add(1, Ordering::Relaxed);
}
fn decrement(&self) {
self.counter.fetch_sub(1, Ordering::Relaxed);
}
}
#[tokio::main]
async fn main() {
let data = SharedData::new();
let app = create_app(data);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("Listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// *** point ***
fn create_app<D: OperationData>(data: D) -> Router {
Router::new()
.route("/data", get(data_handler::<D>))
.with_state(data)
}
// *** point ***
async fn data_handler<D: OperationData>(State(data): State<D>) -> impl IntoResponse {
let current = data.get();
format!("Current: {}", current)
}
小細工がたくさんあるけどこの記事の大事なポイントは***point***
とコメントアウトしているところ。
どんなロケーター(窓に入れるURLとかパスみたいなもん)に対してどんなハンドラ(なにかしら行うもの)をあてるかの部分で型引数を扱ってる(公式のexamplesにはここまではしてない)
// *** point ***
fn create_app<D: OperationData>(data: D) -> Router {
Router::new()
.route("/data", get(data_handler::<D>))
.with_state(data)
}
このコードは、GET: /data
というリクエストを受け取った場合、data_handler::<D>
を実行するということを示している。
・・・・・・あれ?data
引数に取らないの?という話が、route()
の次のメソッドチェーンのwith_state()
。状態を持つデータに対して、このルーターがアクセス可能になる宣言。つまりリクエストを行った場合、このAPIはdata
にアクセスでき、get_handler
はdata
を引数にとり実行される。
// *** point ***
async fn data_handler<D: OperationData>(State(data): State<D>) -> impl IntoResponse {
let current = data.get();
format!("Current: {}", current)
}
注意点: ハンドラに対して複数引数を取る場合
例えば、ブログだったり掲示板のAPIは、任意のidをURLに入れてGETできるような仕組みになっている。あれはURLを解析して、その情報を正しくAPIのハンドラに渡していたり、また上のコードはリクエストボディは不必要だが、同じAPIでの例では投稿時のテキストなどはボディになんらかの形(JSONなり生テキストなり)で渡される。
なので特定のidにJSONボディ付きのリクエストを受け取りデータを操作する場合、下記のような関数になる
Router::new()
.route("/post/:id", get(get_handler).post(post_handler::<T>))
.with_state(Arc::new(T))
// ...
async fn post_handler<T: PostTrait>(
Path(id: i32): Path<(i32)>,
State(repository): State<Arc<T>>,
Json(payload): Json<CreatePost>,
) -> impl IntoResponse {
// ...
}
(細かい記述は面倒なので省略 各自興味があればどうぞ)
ここで注意点は「引数の順番を必ず守ること」。
実際のエラーは諸事情で載せれないが、似たような症状についてのTipsが公式ドキュメントにあるので引用
ハンドラに関する制約も書かれているので、きちんと読んで噛み砕いてから実装するとよい。
ハンドラの制約(訳)
ちょっと不正確かもしれないので、各自英語で再確認はしてください。
- 非同期関数であること(
async fn
) - 16個以下の引数をとり、すべて
FromRequest
を実装していること - 返り値は
IntoResponse
を実装したものであること - クロージャが使用される場合それは必ず
Clone + Send + 'static
であること -
Send
である未来を返すこと。awaitで挟むと!Send
になってしまい、これは最も起こりやすいこと方法である。
ハンドラ関連のデバッグについて
自分がこの記事を書くに当たってぶつかったコードとの格闘で役に立ったセクション
Unfortunately Rust gives poor error messages if you try to use a function that doesn’t quite match what’s required by Handler.
ハンドラによって引き起こされたコンパイルエラーの場合、Rustコンパイラはメッセージの情報が不足することがある。
error[E0277]: the trait bound `fn(bool) -> impl Future {handler}: Handler<_, _>` is not satisfied
--> src/main.rs:13:44
|
13 | let app = Router::new().route("/", get(handler));
| ^^^^^^^ the trait `Handler<_, _>` is not implemented for `fn(bool) -> impl Future {handler}`
|
::: axum/src/handler/mod.rs:116:8
|
116 | H: Handler<T, B>,
| ------------- required by this bound in `axum::routing::get`
This error doesn’t tell you why your function doesn’t implement Handler. It’s possible to improve the error with the debug_handler proc-macro from the axum-macros crate.
「もしかしたらdebug_handler
マクロを使えばエラーが改善できるかもしれないよ」
ただこのマクロは型引数がある場合使えないため、一旦型引数なしの実装で書いてみて、原因を探るといいと思われる。
このマクロを有効にした時、例えば順番を間違える(ボディである引数を最後以外に配置する)と、エラーを出す。
error: `Json<_>` consumes the request body and thus must be the last argument to the handler function
--> src\handlers\section.rs:37:20
|
37 | Json(payload): Json<CreatePost>,
|
「リクエストボディは最後の引数にすべきだよ!」みたいなことを言ってくれる。Rustのコンパイラは神。