Rocket.rs 初探 - QueryGuard, ParamGuard 以及 RequestGuard 實際範例
因為工作的原因需要學一個以 Rust 為基礎的 web framework, 也因為自己對於編譯式語言其實情有獨衷,所以想要學 Rust 很久了, 趁著這次有時間跟機會可以接觸到 Rocket.rs 就順便把 Rocket.rs 跟 Rust 一併學了, 然後寫成筆記。
實際上,自訂 Query String,Parameter 以及 Request 的概念幾乎是一樣的,分別是透過實做 FromQuery、FromParam 以及 FromRequest 來達到這個目的。
Query String
在 Query String 這個段落中,將寫一個 FromQuery 的範例,這個範例是當 search 這個參數的值是一個必須大於兩個字元的字串,才會被該路由所接受:
use rocket::http::RawStr;
use rocket::request::{Query, FromQuery};
struct Search<'q>(&'q RawStr);
impl<'q> FromQuery<'q> for Search<'q> {
type Error = &'q str;
fn from_query(query: Query<'q>) -> Result<Search, Self::Error> {
// 尋找 key 為 search 且 value 的長度 > 2 的 query
let key_items = query.filter(|item| item.key == "search" && item.value.len() >= 2);
// 如果找到的數量為 0 代表沒有 search 或者有 search 但是值的長度不到 2
let count = key_items.clone().count();
if count == 0 {
return Err("no matching search");
}
// 將找到的資料包裝成 Search 並返回第 0 個
// The `ok_or` gets us a `Result`. We will never see `Err(0)`.
return key_items.map(|i| Search(i.value)).nth(0).ok_or("no matching search");
}
}
#[get("/products?<search..>")]
fn search_product(search: Search) -> String {
format!("Search: {}", search.0)
}
fn main() {
rocket::ignite().mount("/", routes![search_product]).launch();
}
Parameter
在 Parameter 這個段落中,將寫一個 FromParam 的範例,這個範例是當/users/<mobile>
路由中的<mobile>
參數必須大於10個字元,且必須全部皆是數字,且必須以 09 開頭才能被路由所接受:
use rocket::request::FromParam;
use rocket::http::RawStr;
struct Mobile<'r> {
value: &'r str
}
impl<'r> FromParam<'r> for Mobile<'r> {
type Error = &'r str;
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
// 如果 param 的長度小於 10 則返回錯誤
if param.len() < 10 {
return Err("length cannot shorter than 10.");
}
extern crate regex;
use regex::Regex;
// 必須全部為 digit
let re = Regex::new(r"^\d+$").unwrap();
// 不匹配也返回錯誤
if !re.is_match(param) {
return Err("param should contain digit.")
}
return Ok(Mobile{value: param.as_str()});
}
}
#[get("/users/<mobile>")]
fn get_user_with_mobile(mobile: Mobile) -> String
{
return format!("User with mobile: {}", mobile.value);
}
fn main() {
rocket::ignite().mount("/", routes![
get_user_with_mobile
]).launch();
}
Request
在 Request 這個段落中,將寫一個 FromRequest 的範例,這個範例是當/profile
請求中的 Header 必須包含Api-Key
的請求頭,才會被路由所接受。
use rocket::request::{self, Request, FromRequest};
use rocket::Outcome;
use rocket::http::Status;
struct ApiKey(String);
fn is_valid_key(key: &str) -> bool {
return key == "valid_key";
}
impl<'a, 'r> FromRequest<'a, 'r> for ApiKey {
type Error = &'r str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let keys: Vec<_> = request.headers().get("Api-Key").collect();
match keys.len() {
// 沒有提供 Api-Key
0 => Outcome::Failure((Status::BadRequest, "Api-Key no provided")),
// 提供且 Api-Key 正確
1 if is_valid_key(keys[0]) => Outcome::Success(ApiKey(keys[0].to_string())),
// Api-Key 不正確
1 => Outcome::Failure((Status::BadRequest, "Api-Key invalid")),
// 其他狀態,代表過多個 Api-Key
_ => Outcome::Failure((Status::BadRequest, "Too many Api-Key"))
}
}
}
#[get("/profile")]
fn profile(api_key: ApiKey) -> String {
return format!("Valid User, key: {}", api_key.0);
}
fn main() {
rocket::ignite().mount("/", routes![
profile
]).launch();
}
這樣,只有當請求的 Header 中帶上valid_key
時,請求才有辦法進入profile()
路由中:
GET /profile HTTP/1.1
Host: localhost:8000
API-Key: valid_key