Rust 在面对返回数据的字段格式不固定时该怎么办(续)


说了要续的,等不及了,先续个小点的,不然对不住这标题。

拿个例子,对接了一个接口,你会收到这样的返回:

{
    "allow_type": {
        "id_list": [],
        "type": 3
    }
}

你以为这结构应该没啥特别的吧?看起来语义和字段设计都没啥毛病,大概意思你也能猜个 789 对不对。

一旦当你以为程序跑起来顺顺利利的时候,一联调,总会偶尔出现导致程序 panic 出现的情况,看错误提示又无法直接定位到具体是什么问题,一开始莫名其妙很多次,等到狠下心来将收到的原始文本打印出来真正认真比对数据,才能发现问题所在。

因为你不知道的是,偶尔,你也会收到这样的返回:

{
    "allow_type": {
        "id_list": "all",
        "type": 1
    }
}

现在你发现问题了,再搜集多几个场景,你发现这样的数据:

{
    "allow_type": {
        "id_list": [],
        "type": 2
    }
}

最后去真正分析了业务逻辑,type=1 和 2 的时候,根本不用管这个 id_list,只有 type=3 的时候才会关心 id_list 具体有什么内容。

所以 type 本身即表示了语义分类,而在 type=1 的时候填一个 “all” 根本就是多次一举,或许可能开发这个接口的同学心里可能还在默念:你看我这写的多么清晰。

看过了前面企业微信、钉钉、飞书的消息结构体的声明的文章,你可能觉得这个好像也没啥难度,或许可能用枚举就能把它的这个多种情况能完美声明出来,但你也知道这个场景下,数据结构语义与业务语义有偏差才是重点,而很明显这里是有可能统一标准的,而不需要使用枚举进行多个类型结合,也更不需要在后续的业务逻辑处理中,进行 match 的分支处理。

基于统一标准的思路,这里的 id_list 那就应该是 [] 的格式,当 type=1 时,应该是 id_list=[] 这样的。

而问题就来到了如何在收到 “all” 的时候把它在结构体上解析为 [] 的事情。

第一个思路,我是这样想的,用一个结构体应该能搞定这个事情:

  • 先用 serde_json::Value 把数据装到一个私有字段,延迟进行具体的解析
  • 声明一个真正的对外的公开标准字段,但是不参与数据解析,它将由我们自己写的函数去处理得到
  • 然后再写一个具体的处理逻辑把这个私有字段的数据处理到这个公开的标准字段上
  • 这样对外就是公开的标准字段,接收数据就是结构内部私有字段,把问题在结构内部进行消化
  • 但是需要使用方主动调用一个处理此事的函数,实现数据的处理,比直接解析多一步

serde_json::Value 首先要理解,它是一个可以描述 json 中所有类型的一个数据类型,其实呢,它也就是一个 Rust 里面的枚举类型。

第二个要理解的是 Rust 的结构体里面的字段是支持 私有 和 公开 这两个情况的,而且默认是私有,声明了 pub 才是公开。

第三个是 serde_json 针对字段数据的解析,根据场景的不同需求,有非常多的属性可以配置,我们所遇见的大多数情况都能直接使用它的这些属性进行配置化处理。

下面具体看代码,首先是这个结构体局部:


#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct EnableConf {
    #[serde(rename = "type")]
    pub type_: CompanyEnableType,
    pub id_list: Vec<Uuid>,
}

impl Default for EnableConf {
    fn default() -> Self {
        Self {
            type_: CompanyEnableType::All,
            id_list: vec![],
        }
    }
}

#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq, Eq)]
pub enum CompanyEnableType {
    All = 1,
    Enterprise = 2,
    Specific = 3,
}

然后看它被使用到的地方,声明私有字段和公开字段,以及它们的 serde 属性:

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct MainMenu {
    pub menu_id: MenuID,
    #[serde(rename(deserialize = "name"))]
    pub menu_name: String,
    #[serde(rename(deserialize = "main_menu_code"))]
    pub menu_code: String,
    pub icon_url: String,
    pub router_url: String,
    #[serde(rename(deserialize = "sort_value"))]
    pub sort: u32,
    pub product_status: u32,
    #[serde(skip_serializing)]
    pub permission: PermCode,
    #[serde(skip_serializing)]
    pub product_system_id: SubsystemID,
    #[serde(skip_serializing, rename(deserialize = "allow_type"))]
    allow_type_value: Value,
    #[serde(skip_deserializing, skip_serializing)]
    pub allow_type: EnableConf,
    #[serde(skip_serializing, rename(deserialize = "visible_type"))]
    visible_type_value: Value,
    #[serde(skip_deserializing, skip_serializing)]
    pub visible_type: VisibleConf,
}

然后写一个处理函数,这个函数独立在外或者直接在 impl 里面写都是可以的,由于好几个结构体类型都得用它,所以就独立函数出来:

pub fn parse_allow_type(allow_type_value: &serde_json::Value) -> Result<EnableConf, Error> {
    let mut type_default = CompanyEnableType::All;
    let mut id_list_default: Vec<Uuid> = vec![];
    let allow_type_value = allow_type_value.as_object();
    if let Some(allow_type_value) = allow_type_value {
        let type_ = allow_type_value.get("type");
        if let Some(v) = type_ {
            match v.as_u64() {
                Some(1) => {
                    type_default = CompanyEnableType::All;
                }
                Some(2) => {
                    type_default = CompanyEnableType::Enterprise;
                }
                Some(3) => {
                    type_default = CompanyEnableType::Specific;
                }
                _ => {}
            }
        }
        let id_list = allow_type_value.get("id_list");
        if let Some(v) = id_list {
            if let Some(arr) = v.as_array() {
                for item in arr.iter() {
                    if let Some(s) = item.as_str() {
                        let id = Uuid::from_str(s);
                        match id {
                            Ok(id) => {
                                id_list_default.push(id);
                            }
                            Err(e) => {}
                        }
                    }
                }
            }
        }
    }
    let conf = EnableConf {
        type_: type_default,
        id_list: id_list_default,
    };
    Ok(conf)
}


impl MainMenu {
    /// todo 可能有更好的方法就是写 Deserialize 来处理,但是目前没搞定全局之前先这样过
    // "allow_type": {
    //     "id_list": "all",
    //     "type": 1
    // }
    // "allow_type": {
    //     "id_list": [],
    //     "type": 3
    // }
    pub fn parse_allow_type(&mut self) -> Result<(), Error> {
        self.allow_type = parse_allow_type(&self.allow_type_value)?;
        Ok(())
    }
    pub fn parse_visible_type(&mut self) -> Result<(), Error> {
        self.visible_type = parse_visible_type(&self.visible_type_value)?;
        Ok(())
    }
}

impl MainMenuDetail {
    pub fn parse_allow_type(&mut self) -> Result<(), Error> {
        self.allow_type = parse_allow_type(&self.allow_type_value)?;
        Ok(())
    }
    pub fn parse_visible_type(&mut self) -> Result<(), Error> {
        self.visible_type = parse_visible_type(&self.visible_type_value)?;
        Ok(())
    }
}

impl SubMenu {
    pub fn parse_allow_type(&mut self) -> Result<(), Error> {
        self.allow_type = parse_allow_type(&self.allow_type_value)?;
        Ok(())
    }
    pub fn parse_visible_type(&mut self) -> Result<(), Error> {
        self.visible_type = parse_visible_type(&self.visible_type_value)?;
        Ok(())
    }
}

然后再在业务代码的函数里面去主动调一下这个函数,就能解析出我们想要的标准数据:

menu.parse_allow_type()?;

这样后续再做业务逻辑,你能绝对保证你拿到的是标准的数据结构,而不需要再临时做些 if else 判断啥的。

你看,其实是不是还挺容易的?

而且还保持了几个原则:

  • 把问题在内部解决,对外都是标准数据,要烂,也是烂局部,不影响大环境
  • 即便没有用到复杂的高级特性,也是完美地实现了尽量简单易懂的处理方案

当然,始终还是要业务代码多一次调用,对数据多做一次 mut 操作,但也一样保持了只在局部处理掉这个细节问题,不把问题继续往外抛出影响外部使用。

当然你也看到了,这是方案一,方案二可能就是如同备注里面说的:去写 Deserialize 方法

但是由于写 Deserialize 方法还是需要对 serde 和 serde_json 有一定的了解和对写解析器有一定的功底经验,才能合理地写出正确的解析逻辑来,在没有这个经验之前,当前这种也是一个比较合理的方案。

其实结构体这部分还有方案三:与声明私有字段不同,可能考虑多个结构体进行转换,这样结构更清晰,不像现在需要利用到 serde_json 的一些字段属性来帮忙处理,导致结构体内部字段比较杂乱,不太看的清楚。但这两方案思路都是类似,没有绝对好坏,看情况选择即可。

那,等再续?