Rocket的rocket_contrib套件雖然有提供Json結構體可以作為路由處理程序的函式的回傳值型別,來回傳JSON格式的資料,但實際用起來卻不是很方便。



在實作JSON格式的HTTP API時,為了使客戶端知道API的調用是否成功,且若不成功的話也要能知道是什麼原因,所以我們可能會回應非標準的HTTP狀態碼來表示錯誤碼。不過其實比較好的實作方式是將錯誤碼放在HTTP主體的JSON資料中,HTTP狀態碼則永遠按照標準來回應(甚至也可以只回應200)。也就是說,回應給客戶端的JSON格式資料,可能會像是以下這樣:

{
    "code": 狀態碼/錯誤碼,
    "data": JSON資料
}

但是如果用Rocket的rocket_contrib套件提供的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

https://crates.io/crates/rocket-json-response

Cargo.toml

rocket-json-response = "*"

使用方法

rocket_json_response這個crate提供的JSONResponse結構體,可以用來包裹任意可轉為JSON格式的資料,使JSON資料變成:

{
    "code": 狀態碼,
    "data": JSON資料
}

狀態碼可以是任意有實作同樣來自於rocket_json_response這個crate的JSONResponseCode特性的型別,而Rust內建的小於或等於32位元的整數型別(i8u32等)已經有實作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的狀態碼表示調用失敗。因此,不論是JSONResponseJSONResponseWithoutData結構體,都提供了okerr關聯函數來產生實體。前者不需要指定狀態碼(預設為0),後者需要指定狀態碼。

舉個例子:

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;

#[macro_use]
extern crate rocket_json_response;

extern crate serde;

#[macro_use]
extern crate serde_derive;

#[macro_use]
extern crate enum_ordinalize;

use rocket_json_response::{JSONResponse, JSONResponseWithoutData, JSONResponseCode};

#[derive(Serialize)]
struct User {
    id: i32,
    name: String,
}

serialize_to_json!(User);

create_ordinalized_enum!(ErrorCode: i32,
    IncorrectIDFormat = 100,
);

impl JSONResponseCode for ErrorCode {
    fn get_code(&self) -> i32 {
        self.ordinal()
    }
}

#[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(),
    })
}

use rocket::request::{Request, FromRequest, Outcome as RequestOutcome};
use rocket::Outcome;

struct UserAgent<'a> {
    user_agent: &'a str
}

impl<'a, 'r> FromRequest<'a, 'r> for UserAgent<'a> {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> 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("/client/user-agent")]
fn user_agent(user_agent: UserAgent) -> JSONResponse<&str> {
    JSONResponse::ok(user_agent.user_agent)
}

fn main() {
    rocket::ignite()
        .mount("/", routes![alive])
        .mount("/", routes![id, id_str])
        .mount("/", routes![user])
        .mount("/", routes![user_agent])
        .launch();
}

比較需要注意的是,在這個例子中,UserAgent這個請求守衛會去取得HTTP請求中的User-Agent標頭欄位,但它不會將欄位值複製成新的字串,所以這時候JSONResponse的泛型生命周期參數就派上用場啦!在user_agent路由處理程序裡,沿用了User-Agent欄位值的生命周期,使它從接收到回應都不會被另外複製!

另外,雖然在例子中沒有見到,但JSONResponse的泛型型別參數如果被省略掉的話,該泛型型別參數預設的型別會是JSONGetTextValue<'a>。因為JSONGetTextValue結構體可以用來表示所有類型的JSON資料(物件、陣列、字串、數值、布林值),所以不太需要被明寫出來。(不預設使用serde_json這個crate的Value結構體的原因是,Value結構體並沒有辦法直接儲存字串切片,所以如果要用它表示JSON字串,一定要將字串切片轉成字串才行,然而這樣又需要進行複製的動作了。)