hyperdb_api_derive/
lib.rs1use proc_macro::TokenStream;
49use proc_macro2::TokenStream as TokenStream2;
50use quote::quote;
51use syn::{
52 parse_macro_input, spanned::Spanned, Data, DataStruct, DeriveInput, Field, Fields,
53 GenericArgument, LitInt, LitStr, PathArguments, Type, TypePath,
54};
55
56enum FieldSource {
60 Name(String),
61 Index(usize),
62}
63
64#[proc_macro_derive(FromRow, attributes(hyperdb))]
68pub fn from_row_derive(input: TokenStream) -> TokenStream {
69 let input = parse_macro_input!(input as DeriveInput);
70 match expand(&input) {
71 Ok(ts) => ts.into(),
72 Err(e) => e.to_compile_error().into(),
73 }
74}
75
76fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
77 let name = &input.ident;
78 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
79
80 let fields = match &input.data {
81 Data::Struct(DataStruct {
82 fields: Fields::Named(named),
83 ..
84 }) => &named.named,
85 Data::Struct(_) => {
86 return Err(syn::Error::new_spanned(
87 &input.ident,
88 "FromRow can only be derived on structs with named fields",
89 ));
90 }
91 Data::Enum(_) => {
92 return Err(syn::Error::new_spanned(
93 &input.ident,
94 "FromRow cannot be derived on enums",
95 ));
96 }
97 Data::Union(_) => {
98 return Err(syn::Error::new_spanned(
99 &input.ident,
100 "FromRow cannot be derived on unions",
101 ));
102 }
103 };
104
105 let assignments = fields
106 .iter()
107 .map(field_assignment)
108 .collect::<syn::Result<Vec<_>>>()?;
109
110 Ok(quote! {
111 #[automatically_derived]
112 impl #impl_generics ::hyperdb_api::FromRow for #name #ty_generics #where_clause {
113 fn from_row(
114 row: ::hyperdb_api::RowAccessor<'_>,
115 ) -> ::hyperdb_api::Result<Self> {
116 Ok(Self {
117 #(#assignments),*
118 })
119 }
120 }
121 })
122}
123
124fn field_assignment(field: &Field) -> syn::Result<TokenStream2> {
127 let ident = field
128 .ident
129 .as_ref()
130 .ok_or_else(|| syn::Error::new_spanned(field, "tuple-struct fields are not supported"))?;
131 let source = field_source_for(field, ident)?;
132 let is_opt = is_option_type(&field.ty);
133
134 let getter = match (source, is_opt) {
135 (FieldSource::Name(name), true) => {
136 let lit = LitStr::new(&name, ident.span());
137 quote!(row.get_opt(#lit)?)
138 }
139 (FieldSource::Name(name), false) => {
140 let lit = LitStr::new(&name, ident.span());
141 quote!(row.get(#lit)?)
142 }
143 (FieldSource::Index(idx), true) => quote!(row.position_opt(#idx)?),
144 (FieldSource::Index(idx), false) => quote!(row.position(#idx)?),
145 };
146
147 Ok(quote! { #ident: #getter })
148}
149
150fn field_source_for(field: &Field, default: &syn::Ident) -> syn::Result<FieldSource> {
154 let mut rename: Option<(String, proc_macro2::Span)> = None;
155 let mut index: Option<(usize, proc_macro2::Span)> = None;
156
157 for attr in &field.attrs {
158 if !attr.path().is_ident("hyperdb") {
159 continue;
160 }
161 attr.parse_nested_meta(|meta| {
162 if meta.path.is_ident("rename") {
163 let s: LitStr = meta.value()?.parse()?;
164 rename = Some((s.value(), meta.path.span()));
165 Ok(())
166 } else if meta.path.is_ident("index") {
167 let n: LitInt = meta.value()?.parse()?;
168 let parsed: usize = n.base10_parse()?;
169 index = Some((parsed, meta.path.span()));
170 Ok(())
171 } else {
172 Err(meta.error(format!(
173 "unrecognized hyperdb attribute `{}`; supported attributes: rename, index",
174 meta.path
175 .get_ident()
176 .map_or_else(|| "?".to_string(), ToString::to_string)
177 )))
178 }
179 })?;
180 }
181
182 match (rename, index) {
183 (Some(_), Some((_, idx_span))) => Err(syn::Error::new(
184 idx_span,
185 "`#[hyperdb(rename = ...)]` and `#[hyperdb(index = N)]` are mutually exclusive",
186 )),
187 (Some((name, _)), None) => Ok(FieldSource::Name(name)),
188 (None, Some((idx, _))) => Ok(FieldSource::Index(idx)),
189 (None, None) => Ok(FieldSource::Name(default.to_string())),
190 }
191}
192
193fn is_option_type(ty: &Type) -> bool {
195 let Type::Path(TypePath { path, qself: None }) = ty else {
196 return false;
197 };
198 let Some(last) = path.segments.last() else {
199 return false;
200 };
201 if last.ident != "Option" {
202 return false;
203 }
204 matches!(
205 last.arguments,
206 PathArguments::AngleBracketed(ref args)
207 if matches!(args.args.first(), Some(GenericArgument::Type(_)))
208 )
209}