vaultdb_orm_macros/
lib.rs1use proc_macro::TokenStream;
25use quote::{format_ident, quote};
26use syn::{Data, DeriveInput, Fields, LitStr, parse_macro_input};
27
28#[proc_macro_derive(Note, attributes(note))]
29pub fn derive_note(input: TokenStream) -> TokenStream {
30 let input = parse_macro_input!(input as DeriveInput);
31 let name = input.ident.clone();
32
33 let mut folder: Option<LitStr> = None;
34 let mut filter: Option<LitStr> = None;
35
36 for attr in &input.attrs {
37 if !attr.path().is_ident("note") {
38 continue;
39 }
40 let parsed = attr.parse_nested_meta(|meta| {
41 if meta.path.is_ident("folder") {
42 folder = Some(meta.value()?.parse()?);
43 } else if meta.path.is_ident("filter") {
44 filter = Some(meta.value()?.parse()?);
45 } else {
46 return Err(meta.error("unknown #[note(...)] key — expected `folder` or `filter`"));
47 }
48 Ok(())
49 });
50 if let Err(e) = parsed {
51 return e.to_compile_error().into();
52 }
53 }
54
55 let folder_lit = match folder {
56 Some(f) => f,
57 None => {
58 return syn::Error::new_spanned(
59 &name,
60 "missing required #[note(folder = \"...\")] on derive(Note)",
61 )
62 .to_compile_error()
63 .into();
64 }
65 };
66
67 let discriminator_impl = match filter {
68 Some(f) => quote! {
69 fn discriminator() -> ::core::option::Option<::vaultdb_orm::Expr> {
70 ::vaultdb_orm::Expr::parse(#f).ok()
71 }
72 },
73 None => quote! {},
74 };
75
76 let accessors = match field_accessors(&input.data) {
79 Ok(toks) => toks,
80 Err(e) => return e.to_compile_error().into(),
81 };
82
83 let expanded = quote! {
84 impl ::vaultdb_orm::Note for #name {
85 const FOLDER: &'static str = #folder_lit;
86 #discriminator_impl
87 }
88
89 impl #name {
90 #accessors
91 }
92 };
93
94 expanded.into()
95}
96
97fn field_accessors(data: &Data) -> syn::Result<proc_macro2::TokenStream> {
98 let fields = match data {
99 Data::Struct(s) => match &s.fields {
100 Fields::Named(named) => &named.named,
101 Fields::Unnamed(_) => {
102 return Err(syn::Error::new_spanned(
103 &s.fields,
104 "derive(Note) requires a struct with named fields",
105 ));
106 }
107 Fields::Unit => return Ok(quote! {}),
108 },
109 Data::Enum(e) => {
110 return Err(syn::Error::new_spanned(
111 &e.enum_token,
112 "derive(Note) cannot be applied to an enum",
113 ));
114 }
115 Data::Union(u) => {
116 return Err(syn::Error::new_spanned(
117 &u.union_token,
118 "derive(Note) cannot be applied to a union",
119 ));
120 }
121 };
122
123 let mut out = proc_macro2::TokenStream::new();
124 for f in fields {
125 let ident = match &f.ident {
126 Some(i) => i,
127 None => continue,
128 };
129 let accessor_ident = format_ident!("{}", ident);
130
131 match relation_kind(f)? {
133 Some(RelationKind::Outgoing) => {
134 out.extend(quote! {
135 pub fn #accessor_ident() -> ::vaultdb_orm::RelationRef {
136 ::vaultdb_orm::RelationRef::outgoing()
137 }
138 });
139 continue;
140 }
141 Some(RelationKind::Incoming) => {
142 out.extend(quote! {
143 pub fn #accessor_ident() -> ::vaultdb_orm::RelationRef {
144 ::vaultdb_orm::RelationRef::incoming()
145 }
146 });
147 continue;
148 }
149 None => {}
150 }
151
152 let frontmatter_key = serde_rename(f).unwrap_or_else(|| ident.to_string());
153 out.extend(quote! {
154 pub fn #accessor_ident() -> ::vaultdb_orm::FieldRef {
155 ::vaultdb_orm::FieldRef::new(#frontmatter_key)
156 }
157 });
158 }
159
160 Ok(out)
161}
162
163#[derive(Debug, Clone, Copy)]
164enum RelationKind {
165 Outgoing,
166 Incoming,
167}
168
169fn relation_kind(field: &syn::Field) -> syn::Result<Option<RelationKind>> {
173 let mut kind: Option<RelationKind> = None;
174 for attr in &field.attrs {
175 if !attr.path().is_ident("note") {
176 continue;
177 }
178 attr.parse_nested_meta(|meta| {
179 if meta.path.is_ident("wikilink") {
180 kind = Some(RelationKind::Outgoing);
181 } else if meta.path.is_ident("backlink") {
182 kind = Some(RelationKind::Incoming);
183 } else {
184 return Err(meta.error(
185 "unknown #[note(...)] key on a field — expected `wikilink` or `backlink`",
186 ));
187 }
188 Ok(())
189 })?;
190 }
191 Ok(kind)
192}
193
194fn serde_rename(field: &syn::Field) -> Option<String> {
200 for attr in &field.attrs {
201 if !attr.path().is_ident("serde") {
202 continue;
203 }
204 let mut found: Option<String> = None;
205 let _ = attr.parse_nested_meta(|meta| {
206 if meta.path.is_ident("rename") {
207 if let Ok(s) = meta.value().and_then(|v| v.parse::<LitStr>()) {
208 found = Some(s.value());
209 }
210 } else {
211 if meta.input.peek(syn::Token![=]) {
214 let _: syn::Result<syn::Expr> = meta.value().and_then(|v| v.parse());
215 } else if meta.input.peek(syn::token::Paren) {
216 let _content;
217 let _ = syn::parenthesized!(_content in meta.input);
218 let _: proc_macro2::TokenStream = _content.parse().unwrap_or_default();
219 }
220 }
221 Ok(())
222 });
223 if found.is_some() {
224 return found;
225 }
226 }
227 None
228}