diesel自定义类型自动序列化

  • 5 分钟阅读
  • 标签: 
  • diesel

前言

  • 在https://github.com/emo-crab/nvd-rs项目中存在前后端模型用着两种重复的数据结构,在之前写的后端mysql表的json数据保存的serde_jsonValue,并不是对应的struct,前端使用的是正常struct,导致重复写了两份代码,今天将这两部分共用的数据结构独立出来作为一个模块。

现状和思路

  • 例如现在的CVE结构为
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Properties)]
pub struct Cve {
  pub id: String,
  pub year: i32,
  pub assigner: String,
  pub description: Vec<nvd_cves::v4::Description>,
  pub severity: String,
  pub metrics: nvd_cves::impact::ImpactMetrics,
  pub weaknesses: Vec<nvd_cves::v4::Weaknesses>,
  pub configurations: Vec<nvd_cves::v4::configurations::Node>,
  pub references: Vec<nvd_cves::v4::Reference>,
  pub created_at: NaiveDateTime,
  pub updated_at: NaiveDateTime,
}
  • 可以看出前端的是正常的,后端因为mysql的json只能使用Value类型。
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[derive(Queryable, Serialize, Deserialize, Identifiable, Debug, PartialEq)]
#[diesel(table_name = cves)]
pub struct Cve {
  pub id: String,
  pub year: i32,
  pub assigner: String,
  pub description: Value,
  pub severity: String,
  pub metrics: Value,
  pub weaknesses: Value,
  pub configurations: Value,
  pub references: Value,
  pub created_at: NaiveDateTime,
  pub updated_at: NaiveDateTime,
}
  • 要想把两种数据结构合并需要解决mysql的数据类型问题,很明显mysql里面没有前端这边的nvd库里的数据类型,按照官方的例子custom_types和之前的https://github.com/diesel-rs/diesel/issues/1783可以知道,要想自定义类型只需要给上面的Cve数据结构实现ToSqlFromSql这两个trait,详细实现可以看https://github.com/diesel-rs/diesel项目代码中的src/serialize.rssrc/deserialize.rs这两个文件。

解决方案

  • 得到实现ToSqlFromSql这两个trait,就可以按照mysql的json类型录入数据库,但是问题要去每个nvd库中将这些数据类型全部实现这些trait需要引入https://github.com/diesel-rs/diesel这个依赖,自己写的还好,要是这个库不是自己写的就不能在外部再重新实现,所以需要写一个统一通用的数据类型去兼容全部的json。
  • 创建一个AnyValue在后端使用的时候开启db特性,也就是添加AsExpression和FromSqlRow,数据库类型为Json,将上面nvd库中使用到的数据类型都用泛型T表示。这里需要添加transparent属性,不然在序列化的时候会出现找不到inner字段,
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "db", derive(AsExpression, FromSqlRow), diesel(sql_type = Json))]
#[serde(transparent)]
pub struct AnyValue<T: Clone>
where
  T: Clone,
{
  inner: T,
}
  • 添加创建新实例和解引用访问实例的方法,在使用这个数据结构里面的类型的时候可以使用deref方法获取。
impl<T: Default + for<'de> serde::Deserialize<'de> + Clone> AnyValue<T> {
  pub fn new(t: T) -> Self {
    Self { inner: t }
  }
}

impl<T: Default + Clone> Deref for AnyValue<T> {
  type Target = T;

  fn deref(&self) -> &Self::Target {
    &self.inner
  }
}

impl<T: Default + Clone> DerefMut for AnyValue<T> {
  fn deref_mut(&mut self) -> &mut Self::Target {
    &mut self.inner
  }
}
#[cfg(feature = "db")]
impl<T: Debug + Clone, DB: Backend> FromSql<Json, DB> for AnyValue<T>
where
  serde_json::Value: FromSql<Json, DB>,
  T: DeserializeOwned,
{
  fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
    let value = <serde_json::Value as FromSql<Json, DB>>::from_sql(bytes)?;
    Ok(serde_json::from_value(value)?)
  }
}

#[cfg(feature = "db")]
impl<T: Debug + Clone> ToSql<Json, DB> for AnyValue<T>
where
  serde_json::Value: ToSql<Json, DB>,
  T: Serialize,
{
  fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> serialize::Result {
    let value = serde_json::to_value(&self.inner)?;
    <serde_json::Value as ToSql<Json, DB>>::to_sql(&value, &mut out.reborrow())
  }
}

使用自定义类型

  • 在回过头来看Cve这个数据结构,将之前的两个合并为一个公共的数据结构,然后使用feature特性条件编译实现不同的属性,例如在前端我会开启yew特性,然后实现一个组件属性功能,在后端开启db特性,开启deisel的属性
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[cfg_attr(feature = "db", derive(Queryable, Identifiable), diesel(table_name = cves))]
#[cfg_attr(feature = "yew", derive(Properties))]
#[derive(Default, Serialize, Clone, Deserialize, Debug, PartialEq)]
pub struct Cve {
  pub id: String,
  pub year: i32,
  pub assigner: String,
  pub description: AnyValue<Vec<nvd_cves::v4::Description>>,
  pub severity: String,
  pub metrics: AnyValue<nvd_cves::impact::ImpactMetrics>,
  pub weaknesses: AnyValue<Vec<nvd_cves::v4::Weaknesses>>,
  pub configurations: AnyValue<Vec<nvd_cves::v4::configurations::Node>>,
  pub references: AnyValue<Vec<nvd_cves::v4::Reference>>,
  pub created_at: NaiveDateTime,
  pub updated_at: NaiveDateTime,
}
  • 在使用AnyValue里面的类型时,只需要解引用就可以,也可以使用deref显式解引用
<div class="card-header">
{self.description(&cve.description)}
	</div>
	  {self.cvss(cve.clone())}
	  {self.references(&cve.references)}
	  {self.weaknesses(&cve.weaknesses)}
	  {self.exploit(cve.id)}
	  {self.configurations(&cve.configurations)}
	  <Comments/>
	<div class="card-body">
</div>

自动序列化UUID

  • 在mysql驱动里没有uuid这个类型,作为代替只用byte类型,在deisel中就都使用Vec[u8]代替,但是前端对字节类型解析会是一段base64编码,所以需要对字段id序列化为前端可以识别的类型。
#[cfg_attr(feature = "openapi", derive(ToSchema))]
#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "db", derive(Queryable, Identifiable, Selectable), diesel(table_name = vendors))]
#[cfg_attr(feature = "yew", derive(Properties))]
pub struct Vendor {
  #[serde(with = "uuid_serde")]
  pub id: Vec<u8>,
  pub official: u8,
  pub name: String,
  pub description: Option<String>,
  pub meta: AnyValue<MetaData>,
  pub updated_at: NaiveDateTime,
  pub created_at: NaiveDateTime,
}
  • 使用uuid自身的serde特性创建一个mod,在要序列化字段添加#[serde(with = "uuid_serde")],即可做到Vec[u8]和uuid相互转换。
pub mod uuid_serde {
  use serde::{Deserializer, Serializer};

  pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
    match uuid::Uuid::from_slice(v) {
      Ok(u) => uuid::serde::compact::serialize(&u, s),
      Err(e) => Err(serde::ser::Error::custom(e)),
    }
  }

  pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
    match uuid::serde::compact::deserialize(d) {
      Ok(u) => Ok(u.as_bytes().to_vec()),
      Err(e) => Err(serde::de::Error::custom(e)),
    }
  }
}

参考