typeshaper 0.1.1

TypeScript utility-type idioms (Omit, Pick, Merge, Partial…) for Rust structs — one-line type algebra expressions
Documentation

typeshaper

typeshaper 让你用一行表达式从已有结构体派生新的结构体类型——省略字段、挑选字段、合并两个结构体、将所有字段置为可选,或恢复为必填。生成的类型自动获得转换 impl,可链式组合。

你有没有写过这样的代码

pub struct User {
    pub id: u64,
    pub name: String,
    pub email: String,
    pub password_hash: String,
    pub role: String,
    pub active: bool,
    pub created_at: i64,
}

然后 API 层要用,不能把 password_hash 暴露出去,于是你复制了一份:

pub struct UserPublic {
    pub id: u64,
    pub name: String,
    pub email: String,
    // password_hash 不要
    pub role: String,
    pub active: bool,
    pub created_at: i64,
}

impl From<User> for UserPublic {
    fn from(u: User) -> Self {
        Self {
            id: u.id,
            name: u.name,
            email: u.email,
            role: u.role,
            active: u.active,
            created_at: u.created_at,
        }
    }
}

然后搜索功能只需要 id 和 name,又复制一份:

pub struct UserSummary {
    pub id: u64,
    pub name: String,
}
// ... 又一个 From ...

然后增量更新接口要求所有字段可选,又复制一份:

pub struct UserPatch {
    pub id: Option<u64>,
    pub name: Option<String>,
    pub email: Option<String>,
    pub password_hash: Option<String>,
    // ...
}
// ... 又一个 From ...

User 加了一个字段,UserPublicUserPatchUserSummary 全部都要跟着改——改结构体、改 From、改可能遗漏的测试。

这只是 User。你还有 OrderProductArticleComment……


换一种写法

[dependencies]
typeshaper = "0.3"
use typeshaper::{TypeshaperExt, typeshaper, typex};

#[typeshaper]
#[derive(Debug, Clone)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub email: String,
    pub password_hash: String,
    pub role: String,
    pub active: bool,
    pub created_at: i64,
}

// 去掉两个字段
typex!(#[derive(Debug, Clone)] UserPublic  = User - [password_hash, created_at]);

// 只保留两个字段
typex!(#[derive(Debug, Clone)] UserSummary = User & [id, name]);

// 所有字段变可选
typex!(#[derive(Debug, Clone)] UserPatch   = User?);

转换直接用:

let user: User = /* 从数据库来的 */;

let public:  UserPublic  = user.clone().project();  // 自动去掉 password_hash、created_at
let summary: UserSummary = user.clone().project();  // 只有 id 和 name
let patch    = UserPatch::from(user);               // 所有字段变 Option

User 加字段,三行 typex!() 不用改,新字段自动跟着走。


故事还没完:两个来源合并成一个

订单快照需要同时包含用户信息和地址信息:

#[typeshaper]
pub struct Address {
    pub street: String,
    pub city: String,
    pub country: String,
}

// 把 User 和 Address 合进一个新类型
typex!(#[derive(Debug, Clone)] OrderSnapshot = User + Address);

let snapshot = OrderSnapshot::from((user, address));

只需要"User 有而 Address 没有"的字段:

typex!(#[derive(Debug, Clone)] UserOnly = User % Address);  // Diff

表达式可以链式组合

// 先去掉 password_hash,再把剩余字段全部变可选
typex!(#[derive(Debug)] UserSafePatch = User - [password_hash]?);

// 去掉 password_hash 之后,只留摘要字段
typex!(#[derive(Debug)] UserSafeDto = User - [password_hash] & [id, name, email]);

// 括号控制结合方向:先 Partial 再 Required(等价于恢复非可选)
typex!(#[derive(Debug)] UserRestored = (User - [password_hash])?!);

增量更新的完整循环

// 可选版本用于更新接口
typex!(#[derive(Debug, Clone)] UserPatch    = User?);

// 验证通过后恢复为必填版本
typex!(#[derive(Debug, Clone)] UserVerified = UserPatch!);

// ---

let patch = UserPatch {
    name: Some("alice".into()),
    email: Some("new@example.com".into()),
    // 其他字段留 None,表示"不更新"
    ..Default::default()
};

// 如果所有字段都已填写,可以恢复为强类型
match UserVerified::try_from(patch) {
    Ok(verified) => { /* 提交 */ }
    Err(e)       => { /* 告知哪个字段缺失 */ }
}

跨 crate:模型定义在一个地方

领域层定义模型,API 层按需派生,不复制结构体:

// core-crate/src/lib.rs
#[typeshaper(export)]          // export 多生成一个伴生宏
#[derive(Debug, Clone)]
pub struct User { /* ... */ }
// 自动生成并导出:pub macro typeshaper_import_User!()
// api-crate/src/lib.rs
use core_crate::{User, typeshaper_import_User};

typeshaper_import_User!();  // 把 User 的字段元数据注册到本 crate

// 之后完全和本地 #[typeshaper] 类型一样用
typex!(#[derive(Debug, Clone)] UserPublic = User - [password_hash, created_at]);
typex!(#[derive(Debug, Clone)] UserPatch  = User?);

参考文档

安装

[dependencies]
typeshaper = "0.3"

前置标注:#[typeshaper]

在源结构体上添加一次,字段信息写入编译期注册表,结构体本身原样保留。 支持两种形式:

形式 作用
#[typeshaper] 本 crate 内使用
#[typeshaper(export)] 本 crate 内使用 + 生成 typeshaper_import_T!() 供其他 crate 调用
#[typeshaper]
#[derive(Debug, Clone, PartialEq)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub age: u8,
    pub email: String,
}

#[typeshaper] 可以叠放在任意其他属性之上,不影响结构体的任何已有行为。


操作符速查

语法 名称 含义 生成的 impl
T - [f1, f2] Omit 移除列出的字段 TypeshaperInto<Target> for T
T & [f1, f2] Pick 只保留列出的字段 TypeshaperInto<Target> for T
A + B Merge 合并 A 和 B 的全部字段(不允许重名) From<(A, B)> for Target
T? Partial 所有字段变为 Option<_> From<T> for Target
T! Required 还原 Partial 的 Option<_> TryFrom<T> for Target
A % B Diff A 有而 B 没有的字段 TypeshaperInto<Target> for A

组合规则

操作符左结合,可加括号改变结合方向:

// User - [age] & [id, name]  等价于  (User - [age]) & [id, name]
typex!(Dto      = User - [age] & [id, name]);

// 括号让右侧先求值
typex!(Full     = User + (Badge - [label]));

// 后缀链式
typex!(Draft    = User - [password_hash]?);
typex!(Roundtrip = (User - [password_hash])?!);

typex!() 语法

typex!( [#[attr...]]  TargetName  =  Expr );
  • 属性(可选):写在 TargetName 前,原样附加到生成的结构体,支持叠放多个。typex!() 不会自动添加任何 #[derive],全部由调用方声明。
  • TargetName:生成的结构体名称,同时注册到编译期注册表,可继续作为后续 typex!() 的输入。
  • Expr:类型代数表达式,见上表。
typex!(
    #[derive(Debug, Clone, PartialEq)]
    #[serde(rename_all = "camelCase")]
    UserPublicDto = User & [id, name, email]
);

转换方法

TypeshaperExt trait 为所有类型自动实现,通过类型推断选择目标:

let public: UserPublic = user.project();   // 等价于 user.typeshaper_into()

Merge 使用元组 FromPartial 使用 FromRequired 使用 TryFrom

let snapshot = OrderSnapshot::from((user, address));
let draft    = UserPatch::from(user);
let verified = UserVerified::try_from(draft)?;

跨 crate 详细用法

导出端

// core-crate/src/lib.rs
use typeshaper::typeshaper;

#[typeshaper(export)]
#[derive(Debug, Clone)]
pub struct User {
    pub id: u64,
    pub name: String,
    pub role: String,
    pub active: bool,
}
// 编译后自动导出:pub macro typeshaper_import_User!()

导入端

// app-crate/src/lib.rs
use typeshaper::typex;
use core_crate::{User, typeshaper_import_User};

typeshaper_import_User!();  // 仅一次,写在模块顶层

typex!(#[derive(Debug, Clone)] UserPublic = User - [role, active]);
typex!(#[derive(Debug, Clone)] UserPatch  = User?);

多个类型分别调用各自的伴生宏,跨 crate Merge / Diff 同样支持:

use core_crate::{Address, typeshaper_import_Address};

typeshaper_import_User!();
typeshaper_import_Address!();

typex!(#[derive(Debug, Clone)] OrderSnapshot = User + Address);
typex!(#[derive(Debug, Clone)] UserOnly      = User % Address);
本 crate 内 跨 crate
源类型标注 #[typeshaper] #[typeshaper(export)]
调用方前置步骤 typeshaper_import_T!()
typex!() 语法 完全相同 完全相同

已支持的操作

  • Omit — T - [fields]
  • Pick — T & [fields]
  • Merge — A + B
  • Partial — T?
  • Required — T!
  • Diff — A % B
  • 表达式组合与链式操作
  • 属性透传
  • 跨 crate 导出 / 导入