#dto #data-transfer #field-name #data-transfer-object #model-mapper #dto-pattern

dto_mapper

一个用于从结构体创建动态 DTO(数据传输对象)的库

3 个不稳定版本

0.2.0 2024 年 4 月 25 日
0.1.3 2023 年 12 月 23 日
0.1.2 2023 年 11 月 20 日

#611 in 网页编程

Apache-2.0

49KB
759

dto_mapper

这是一个用于为基于 Rust 的项目创建动态 DTO 的库。它与在 Java SpringBoot 应用程序中广泛使用的已知 Java DTO Mapper 库 Model Java 具有相同的目的。

DTO 代表数据传输对象。它是一种软件设计模式,涉及通过重用一些属性将父对象映射到新的对象。例如,如果一个应用程序正在处理敏感数据,如信用卡信息、密码或任何其他不应通过网络或以任何形式显示给用户的敏感信息。因此,应用程序需要一种方法来通过删除它们不希望以 JSON 或任何其他格式暴露给用户的属性或字段,将此实体映射到新的一个。

这个库非常方便。它帮助用特殊属性注解结构体,以便提取字段以组成新的结构体对象以供重用。这个库基于 Syn 库和 Rust 中嵌入的宏 derive 属性的强大功能。DTO 是在编译时生成的。由于最终的二进制文件将包含构建时生成的 DTO 结构,因此在运行时没有开销。我建议使用安装了 rust analyzer 插件的 Visual Studio code,以便自动完成并提供更好的开发体验,当悬停在 DTO 结构上时可以预览字段名称。

摘要

此库可用于使用 DAO(数据访问对象)模式在数据库和发送信息到用户的控制器之间进行数据转换的网页应用程序。让我们考虑一个应用程序用来存储和检索用户记录的用户实体。让我们这样描述我们的用户实体

    struct User{
        username: String,
        password: String,
        email: String,
        firstname: String,
        middle_name: Option<String>,
        lastname: String,
        age: u8,
    }

如果我们需要从数据库中加载一个 'user' 记录,我们不想发送所有这些信息回网页,因为在不同的场景下并不需要。如果我们需要重用信息发送到网页,我们希望从用户中移除 密码 信息。有人会坚持说我们可以使用 Json 序列化库,并注释密码字段以便忽略它。好吧,让我们更进一步。如果我们有一个后端应用程序,它为认证请求、个人资料信息请求或其他以数据混合形式请求其他信息的服务客户端提供服务,而我们只想获取该人的姓名、姓和年龄。Json 注解在这里就无能为力了。我们不得不手动创建这些对象,重复相同的信息,并实现适当的转换方法。这听起来像是太多的工作。这正是 DTO 映射库派上用场的地方。

安装

dto_mapper 库依赖于 unstringifyderive_builder,后者实现了 DTO 映射器生成的 dto 对象的构建器模式。默认情况下,它会为 dto 生成构建器。您可以使用以下指令安装 dto_mapper 库的最新版本到您的项目中:

cargo add derive_builder
cargo add dto_mapper
cargo add unstringify

并将其导入您需要使用的 rust 源文件

use dto_mapper::DtoMapper;

有关如何使用 derive_builder crate 的更多详细信息,请参阅: https://crates.io/crates/derive_builder

示例

让我们假设我们想为我们的应用程序创建 3 个特殊的实体

  • LoginDto,它将只包含来自 User 的两个字段,例如 usernamepassword。我们希望在 LoginDto 中将 username 重命名为 login
  • ProfileDto,它将包含来自 User 的所有字段,但会忽略 密码 字段。
  • PersonDto,它将只包含来自 User 的 3 个字段,例如 firstnamelastnameemail。但我们希望将 email 字段设置为可选的,使其最终数据类型为 Option。也就是说,如果 email 在 User 中是 String,它将结果为 Option

只需以下几行代码即可完成这项工作。转换是自动在 dto 类型与其原始结构体之间进行的。

  use dto_mapper::DtoMapper;

  /*** Use this declaration below in lib.rs if you're using a library crate , or in main.rs if you're using a binary crate.
   if your crate has lib.rs and main.rs. Use it instead inside your lib.rs.
  ***/
  #[macro_use]
  extern crate derive_builder;
  #[allow(unused)]
  use std::str::FromStr;
  #[allow(unused)]
  use unstringify::unstringify;

  fn concat_str(s1: &str, s2: &str) -> String {
      s1.to_owned() + " " + s2
  }

  #[derive(DtoMapper,Default,Clone)]
  #[mapper( dto="LoginDto"  , map=[ ("username:login",true) , ("password",true)] , derive=(Debug, Clone, PartialEq) )]
  #[mapper( dto="ProfileDto" , ignore=["password"]  , derive=(Debug, Clone, PartialEq) )]
  //no_builder=true will not create default builder for that dto
  #[mapper( dto="PersonDto" , no_builder=true,  map=[ ("firstname",true), ("lastname",true), ("email",false) ]  )] 
  #[mapper( dto="CustomDto" , no_builder=true , map=[ ("email",false) ] , derive=(Debug, Clone) ,
      new_fields=[( "name: String", "concat_str( self.firstname.as_str(), self.lastname.as_str() )" )]
  )]
  struct User{
      username: String,
      password: String,
      email: String,
      firstname: String,
      middle_name: Option<String>,
      lastname: String,
      age: u8,
  }

let login_dto : LoginDto = LoginDto::default();
let profile_dto: ProfileDto = ProfileDto::default();

假设我们有一个 User 结构体值,我们想将其转换回 dto 对象

        let user = User{
            username : "dessalines".to_string(),
            email: "[email protected]".to_string(),
            password: "XXXXXXXXXXXXX".into(),
            firstname: "Dessalines".to_string(),
            lastname: "Jean jacques".to_string(),
            age: 50,
            ..User::default()
        };

        //clone user as into moves user after into() operation in order to reuse user in subsequent calls
        let lg_dto_user : LoginDto = user.clone().into();
        let pf_dto_user: ProfileDto = user.clone().into();

        println!("User to LoginDto = {:?}",lg_dto_user);
        println!("User to ProfileDto = {:?}",pf_dto_user);

假设现在我们有一个 PersonDto,我们想将其部分转换回 User 对象,知道 PersonDto 缺少一些字段

        let person = PersonDto{
            firstname: "Dessalines".to_string(),
            lastname: "Jean Jacques".to_string(),
            email: Some("[email protected]".to_string()),
        };

        let user_from_person: User = person.into();

假设使用 dto_mapper 生成的构建器模式对象构建 LoginDto

        let mut login_dto_builder = LoginDtoBuilder::default();
        let login_dto = login_dto_builder
            .login("capois-lamort".into())
            .password("hello123".into())
            .build()
            .expect("Failed to build login dto");
        println!("LoginDto built with a builder: {:?}", login_dto);

以下是 vscode 打印出由 DTO 映射器为 LoginDtoProfileDto 生成的代码

pub struct LoginDto {
    pub login: String,
    pub password: String,
} // size = 48 (0x30), align = 0x8

pub struct ProfileDto {
    pub username: String,
    pub email: String,
    pub firstname: String,
    pub middle_name: Option<String>,
    pub lastname: String,
    pub age: u8,
} // size = 128 (0x80), align = 0x8

pub struct PersonDto {
    pub email: Option<String>,
    pub firstname: String,
    pub lastname: String,
} // size = 72 (0x48), align = 0x8

假设通过添加一个名为 name 的新字段来构建 CustomDto,该字段将使用 User 结构体中的 firstname 和 lastname 字段进行初始化,并使用 dto_mapper 进行初始化

        let user = User {
            firstname: "Dessalines".into(),
            lastname: "Jean Jacques".into(),
            ..User::default()
        };

        println!("{:?}", user);
        let custom_dto: CustomDto = user.into();
        println!("{:?}", custom_dto);

以下是 vscode 打印出由 DTO 映射器为 CustomDTOUser 为其实现的 Into Trait 生成的代码

pub struct CustomDto {
    pub email: Option<String>,
    pub name: String,
}

impl Into<CustomDto> for User {
    fn into(self) -> CustomDto {
        CustomDto {
            email: Some(self.email),
            name: concat_str(self.firstname.as_str(), self.lastname.as_str()),
        }
    }
}

您可以通过安装 'expand' 二进制 crate 并使用 cargo expand 命令来打印出上面所示的 DTO 结构

cargo install expand
cargo expand

Dto 映射器的宏 derive 和属性的描述

首先,DTO 映射库要求源结构体实现 Default 特性,因为库依赖于它来实现 DTO 和源结构体之间的 Into Traits 转换。如果没有,它将在您的 IDE 中导致错误。为源结构体 derive 或实现 Default 是必须的。

#[derive(DtoMapper,Default,Clone)]
struct SourceStruct{ }
  • #[mapper()] 属性

    mapper 属性可以重复,以便创建所需的任何 dto。每个 mapper 代表一个具体的 dto 结构。
    • 必填字段 如果不存在,会导致构建错误。
      • dto:将生成具有相同名称的结构的 dto 名称。例如:dto="MyDto" 将生成名为 MyDto 的结构。dto 名称必须唯一。否则,将导致构建错误。

      • map:从原始结构到新 dto 字段包含或映射的字段名称数组。map=[("fieldname:new_fieldname", required_flag )]fieldname:new_fieldname 将将源字段重命名为新字段。不强制重命名。可以具有 map=[("fieldname",true)] required_flag 可以是 true 或 false。如果 required_flag 为 false,则将字段在 dto 中变为 Option 类型。

        如果 required_flag 设置为 true,则目标 dto 字段将与结构中的源字段完全相同类型。

    • 可选字段
      • ignore:不包含在目标 dto 中的字段名称数组。ignore=["field1", "field1"] 如果存在 ignore,则 map 字段变为可选。除非需要重命名 dto 的目标字段。
      • derive:要从其中派生的宏列表。derive=(Debug,Clone)
      • no_builder:一个布尔标志,用于打开或关闭 dto 的构建器。默认值是 false。如果 dto 名称是 "MyDto",构建器将创建一个名为 "MyDtoBuilder" 的结构,可以用来构建 "MyDto" 结构。
      • new_fields : 一个包含要添加到结果 dto 结构中的新字段名称声明的数组。 new_fields=[("fieldname:type"), ("initialize_expression") )]. fieldname:type 将创建一个名为 fieldname 的新字段,其类型为 type。不需要重命名。可以有 map=[("fieldname",true)] initialize_expression 用于将原始结构转换为 dto 时的初始化值或表达式。例如,new_fields=[("name:String"), ("concat_str(self.firstname,self.lastname)") )] 将在 dto 中创建一个名为 name 的新字段,该字段将使用原始结构 firstnamelastname 字段的拼接初始化。请参见上面的示例。 强烈建议在更复杂的场景中使用函数作为 initialize_expression,以防在直接编写复杂的内联表达式时解析失败。这将降低代码复杂性!!

依赖关系

~0.8–1.2MB
~27K SLoC