You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过字段属性将Rust结构体转换为适配Notion API的HashMap?

如何通过字段属性将Rust结构体转换为适配Notion API的HashMap?

嘿,这个需求我太懂了!手动写一堆HashMap插入和类型转换的样板代码,不仅容易写错,改起来也麻烦。咱们可以用Rust的过程宏来搞定这个——自定义一个字段属性,配合派生宏,让结构体自动生成适配Notion API的HashMap转换逻辑,完全解放双手!

核心思路

咱们要做两件事:

  1. 定义一个自定义属性#[notion_property],用来标记每个字段对应的Notion属性名称和类型
  2. 写一个派生宏#[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啦!

一些小提示

  1. 扩展属性类型:上面只实现了Title、Url、MultiSelect三种类型,你可以根据Notion API的所有属性类型继续补充宏里的匹配逻辑
  2. 克隆优化:示例里用了clone()是因为Notion API的类型一般需要所有权,如果你的结构体字段可以放弃所有权,也可以改成直接转移所有权
  3. 编译期检查:如果字段类型和属性类型不匹配(比如把i32字段标记为url类型),宏会在编译期就报错,比运行时发现问题更友好

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 13:13:04