返回值有多种格式那要怎么声明 Rust 结构体


这算是 Rust 的第一篇内容,虽然有点水,但总算开动了。

最难跨越的就是真正开始去行动的那一刻。

Rust 的结构体和枚举非常厉害,它在形式上,也非常接近人的常规理解:层层分解。

写 Rust 其实有很大的一部分内容就是写结构体声明,一旦结构体声明出来了,基本上心理上就是落实了一大半了。

今天看下这种情况,比如你要请求一个接口,然后这个接口的返回值吧,并不总是保持一致的结构,而是根据不同情况有所不同的。

就比如拿 https://support.huaweicloud.com/api-ecs/ecs_02_0101.html#section7 这个页面的接口进行举例。

它的接口,除请求失败以外,请求成功的响应中,还分为正常响应和错误响应,而且错误响应可能也还有些字段内容是不一定返回的。

// 响应示例
{
    "job_id": "ff808082739334d80173943ec9b42130",
    "order_id": "CS2007281506xxxxx",
    "serverIds": [
        "fe0528f0-5b1c-4c8c-9adf-e5d5047b8c17",
        "679854ae-a50d-40c9-8132-b19bf3a306a1"
    ] 
}

// 
{
    "error": {
        "code": "Ecs.0005", 
        "message": "request body is illegal."
    }
}

// 
{
    "error": {
        "message": "privateIp [%s] is not in this subnet [%s]",
        "code": "Ecs.0005",
        "details": [
            {
                "code": "Ecs.0039"
            }
        ]
    }
}

如果是在 Python 里面,上来粗暴点对待可能就是直接解析为字典 dict 然后结合状态码与字典 key 值进行判断处理了,而如果用到了高版本的 Python 然后又想要认真点对待,可能会去声明两个 dataclass 然后也根据状态码来使用 if else 进行分别处理。

那么这种场景在 Rust 中怎么声明结构体呢?都说 Rust 是强类型,然后就一脸懵了,可能还有人跑去说泛型什么的,但是其实真没那么复杂。

看代码。

use serde::{Deserializer, Serializer};
use schemars::JsonSchema;


#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(untagged)]
pub enum CreateCloudServersRes {
    HwcApiError(HwcApiError),
    CreateCloudServers(CreateCloudServers),
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct HwcApiError {
    pub error: HwcApiErrorBody,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct HwcApiErrorBody {
    pub code: String,
    pub message: String,
    pub details: Option<Vec<HwcApiErrorBodyDetail>>,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct HwcApiErrorBodyDetail {
    pub code: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct CreateCloudServers {
    pub job_id: Uuid,
    pub order_id: String,
    #[serde(rename = "serverIds")]
    pub server_ids: Vec<Uuid>,
}

这样就实现了对这个接口响应的结构体声明,在使用的时候,就如下这样:

let client = reqwest::Client::new();
let resp = client.post(url).headers(headers).json(&req).send().await?;
let res = resp.json::<CreateCloudServersRes>().await?;
match res { 
    CreateCloudServersRes::CreateCloudServers(data) => {},
    CreateCloudServersRes::HwcApiError(err) => {},
}

然后就再对这个返回值枚举进行处理即可,当然同样也还可以再结合状态码进行一些处理逻辑,在业务逻辑处理上,语言上的差异就没那么大了,看你的具体需求。

如果在拿 res 的时候并不想让错误直接向上层抛出,那么可以去除 .await?; 这里的问号成为 .await; 这样就可以自己来针对性地做一些事情,而我们在 Python 里面经常会用 try: ... except Exception as e: ... 来处理一些特别任务,例如记录点特别的日志什么的:

let res = resp.json::<CreateCloudServersRes>().await;
match res {
    Ok(res) => {
        info!("do something success: req: {:?} res: {:?}", &req, &res);
        match res { 
            CreateCloudServersRes::CreateCloudServers(data) => {},
            CreateCloudServersRes::HwcApiError(err) => {},
        }
    }
    Err(err) => {
        capture_error(&err);
        error!("do something failed: req: {:?} error: {:?}", &req, &err);
        return Err(err.into());
    }
}

当然这里这个 match 也可以用 let else 等写法进行替代,看你自己的需求。

整体上来说,面对接口在不同情况返回不同格式的结构的时候,在 Rust 中就可以以这样的形式去实现即可。

总结下知识点:

  • 对于完全格式不同的结构体,可以使用 enum 然后内嵌结构体进行声明
  • 使用 #[serde(untagged)] 直接穿透解析不同情况下为相对应的结构体
  • 使用 Option<T> 对可能不一定总是存在的字段声明为可选字段
  • 使用 #[serde(rename = "serverIds")] 对字段名称不怎么规范或者关键字冲突进行重命名而声明对应的解析字段
  • 如果不想向上层直接抛出错误,可以不加 ? 然后自己对 Result<T, E> 进行处理,这其实非常相当于 Python 里面的 try: ... except Exception as e: ... 但是却又比它感觉上更直观更可靠。