use proc_macro2::TokenStream as TokenStream2;
use quote::quote_spanned;
use syn::{Type, spanned::Spanned};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Derive {
Embedded,
Record,
}
#[derive(Default)]
pub(crate) struct FormAttrs {
pub(crate) embed: bool,
pub(crate) label: Option<String>,
pub(crate) multiline: Option<u32>,
pub(crate) blank: Option<syn::Expr>,
pub(crate) options: Option<syn::Path>,
pub(crate) choice: bool,
pub(crate) file: bool,
}
pub(crate) fn form_attrs(field: &syn::Field, derive: Derive) -> syn::Result<FormAttrs> {
let mut out = FormAttrs::default();
for attr in &field.attrs {
if !attr.path().is_ident("form") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("embed") {
out.embed = true;
} else if meta.path.is_ident("label") && derive == Derive::Embedded {
let text: syn::LitStr = meta.value()?.parse()?;
out.label = Some(text.value());
} else if meta.path.is_ident("multiline") && derive == Derive::Embedded {
let rows: syn::LitInt = meta.value()?.parse()?;
out.multiline = Some(rows.base10_parse()?);
} else if meta.path.is_ident("blank") {
out.blank = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("options") && derive == Derive::Record {
out.options = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("choice") && derive == Derive::Record {
out.choice = true;
} else if meta.path.is_ident("file") && derive == Derive::Record {
out.file = true;
} else {
let expected = match derive {
Derive::Embedded => {
"`embed`, `label = \"…\"`, `multiline = N`, or `blank = <expr>`"
}
Derive::Record => {
"`embed`, `blank = <expr>`, `options = <Type>`, `choice`, or `file`"
}
};
return Err(meta.error(format!("unknown `#[form(..)]` key: expected {expected}")));
}
Ok(())
})?;
}
let controls = [
(out.options.is_some(), "`options`"),
(out.choice, "`choice`"),
(out.file, "`file`"),
];
let chosen: Vec<&str> = controls
.iter()
.filter(|(set, _)| *set)
.map(|(_, key)| *key)
.collect();
if chosen.len() > 1 {
return Err(syn::Error::new_spanned(
field,
format!(
"{} each pick the field's control: declare one",
chosen.join(" and ")
),
));
}
if out.embed {
let misplaced = [
(out.label.is_some(), "`label`"),
(out.multiline.is_some(), "`multiline`"),
(out.blank.is_some(), "`blank`"),
(out.options.is_some(), "`options`"),
(out.choice, "`choice`"),
(out.file, "`file`"),
];
if let Some((_, key)) = misplaced.iter().find(|(set, _)| *set) {
return Err(syn::Error::new_spanned(
field,
format!(
"{key} does not apply to an embedded value: its own fields declare their \
controls and their blank answers"
),
));
}
}
if out.blank.is_some() && last_segment(&field.ty).is_some_and(|name| name == "Option") {
return Err(syn::Error::new_spanned(
&field.ty,
"an `Option` field's blank answer is `None`",
));
}
Ok(out)
}
pub(crate) fn assert_scalar(krate: &TokenStream2, ty: &Type) -> TokenStream2 {
quote_spanned! {ty.span()=>
#krate::__macro::assert_form_scalar::<#ty>();
}
}
pub(crate) fn last_segment(ty: &Type) -> Option<String> {
match ty {
Type::Path(path) => path.path.segments.last().map(|s| s.ident.to_string()),
_ => None,
}
}
#[cfg(test)]
mod tests;