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) optional: bool,
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("optional") {
out.optional = true;
} 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`, `blank = <expr>`, or \
`optional`"
}
Derive::Record => {
"`embed`, `blank = <expr>`, `optional`, `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.optional, "`optional`"),
(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.embed {
return Ok(out);
}
let ty = last_segment(&field.ty);
if (out.blank.is_some() || out.optional) && ty.as_deref() == Some("Option") {
return Err(syn::Error::new_spanned(
&field.ty,
"an `Option` field is optional already: its blank answer is `None`",
));
}
if out.optional && out.blank.is_some() {
return Err(syn::Error::new_spanned(
field,
"`optional` and `blank` each declare the blank answer: declare one",
));
}
if out.optional && ty.as_deref() != Some("String") {
return Err(syn::Error::new_spanned(
&field.ty,
"`optional` reads an empty `String` as `\"\"`; another type has no empty value: \
declare `blank = <value>`, or make the field an `Option`",
));
}
Ok(out)
}
pub(crate) fn blank_answer(ty: &Type, attrs: &FormAttrs) -> Option<TokenStream2> {
if let Some(expr) = &attrs.blank {
return Some(quote_spanned! {ty.span()=>
::std::convert::Into::<#ty>::into(#expr)
});
}
let defaulted =
attrs.optional || matches!(last_segment(ty).as_deref(), Some("Option" | "bool"));
defaulted.then(|| {
quote_spanned! {ty.span()=>
<#ty as ::std::default::Default>::default()
}
})
}
pub(crate) fn blank_option(ty: &Type, attrs: &FormAttrs) -> TokenStream2 {
match blank_answer(ty, attrs) {
Some(blank) => quote::quote! { ::std::option::Option::Some(#blank) },
None => quote::quote! { ::std::option::Option::None },
}
}
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;