Rocket的rocket_contrib
套件雖然有提供Json
結構體可以作為路由處理程序的函式的回傳值型別,來回傳JSON格式的資料,但實際用起來卻不是很方便。
在實作JSON格式的HTTP API時,為了使客戶端知道API的調用是否成功,且若不成功的話也要能知道是什麼原因,所以我們可能會回應非標準的HTTP狀態碼來表示錯誤碼。不過其實比較好的實作方式是將錯誤碼放在HTTP主體的JSON資料中,HTTP狀態碼則永遠按照標準來回應(甚至也可以只回應200
)。也就是說,回應給客戶端的JSON格式資料,可能會像是以下這樣:
{
"code": 狀態碼/錯誤碼,
"data": JSON資料
}
但是如果用Rocket的json
特色提供的Json
結構體來做這件事的話,原先用來序列化成JSON資料的型別,就要再使用額外的結構體包裝起來。例如:
#[derive(Serialize)]
struct User {
id: i32,
name: String,
}
#[derive(Serialize)]
struct UserWithCode {
code: i32,
user: User,
}
有了UserWithCode
結構體後才可以再使用Json
結構體來包裹成Json<UserWithCode>
。
當然,如果要回應的資料型別有很多種的話,可以設計一個有泛型的結構體。例如:
#[derive(Serialize)]
struct User {
id: i32,
name: String,
}
#[derive(Serialize)]
struct DataWithCode<T: Serialize> {
code: i32,
user: T,
}
不過這下子,要作為路由處理程序的函式的回傳值型別時就要用Json
結構體來包裹成Json<DataWithCode<User>>
。這樣就變得有太多層泛型了,不是很好用。
JSON Response for Rocket Framework
「JSON Response for Rocket Framework」是筆者開發的套件,用來包裹任意可轉為JSON格式的資料,並加上狀態碼機制。
Crates.io
Cargo.toml
使用方法
rocket_json_response
這個crate提供的JSONResponse
結構體,可以用來包裹任意可轉為JSON格式的資料,使JSON資料變成:
{
"code": 狀態碼,
"data": JSON資料
}
狀態碼可以是任意有實作同樣來自於rocket_json_response
這個crate的JSONResponseCode
特性的型別,而Rust內建的小於或等於32位元的整數型別(i8
、u32
等)已經有實作JSONResponseCode
特性了。不過建議還是參考這篇文章來定義列舉,並替它實作JSONResponseCode
特性。
JSON資料則可以是任意有實作同樣來自於rocket_json_response
這個crate的ToJSON
特性的型別,Rust內建的基本資料型別和字串以及字串切片都已實作ToJSON
特性。另外,serde_json
這個crate的Value
結構體也有實作ToJSON
特性。而json_gettext
這個crate的JSONGetTextValue
列舉也有實作ToJSON
特性,有關於json_gettext
的介紹可以參考這篇文章。
如果要替有實作Serade框架的Serialize
特性的型別實作ToJSON
特性的話,可以使用rocket_json_response
提供的serialize_to_json
巨集來快速達成。
如果不想要在回應的JSON資料中加上data
欄位,而只有code
欄位的話,這樣的JSON資料如下所示:
{
"code": 狀態碼
}
我們可以使用rocket_json_response
這個crate提供的JSONResponseWithoutData
結構體,來回應以上的JSON格式的資料。
典型的0
狀態碼表示這個HTTP API調用成功了,而非0
的狀態碼表示調用失敗。因此,不論是JSONResponse
和JSONResponseWithoutData
結構體,都提供了ok
和err
關聯函數來產生實體。前者不需要指定狀態碼(預設為0
),後者需要指定狀態碼。
舉個例子:
#[macro_use]
extern crate rocket;
use enum_ordinalize::Ordinalize;
use rocket::{
outcome::Outcome,
request::{FromRequest, Outcome as RequestOutcome, Request},
serde::Serialize,
};
use rocket_json_response::{
serialize_to_json, JSONResponse, JSONResponseCode, JSONResponseWithoutData,
};
#[derive(Serialize)]
struct User {
id: i32,
name: String,
}
serialize_to_json!(User);
#[derive(Ordinalize)]
#[repr(i32)]
pub enum ErrorCode {
IncorrectIDFormat = 100,
}
impl JSONResponseCode for ErrorCode {
fn get_code(&self) -> i32 {
self.ordinal()
}
}
struct UserAgent<'a> {
user_agent: &'a str,
}
#[async_trait]
impl<'r> FromRequest<'r> for UserAgent<'r> {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> RequestOutcome<Self, Self::Error> {
let user_agent: Option<&str> = request.headers().get("user-agent").next();
match user_agent {
Some(user_agent) => Outcome::Success(UserAgent {
user_agent,
}),
None => Outcome::Forward(()),
}
}
}
#[get("/")]
fn alive() -> JSONResponseWithoutData {
JSONResponseWithoutData::ok()
}
#[get("/<id>")]
fn id(id: u32) -> JSONResponse<'static, u32> {
JSONResponse::ok(id)
}
#[get("/<id>", rank = 1)]
fn id_str(id: String) -> JSONResponse<'static, String> {
JSONResponse::err(ErrorCode::IncorrectIDFormat, id)
}
#[get("/user/magiclen")]
fn user() -> JSONResponse<'static, User> {
JSONResponse::ok(User {
id: 0, name: "Magic Len".to_string()
})
}
#[get("/client/user-agent")]
fn user_agent(user_agent: UserAgent) -> JSONResponse<&str> {
JSONResponse::ok(user_agent.user_agent)
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![alive])
.mount("/", routes![id, id_str])
.mount("/", routes![user])
.mount("/", routes![user_agent])
}
比較需要注意的是,在這個例子中,UserAgent
這個請求守衛會去取得HTTP請求中的User-Agent
標頭欄位,但它不會將欄位值複製成新的字串,所以這時候JSONResponse
的泛型生命周期參數就派上用場啦!在user_agent
路由處理程序裡,沿用了User-Agent
欄位值的生命周期,使它從接收到回應都不會被另外複製!
另外,雖然在例子中沒有見到,但JSONResponse
的泛型型別參數如果被省略掉的話,該泛型型別參數預設的型別會是JSONGetTextValue<'a>
。因為JSONGetTextValue
結構體可以用來表示所有類型的JSON資料(物件、陣列、字串、數值、布林值),所以不太需要被明寫出來。(不預設使用serde_json
這個crate的Value
結構體的原因是,Value
結構體並沒有辦法直接儲存字串切片,所以如果要用它表示JSON字串,一定要將字串切片轉成字串才行,然而這樣又需要進行複製的動作了。)