use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, spanned::Spanned, Attribute, Fields, ItemStruct};
#[proc_macro_attribute]
pub fn table(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr2: proc_macro2::TokenStream = attr.into();
if !attr2.is_empty() {
if let Err(e) = parse_table_args(attr2) {
return e.to_compile_error().into();
}
}
let mut item_struct = parse_macro_input!(item as ItemStruct);
for (marker, allowed) in STRUCT_HELPERS {
if let Err(e) = validate_attrs(&item_struct.attrs, marker, allowed) {
return e.to_compile_error().into();
}
}
item_struct
.attrs
.retain(|a| !STRUCT_HELPERS.iter().any(|(m, _)| is_marker(a, m)));
if let Fields::Named(fields) = &mut item_struct.fields {
for field in &mut fields.named {
if let Err(e) = validate_attrs(&field.attrs, "column", COLUMN_KEYS) {
return e.to_compile_error().into();
}
field.attrs.retain(|a| !is_marker(a, "column"));
}
}
quote! { #item_struct }.into()
}
const COLUMN_KEYS: &[&str] = &[
"primary_key",
"auto_increment",
"unique",
"default",
"default_sql",
"check",
"references",
"on_delete",
"on_update",
"generated",
"generated_kind",
];
const INDEX_KEYS: &[&str] = &["name", "columns", "unique"];
const PRIMARY_KEY_KEYS: &[&str] = &["columns"];
const FOREIGN_KEY_KEYS: &[&str] = &["columns", "references", "on_delete", "on_update"];
const CHECK_KEYS: &[&str] = &["name", "expr"];
const STRUCT_HELPERS: &[(&str, &[&str])] = &[
("index", INDEX_KEYS),
("primary_key", PRIMARY_KEY_KEYS),
("foreign_key", FOREIGN_KEY_KEYS),
("check", CHECK_KEYS),
];
const TABLE_ARG_KEYS: &[&str] = &["name", "strict", "without_rowid"];
fn is_marker(attr: &Attribute, name: &str) -> bool {
attr.path().is_ident(name)
}
fn validate_attrs(attrs: &[Attribute], marker_name: &str, allowed: &[&str]) -> syn::Result<()> {
for attr in attrs.iter().filter(|a| is_marker(a, marker_name)) {
attr.parse_nested_meta(|meta| {
let key = meta
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if !allowed.contains(&key.as_str()) {
return Err(syn::Error::new(
meta.path.span(),
format!(
"unknown `#[{marker_name}]` key `{key}`. Allowed: {}",
allowed.join(", ")
),
));
}
if meta.input.peek(syn::Token![=]) {
let _: syn::Expr = meta.value()?.parse()?;
}
Ok(())
})?;
}
Ok(())
}
fn parse_table_args(tokens: proc_macro2::TokenStream) -> syn::Result<()> {
let parser = syn::meta::parser(|meta| {
let key = meta
.path
.get_ident()
.map(|i| i.to_string())
.unwrap_or_default();
if !TABLE_ARG_KEYS.contains(&key.as_str()) {
return Err(syn::Error::new(
meta.path.span(),
format!(
"unknown `#[reef::table]` arg `{key}`. Allowed: {}",
TABLE_ARG_KEYS.join(", ")
),
));
}
if meta.input.peek(syn::Token![=]) {
let _: syn::Expr = meta.value()?.parse()?;
}
Ok(())
});
syn::parse::Parser::parse2(parser, tokens)
}