use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use syn::parenthesized;
use syn::DeriveInput;
use syn::Field;
use syn::LitInt;
use syn::LitStr;
use syn::Type;
use syn::Visibility;
pub fn derive_form(input: DeriveInput) -> TokenStream {
let fields = match input.data {
syn::Data::Struct(strukt) => match strukt.fields {
syn::Fields::Named(named) => named,
syn::Fields::Unnamed(_) => panic!("cannot derive Form for a tuple struct"),
syn::Fields::Unit => panic!("cannot derive Form for a unit struct"),
},
syn::Data::Enum(_) => panic!("cannot derive Form for an enum"),
syn::Data::Union(_) => panic!("cannot derive Form for a union"),
};
let mut fields_vec = Vec::new();
for pair in fields.named.into_pairs() {
let field = pair.into_value();
let f_info = parse_field_attrs(&field);
fields_vec.push((field, f_info));
}
let partial_ident = format_ident!("{}Raw", input.ident);
let validation_ident = format_ident!("{}Validation", input.ident);
let partial_struct = write_form_partial_struct(&partial_ident, &fields_vec, &input.vis);
let validation_struct =
write_form_validation_struct(&partial_ident, &validation_ident, &fields_vec, &input.vis);
let partial_impl =
write_form_partial_impl(&input.ident, &partial_ident, &validation_ident, &fields_vec);
quote!(
#partial_struct
#validation_struct
#partial_impl
)
}
fn identify_base_type(unprefixed: &Ident) -> FieldType {
match unprefixed.to_string().as_str() {
"u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "u64" | "i64" | "u128" | "i128" | "f32"
| "f64" => FieldType::Numeric,
"String" => FieldType::String,
"bool" => FieldType::Boolean,
other => {
panic!("unsupported field type: `{other}`")
}
}
}
fn identify_type(ty: &Type) -> (FieldType, bool) {
let (ftype, needed) = match ty {
syn::Type::Array(_) => panic!("unsupported field type: array"),
syn::Type::BareFn(_) => panic!("unsupported field type: bare fn"),
syn::Type::Group(_) => panic!("unsupported field type: Group"),
syn::Type::ImplTrait(_) => panic!("unsupported field type: impl Trait"),
syn::Type::Infer(_) => panic!("unsupported field type: Infer"),
syn::Type::Macro(_) => panic!("unsupported field type: macro"),
syn::Type::Never(_) => panic!("unsupported field type: Never"),
syn::Type::Paren(_) => panic!("unsupported field type: parenthesised"),
syn::Type::Path(type_path) => {
let path = &type_path.path;
if path.segments.len() != 1 {
panic!("unsupported field type syntax (can't deal with multiple path segments)");
}
let segment = path.segments.get(0).unwrap();
if segment.ident == "Option" {
match &segment.arguments {
syn::PathArguments::None => {
panic!("unsupported field type: Option without args")
}
syn::PathArguments::AngleBracketed(bracks) => {
if bracks.args.len() != 1 {
panic!("unsupported field type: Option with other than 1 arg");
}
let arg = bracks.args.get(0).unwrap();
match arg {
syn::GenericArgument::Lifetime(_) => {
panic!("unsupported field type with lifetime")
}
syn::GenericArgument::Type(ty) => {
let (base_type, check_needed) = identify_type(ty);
if base_type == FieldType::Boolean {
panic!(
"Option<bool> isn't supported (does it even make sense?)"
);
}
if !check_needed {
panic!("unsupported field type with nested Options or something like that");
}
(base_type, false)
}
syn::GenericArgument::Const(_) => {
panic!("unsupported field type with const")
}
syn::GenericArgument::AssocType(_) => {
panic!("unsupported field type with associated type")
}
syn::GenericArgument::AssocConst(_) => {
panic!("unsupported field type with associated const")
}
syn::GenericArgument::Constraint(_) => {
panic!("unsupported field type with constraint")
}
_ => panic!("other generic argument unsupported?"),
}
}
syn::PathArguments::Parenthesized(_) => {
panic!("unsupported field type with ( )s")
}
}
} else {
match &segment.arguments {
syn::PathArguments::None => {
let base_type = identify_base_type(&segment.ident);
let needed = base_type != FieldType::Boolean;
(base_type, needed)
}
syn::PathArguments::AngleBracketed(_) => {
panic!("unsupported field type: non-Option with args")
}
syn::PathArguments::Parenthesized(_) => {
panic!("unsupported field type with ( )s")
}
}
}
}
syn::Type::Ptr(_) => panic!("unsupported field type: pointer"),
syn::Type::Reference(_) => panic!("unsupported field type: ref"),
syn::Type::Slice(_) => panic!("unsupported field type: slice"),
syn::Type::TraitObject(_) => panic!("unsupported field type: trait object"),
syn::Type::Tuple(_) => panic!("unsupported field type: tuple"),
syn::Type::Verbatim(_) => panic!("verbatim?"),
_ => panic!("unsupported (unknown) field type"),
};
(ftype, needed)
}
fn parse_field_attrs(f: &Field) -> FieldInfo {
let (ftype, needed) = identify_type(&f.ty);
let mut out = FieldInfo {
ftype,
needed,
need_nonempty: true,
min_chars: None,
max_chars: None,
email: false,
regex: None,
};
for attr in &f.attrs {
if !attr.meta.path().is_ident("form") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("allow_empty") {
out.need_nonempty = false;
return Ok(());
}
if meta.path.is_ident("email") {
if out.email {
panic!("duplicate email annotation");
}
out.email = true;
return Ok(());
}
if meta.path.is_ident("min_chars") {
let content;
parenthesized!(content in meta.input);
let lit: LitInt = content.parse()?;
let n: u32 = lit.base10_parse()?;
if out.min_chars.is_some() {
panic!("duplicate min_chars annotation");
}
out.min_chars = Some(n);
return Ok(());
}
if meta.path.is_ident("max_chars") {
let content;
parenthesized!(content in meta.input);
let lit: LitInt = content.parse()?;
let n: u32 = lit.base10_parse()?;
if out.max_chars.is_some() {
panic!("duplicate max_chars annotation");
}
out.max_chars = Some(n);
return Ok(());
}
if meta.path.is_ident("regex") {
let content;
parenthesized!(content in meta.input);
let lit: LitStr = content.parse()?;
if out.regex.is_some() {
panic!("duplicate regex annotation");
}
out.regex = Some(lit.value());
return Ok(());
}
panic!(
"unrecognised field attribute: {}",
meta.path
.get_ident()
.map(|i| format!("`{i}`"))
.unwrap_or_else(|| "<???>".to_owned())
)
})
.unwrap();
}
out
}
fn write_form_partial_struct(
partial_ident: &Ident,
fields: &[(Field, FieldInfo)],
vis: &Visibility,
) -> TokenStream {
let field_idents: Vec<&Ident> = fields
.iter()
.map(|(f, _)| f.ident.as_ref().unwrap())
.collect();
quote!(
#[allow(missing_docs)]
#[derive(Default, Debug, ::bevy_reflect::Reflect, ::serde::Deserialize)]
#vis struct #partial_ident {
#(#field_idents: Option<String>),*
}
)
}
fn write_form_validation_struct(
partial_ident: &Ident,
validation_ident: &Ident,
fields: &[(Field, FieldInfo)],
vis: &Visibility,
) -> TokenStream {
let field_idents: Vec<&Ident> = fields
.iter()
.map(|(f, _)| f.ident.as_ref().unwrap())
.collect();
quote!(
#[allow(missing_docs)]
#[derive(Default, Debug, ::bevy_reflect::Reflect)]
#vis struct #validation_ident {
form_wide: ::formbeam::FieldErrors,
#(#field_idents: ::formbeam::FieldErrors),*
}
impl ::formbeam::traits::FormValidation for #validation_ident {
type Partial = #partial_ident;
fn is_valid(&self) -> bool {
self.form_wide.is_empty()
#(&& self.#field_idents.is_empty())*
}
}
)
}
fn write_form_partial_impl(
form_ident: &Ident,
partial_ident: &Ident,
validation_ident: &Ident,
fields: &[(Field, FieldInfo)],
) -> TokenStream {
let validate_body = write_partial_impl_validate(validation_ident, fields);
let form_method_body = write_partial_impl_form_method(form_ident, fields);
let form_info = write_form_info(fields);
quote!(
impl ::formbeam::Form for #form_ident {
type Partial = #partial_ident;
}
#[::async_trait::async_trait]
impl ::formbeam::FormPartial for #partial_ident {
type Form = #form_ident;
type Validation = #validation_ident;
type Error = ::core::convert::Infallible;
const INFO: &'static ::formbeam::FormPartialInfo = &#form_info;
async fn validate(&self) -> Result<Self::Validation, Self::Error> {
use ::formbeam::FieldValidator;
#validate_body
}
fn form(&self) -> Result<Self::Form, &'static str> {
#form_method_body
}
}
)
}
fn write_partial_impl_validate(
validation_ident: &Ident,
fields: &[(Field, FieldInfo)],
) -> TokenStream {
let field_statements = fields.iter().map(write_validate_statement);
quote!(
let mut errors = #validation_ident::default();
#(#field_statements)*
Ok(errors)
)
}
struct FieldInfo {
ftype: FieldType,
needed: bool,
need_nonempty: bool,
min_chars: Option<u32>,
max_chars: Option<u32>,
email: bool,
regex: Option<String>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum FieldType {
Numeric,
String,
Boolean,
}
fn write_form_info(fields: &[(Field, FieldInfo)]) -> TokenStream {
let form_validators: Vec<TokenStream> = Vec::new();
let fields: Vec<TokenStream> = fields
.iter()
.map(|(f, fi)| write_form_info_for_field(f, fi))
.collect();
quote!(
::formbeam::FormPartialInfo {
form_validators: &[
#(#form_validators),*
],
fields: &[
#(#fields),*
]
}
)
}
fn write_form_info_for_field(field: &Field, f_info: &FieldInfo) -> TokenStream {
let name = field.ident.as_ref().unwrap().to_string();
let mut validators = Vec::new();
if f_info.needed && f_info.need_nonempty {
validators.push(quote!(::formbeam::FieldValidatorInfo::Required));
}
if let Some(l) = f_info.min_chars {
validators.push(quote!(::formbeam::FieldValidatorInfo::MinLength(#l)));
}
if let Some(l) = f_info.max_chars {
validators.push(quote!(::formbeam::FieldValidatorInfo::MaxLength(#l)));
}
if f_info.email {
validators.push(quote!(::formbeam::FieldValidatorInfo::Email));
}
if let Some(r) = &f_info.regex {
validators.push(quote!(::formbeam::FieldValidatorInfo::Regex(#r)));
}
quote!(
::formbeam::FieldInfo {
name: #name,
validators: &[#(#validators),*]
}
)
}
fn write_validate_statement((field, f_info): &(Field, FieldInfo)) -> TokenStream {
let f = field.ident.as_ref().unwrap();
let configured_validators = write_validators(field, f_info);
let check_nonempty = if f_info.need_nonempty {
quote!(
if field.is_empty() {
errors.#f.push(::formbeam::FieldError::Missing);
} else {
#(#configured_validators)*
}
)
} else {
quote!(#(#configured_validators)*)
};
let else_not_present = if f_info.needed {
quote!(
errors.#f.push(::formbeam::FieldError::Missing);
)
} else {
TokenStream::new()
};
quote!(
if let Some(field) = &self.#f {
#check_nonempty
} else {
#else_not_present
}
)
}
fn write_validators(field: &Field, f_info: &FieldInfo) -> Vec<TokenStream> {
let f = field.ident.as_ref().unwrap();
let mut out = Vec::new();
if f_info.min_chars.is_some() || f_info.max_chars.is_some() {
let effective_min = f_info.min_chars.unwrap_or(0);
let effective_max = f_info.max_chars.unwrap_or(u32::MAX);
out.push(quote!(
::formbeam::validators::LengthInChars { min_length: #effective_min, max_length: #effective_max }.validate(&field, &mut errors.#f, &mut ()).await?;
));
}
if let Some(regex) = &f_info.regex {
let regex_ident = format_ident!("REGEX_{}", f.to_string().to_uppercase());
out.push(quote!(
static #regex_ident: ::std::sync::LazyLock<::formbeam::validators::Regex> = ::std::sync::LazyLock::new(|| {
::formbeam::validators::Regex::new(#regex).expect("invalid regex")
});
#regex_ident.validate(&field, &mut errors.#f, &mut ()).await?;
));
}
if f_info.email {
out.push(quote!(
::formbeam::validators::Email.validate(&field, &mut errors.#f, &mut ()).await?;
));
}
out
}
fn write_partial_impl_form_method(
form_ident: &Ident,
fields: &[(Field, FieldInfo)],
) -> TokenStream {
let field_idents: Vec<&Ident> = fields
.iter()
.map(|(f, _)| f.ident.as_ref().unwrap())
.collect();
let mut statements = Vec::new();
for (field, field_info) in fields.iter() {
let f = field.ident.as_ref().unwrap();
let none_case = if field_info.needed {
let f_name = f.to_string();
quote!(return Err(#f_name);)
} else if field_info.ftype == FieldType::Boolean {
quote!(false)
} else {
quote!(None)
};
let some_case = {
let converter = converter_from_raw_to_field(f, field_info);
if field_info.needed || field_info.ftype == FieldType::Boolean {
converter
} else if field_info.need_nonempty {
quote!(
if raw.is_empty() {
None
} else {
Some({#converter})
}
)
} else {
quote!(Some({#converter}))
}
};
statements.push(quote!(
let #f = match &self.#f {
Some(raw) => {
#some_case
},
None => {
#none_case
}
};
))
}
quote!(
#(#statements)*
Ok(#form_ident { #(#field_idents),* })
)
}
fn converter_from_raw_to_field(f: &Ident, field_info: &FieldInfo) -> TokenStream {
let f_name = f.to_string();
match field_info.ftype {
FieldType::Numeric => quote!(
raw.parse().map_err(|_| #f_name)?
),
FieldType::String => quote!(raw.to_owned()),
FieldType::Boolean => quote!(!raw.is_empty()),
}
}