3 个版本 (有破坏性更新)

0.3.0 2023年7月17日
0.2.0 2023年7月17日
0.1.1 2023年3月16日
0.1.0 2023年3月16日

#1480开发工具

Download history 71/week @ 2024-03-13 61/week @ 2024-03-20 94/week @ 2024-03-27 70/week @ 2024-04-03 50/week @ 2024-04-10 55/week @ 2024-04-17 33/week @ 2024-04-24 39/week @ 2024-05-01 43/week @ 2024-05-08 27/week @ 2024-05-15 17/week @ 2024-05-22 30/week @ 2024-05-29 23/week @ 2024-06-05 67/week @ 2024-06-12 50/week @ 2024-06-19 37/week @ 2024-06-26

每月179 次下载
3 个crate中使用了(通过libtaskrs

自定义许可证

26KB
424

boilermates – 相似结构体的模板生成器

就像“模板”,但它们是伙伴... G... 理解了吗??

这不是什么

这不是Rust中继承的尝试。

那它是什么呢?

它是一个用于生成的proc_macro

  • 具有一些相同字段的相似结构体
  • 实现,以便可以轻松地在它们之间进行转换(尽可能使用From/Into,在其他情况下使用其他方法)
  • 特质,用于识别具有共同字段的类型,以便为具有这些字段的任何类型实现一次所需的功能。

为什么?

这是一个古老的故事。您正在实现一个API,您有输入类型、输出类型和内部类型,假设它将发送到数据库。它们大部分是相同的,有一个id、一个checksum或一些散布的私有数据。

因此,您最终会得到一个具有许多Optionstruct,或者多个具有许多转换实现的struct。如果它们之间有共同的功能,并且具有相同的实现,因为它们使用相同的字段,您需要复制粘贴。

是的,这不是很糟糕,但代码变得混乱,只是因为您不知道当用户发送它以创建时对象的ID等。

它是如何工作的

以以下代码为例

use boilermates::boilermates;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

// This is for illustration purposes
const UNIT_PRICE: u64 = 100;
const SHIPPING_PRICE: u64 = 50;

#[boilermates("OrderRequest", "OrderResponse")]
#[boilermates(attr_for("OrderRequest", "#[derive(Clone, Debug, Deserialize)]"))]
#[boilermates(attr_for("OrderResponse", "#[derive(Clone, Debug, Serialize)]"))]
#[derive(Clone, Debug, Deserialize, Serialize)]
struct Order {
    user_id: u64,
    amount: u64,
    address: String,
    #[serde(default)]
    comments: Option<String>,
    shipping_required: bool,

    #[boilermates(not_in("OrderRequest"))]
    id: Uuid,
    
    #[boilermates(only_in("OrderRequest"))]
    jwt_token: String,

    #[boilermates(only_in("Order", "OrderResponse"))]
    #[boilermates(default)]
    status: OrderStatus,

    #[boilermates(only_in_self)]
    #[boilermates(default)]
    assigned_employee_id: Option<u64>,

    #[boilermates(only_in("OrderResponse"))]
    #[boilermates(default)]
    response_code: ResponseCode,
}

// This is for illustration purposes
#[derive(Clone, Debug, Deserialize, Serialize)]
enum OrderStatus {
    Received,
    Packaging,
    Shipped,
}

impl Default for OrderStatus {
    fn default() -> Self {
        Self::Received
    }
}

// This is for illustration purposes
#[derive(Clone, Debug, Deserialize, Serialize)]
enum ResponseCode {
    Ok,
    BadRequest,
    ServerError,
}

impl Default for ResponseCode {
    fn default() -> Self {
        Self::Ok
    }
}

结构体生成

这将创建3个结构体。

首先,Orders 将包含所有字段,除了仅在 OrderRequest 中存在的 jwt_token 字段,以及由于 #[boilermates(only_in("OrderRequest"))]response_code 字段,该字段仅在 OrderResponse 中存在,因为 #[boilermates(only_in("OrderResponse"))]

struct Order {
    user_id: u64,
    amount: u64,
    address: String,
    #[serde(default)]
    comments: Option<String>,
    shipping_required: bool,
    status: OrderStatus,
    id: Uuid,
    assigned_employee_id: Option<u64>,
}

然后是 OrderRequest,它将不会包含 id 字段,因为它被标记为 #[boilermates(not_in("OrderRequest"))]status 字段因为它在 #[boilermates(only_in("Order", "OrderResponse"))] 中未提及,以及 assigned_employee_id 字段,因为它被标记为 #[boilermates(only_in_self)],这相当于 #[boilermates(only_in("Order"))]。然而,它将包含 jwt_token

struct OrderRequest {
    user_id: u64,
    amount: u64,
    address: String,
    #[serde(default)]
    comments: Option<String>,
    shipping_required: bool,
    jwt_token: String,
}

最后,OrderResponse 将包含 Order 所有的内容,加上 response_code 字段,但不包含 assigned_employee_id 字段,这显然与客户无关。

struct OrderResponse {
    user_id: u64,
    amount: u64,
    address: String,
    #[serde(default)]
    comments: Option<String>,
    shipping_required: bool,
    id: Uuid,
    status: OrderStatus,
    response_code: ResponseCode,
}

转换

现在来点有趣的东西。假设我们通过API接收到了一个新的订单,并且在 request 变量中有一个 OrderRequest 对象。我们可以轻松地将它转换为 Order,只需要填写缺失的数据。我们可以有两种方法来做这件事。首先,使用 into_order 方法,该方法按照原始 struct 声明中定义的顺序接受 Order 缺失的参数。它的签名是 pub fn into_order(self, id: Uuid, status: OrderStatus assigned_employee_id: Option<u64> ) -> Order,所以我们可以这样操作

let order = request.into_order(Uuid::new_v4(), OrderStatus::Received, None);

但是,statusassigned_employee_id 被标记为 #[boilermates(default)],所以当我们想要将这些字段转换为具有这些字段的类型时使用默认值,我们可以使用

let order = request.into_order_defaults(Uuid::new_v4);

接下来,在我们成功将订单保存到数据库后,我们可以这样将其转换为 OrderResponse

let response = order.into_order_response(ResponseCode::Ok);

但是,由于 ResponseCode 有一个 Default 实现,return_code 被标记为 #[boilermates(default)],并且所有其他来自 Order 的字段都包含在 OrderResponse 中,我们可以这样做

let response = OrderResponse::from(order); // or `let response: OrderResponse = order.into()`

当转换不需要额外参数时,所有情况都实现了 From/Into 转换。

泛型实现

每个字段都会触发生成一个带有获取器方法 fn {field}(&self) -> &{field_type}Has{Field} 特性,以及一个设置器方法 fn set_{field}(&mut self, value: &{field_type}),每个具有字段的 struct 都有相应的实现。

由于这3个结构体共享大量相同的数据,它们可以实现一些相同的功能。例如,如果我们想找出订单总额是多少(还记得示例开头中的 UNIT_PRICESHIPPING_PRICE 吗?),我们可以创建一个通用的实现,使用 HasAmountHasShippingRequired 特性,这些特性已经为所有具有 amountshipping_required 字段的类型实现。这允许我们像这样使用 amount()shipping_required() 获取器方法

// These work out of the box:
request.set_amount(10);
order.set_amount(10);
response.set_amount(10);

trait GetTotal: HasAmount + HasShippingRequired {
    fn total(&self) -> u64 {
        self.amount() * UNIT_PRICE
            + if *self.shipping_required() {
                SHIPPING_PRICE
            } else {
                0
            }
    }
}
impl<T: HasAmount + HasShippingRequired> GetTotal for T {}

// Now all of these work:
let total = request.total()
let total = order.total()
let total = response.total()

类似地,对于每个不包含特定字段的struct,会生成一个 'HasNo{Field}' 特性。

依赖项

~1.5MB
~35K SLoC