我們先把訂閱API的路由加上去,目前main.rs
的內容如下:
use axum::{http::StatusCode, routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/health_check", get(health_check))
//// 在路由中加上訂閱的API
.route("/subscriptions", post(subscribe));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
pub async fn health_check() -> StatusCode {
StatusCode::OK
}
上面的方法中還缺少subscribe
的處理函數,接下來就要實做它。
subscribe
和前面health_check
不一樣的地方在於,需要從HTTP Request中提取客戶端傳送的訊息,我們先實現一版最簡單的處理函數,接收請求、提取訊息,並簡單的回覆結果。
首先將serde
的參考加入專案中:
cargo add serde --features derive
serde
是rust中用於處裡資料序列化的crate,只要在結構前面加上巨集標注就可以簡單的讓結構支援序列化。安裝完後加上以下程式碼:
#[derive(Deserialize)]
pub struct NewSubscriber {
/// Subscriber Email
pub email: Option<String>,
/// Subscriber Name
pub name: Option<String>,
}
pub async fn subscribe(Form(data): Form<NewSubscriber>) -> StatusCode {
if data.email.is_some() && data.name.is_some() {
StatusCode::OK
} else {
StatusCode::BAD_REQUEST
}
}
簡單說明一下,這個hander會從request中題取出訂閱者的email與名稱,如果兩者都有成功獲取得到就會回覆200,否則就會回覆400。
我們現在把焦點放在subscribe
的參數宣告部份Form(data): Form<NewSubscriber>
,在Axum中,Form稱為提取器(Extractor),作用就是解析請求中的資訊,在這裡使用Form
就可以從application/x-www-form-urlencoded
中取得資料。
正如回傳值一樣,處理函數的參數要實做另一個traitFromRequest
或是FromRequestParts
,通常情況下我們也不用自己實做,Axum中已經準備好了幾個內建的提取器
Form
就不在其中。接下來再把目光轉移到NewSubscriber
這個結構體上。我在這邊使用了Option<String>
這個型別去解析email與名稱,讓我們討論一下如果使用單純String
的情況:
pub struct NewSubscriber {
pub email: String,
pub name: String,
}
如果不使用Option,這個兩個欄位就會是必填,在rust中沒有null
的概念,這意謂如果使用者在發出請求時沒有給其中的值,就會在反序列化資料時解析失敗,在這個情況下Axum會自動回覆使用者400的HTTP狀態,請求也不會傳遞到handler中。
如果要讓請求能夠進到handler內部讓程式可以做處理,就必須使用Option
這個Enum來讓欄位是可選的。Option
是FP中的重要概念,用來表達一個值是否存在,在rust中為了處理Option
提供了很多的方法,像是pattern match等技巧,在這邊只是半成品所以簡單的使用is_some()
來做判斷。
是否要使用Option
這個問題,就看開發上的需求而定,如果只需要簡單回覆客戶端錯誤,直接用String
就可以了,如果有更複雜的業務情境可能就要加上Option
了。在具有null
的語言中通常不太會考慮這麼多,而Option
則是把這個問題擺到檯面上,可能會比較繁瑣,但相應的好處是開發階段考慮過後就可以增進程式的穩定性。
最後稍微提一下這邊使用具有所有權的String
而不是不具備所有權的str
,因為在處理request的時候會需要對輸入的字串做各種加工。
最後我們來看一下FromRequest
trait
pub trait FromRequest<S, B, M = ViaRequest>: Sized {
type Rejection: IntoResponse;
// Required method
fn from_request<'life0, 'async_trait>(
req: Request<B>,
state: &'life0 S
) -> Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait, Global>>
where 'life0: 'async_trait,
Self: 'async_trait;
}
這真的是一個非常複雜的trait阿,我們把它拆成幾個部份
type Rejection: IntoResponse;
這是一個trait的關聯型別,它同時出現在第八行的Result<Self, Self::Rejection>>
中,用來指定from_request
在錯誤時的回傳類型,這是rust中一種對於抽象界面描述的方式,不會具體定義trait中所使用的型別,但又能對型別做一定的限制。Sized
trait:Sized
,這是一個標記,表示實做FromRequest
trait的型別需要在編譯時期就能知道大小,舉例來說u8
就是固定大小的,而String
、Vec<T>
則是會預先分配大小,在執行時期動態擴張,所以也是Sized
,像是切片型別這種在執行時期才能確定要擷取哪一段就不是Sized
,這個標記是為了讓編譯器能夠更好的在編譯時期處裡記憶體分配。Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait, Global>>
Future
traitTask
,當任務結束的時候會回傳一個Result
的結果。Box
structPin
structPin
表示這個結果被固定在特定位置,不管參數如何傳遞都可以在相同位置找到它,他是為了要在非同步任務中能夠確保資料可以被獲取,關於Pin要解決的問題請參考這篇。Send
traitFuture
可以安全的傳遞到不同的執行緒。Send
trait是用來解決多執行緒下資料爭用的問題,通常具有不變性的資料都具有send的特徵。今天完成了訂閱API的殼,覺得在解釋FromRequest
真的大大超出我現有的能力,更不用提實做它了,萬幸通常情況下我們不用自己去實做,只要使用現成的Extractor
就好了,明天就繼續推進進度吧!