如何通过字段属性将Rust结构体转换为适配Notion API的HashMap?
如何通过字段属性将Rust结构体转换为适配Notion API的HashMap?
嘿,这个需求我太懂了!手动写一堆HashMap插入和类型转换的样板代码,不仅容易写错,改起来也麻烦。咱们可以用Rust的过程宏来搞定这个——自定义一个字段属性,配合派生宏,让结构体自动生成适配Notion API的HashMap转换逻辑,完全解放双手!
核心思路
咱们要做两件事:
- 定义一个自定义属性
#[notion_property],用来标记每个字段对应的Notion属性名称和类型 - 写一个派生宏
#[derive(NotionConvert)],让结构体自动实现一个方法(比如into_notion_hashmap()),把结构体实例转成Notion API需要的HashMap<String, PropertyValue>
准备依赖
首先得在Cargo.toml里加上过程宏相关的依赖:
[dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # 假设你已经有了Notion API的类型定义,比如自己封装的PropertyValue、RichTextObject等 [dev-dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full", "derive"] }
实现自定义属性和派生宏
咱们先写宏的代码(一般放在单独的proc-macro crate里,或者用#[proc_macro_derive]标记在lib crate中):
1. 解析自定义属性
首先用syn来解析#[notion_property(type = "...", name = "...")]这个属性,提取我们需要的参数:
use syn::{Attribute, Lit, Meta, MetaNameValue}; // 从字段属性中提取Notion属性的名称和类型 fn parse_notion_attrs(attrs: &[Attribute]) -> Option<(String, String)> { for attr in attrs { if attr.path().is_ident("notion_property") { let meta = attr.parse_meta().ok()?; let mut prop_name = None; let mut prop_type = None; if let Meta::List(list) = meta { for nested in list.nested { if let Meta::NameValue(nv) = nested { let key = nv.path.get_ident()?.to_string(); let value = if let Lit::Str(s) = nv.lit { s.value() } else { continue; }; match key.as_str() { "name" => prop_name = Some(value), "type" => prop_type = Some(value), _ => {} } } } } if let (Some(name), Some(ty)) = (prop_name, prop_type) { return Some((name, ty)); } } } None }
2. 实现派生宏
接下来写#[derive(NotionConvert)]的核心逻辑,遍历结构体的每个字段,根据属性生成对应的转换代码:
use proc_macro::TokenStream; use quote::quote; use syn::{DataStruct, DeriveInput, Fields}; #[proc_macro_derive(NotionConvert, attributes(notion_property))] pub fn derive_notion_convert(input: TokenStream) -> TokenStream { let ast = syn::parse_macro_input!(input as DeriveInput); let name = ast.ident; // 只支持结构体类型 let data = match ast.data { syn::Data::Struct(DataStruct { fields, .. }) => fields, _ => panic!("NotionConvert只能派生给结构体使用"), }; let mut conversions = Vec::new(); for field in data { let field_ident = field.ident.as_ref().unwrap(); // 提取字段上的notion_property属性 let (prop_name, prop_type) = parse_notion_attrs(&field.attrs) .expect("字段缺少#[notion_property]属性"); // 根据不同的Notion属性类型生成转换逻辑 let conversion = match prop_type.as_str() { "title" => { quote! { tmp.insert(#prop_name.to_string(), PropertyValue::Title(vec![ RichTextObject { annotations: None, data_type: Some(RichTextType::Text), equation: None, mention: None, plain_text: Some(self.#field_ident.clone()), href: None, text: Some(TextObject { content: self.#field_ident.clone(), link: None }) } ])); } } "url" => { quote! { if let Some(url) = self.#field_ident.clone() { tmp.insert(#prop_name.to_string(), PropertyValue::Url(url)); } } } "MultiSelect" => { quote! { if let Some(items) = &self.#field_ident { tmp.insert(#prop_name.to_string(), PropertyValue::MultiSelect( items.iter().map(|f| SelectObject { id: None, color: None, name: format!("{}", f) }).collect() )); } } } // 这里可以扩展更多Notion属性类型,比如Number、Checkbox、Date等 _ => panic!("不支持的属性类型: {}", prop_type), }; conversions.push(conversion); } // 生成最终的结构体实现代码 let expanded = quote! { impl #name { pub fn into_notion_hashmap(self) -> std::collections::HashMap<String, PropertyValue> { let mut tmp = std::collections::HashMap::new(); #(#conversions)* tmp } } }; expanded.into() }
实际使用示例
现在回到你给的结构体,加上咱们的派生宏和属性:
#[derive(Serialize, Deserialize, NotionConvert)] struct MyUnit { #[notion_property(type = "title", name = "Name")] pub name: String, #[notion_property(type = "url", name = "Icon url")] pub icon_url: Option<String>, #[notion_property(type = "MultiSelect", name = "Roles")] pub categories: Option<Vec<serde_json::Value>>, }
然后你只需要一行代码就能得到想要的HashMap了:
let unit = MyUnit { name: "我的第一个页面".to_string(), icon_url: Some("https://example.com/icon.png".to_string()), categories: Some(vec![serde_json::json!("管理员"), serde_json::json!("编辑")]), }; let notion_properties = unit.into_notion_hashmap(); // 这个notion_properties就是完全适配Notion API的HashMap啦!
一些小提示
- 扩展属性类型:上面只实现了Title、Url、MultiSelect三种类型,你可以根据Notion API的所有属性类型继续补充宏里的匹配逻辑
- 克隆优化:示例里用了
clone()是因为Notion API的类型一般需要所有权,如果你的结构体字段可以放弃所有权,也可以改成直接转移所有权 - 编译期检查:如果字段类型和属性类型不匹配(比如把i32字段标记为url类型),宏会在编译期就报错,比运行时发现问题更友好
内容来源于stack exchange
相关产品推荐
相关产品推荐

