feat(derive): check form parser availability
Introduce FormValue as the parser-availability bound for #[slhx::form] models, blanket it for FromStr types, and add compile-fail coverage for a form field whose domain type lacks a parser. req: form/004 req: form/006
This commit is contained in:
+57
-2
@@ -91,7 +91,11 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let errors = form_contract_errors(&form_name, &form_struct);
|
||||
if errors.is_empty() {
|
||||
let ident = &form_struct.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = form_struct.generics.split_for_impl();
|
||||
let mut generics = form_struct.generics.clone();
|
||||
for ty in form_parser_types(&form_struct) {
|
||||
generics.make_where_clause().predicates.push(parse_quote!(#ty: ::slhx::FormValue));
|
||||
}
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
quote!(
|
||||
#form_struct
|
||||
impl #impl_generics ::slhx::FormModel for #ident #ty_generics #where_clause {}
|
||||
@@ -342,6 +346,40 @@ fn form_contract_errors(form_name: &str, form_struct: &ItemStruct) -> Vec<String
|
||||
errors
|
||||
}
|
||||
|
||||
fn form_parser_types(form_struct: &ItemStruct) -> Vec<Type> {
|
||||
let Fields::Named(fields) = &form_struct.fields else {
|
||||
return Vec::new();
|
||||
};
|
||||
fields
|
||||
.named
|
||||
.iter()
|
||||
.map(|field| parser_type(&field.ty).clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parser_type(ty: &Type) -> &Type {
|
||||
generic_inner_type(ty, "Option")
|
||||
.or_else(|| generic_inner_type(ty, "Vec"))
|
||||
.unwrap_or(ty)
|
||||
}
|
||||
|
||||
fn generic_inner_type<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> {
|
||||
let Type::Path(path) = ty else {
|
||||
return None;
|
||||
};
|
||||
let segment = path.path.segments.last()?;
|
||||
if segment.ident != name {
|
||||
return None;
|
||||
}
|
||||
let PathArguments::AngleBracketed(args) = &segment.arguments else {
|
||||
return None;
|
||||
};
|
||||
args.args.iter().find_map(|arg| match arg {
|
||||
GenericArgument::Type(ty) => Some(ty),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn form_fields(path: &PathBuf, form_name: &str) -> Vec<GeneratedFormField> {
|
||||
let Ok(syms) = std::fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
@@ -484,7 +522,8 @@ fn compile_error(message: &str) -> TokenStream {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{component_handler_names, handle_params, handle_requires_form, has_form_param, has_non_unit_return, is_form_type, missing_component_handlers, missing_handle_params, syms_contains_handle};
|
||||
use super::{component_handler_names, handle_params, handle_requires_form, has_form_param, has_non_unit_return, is_form_type, missing_component_handlers, missing_handle_params, parser_type, syms_contains_handle};
|
||||
use quote::quote;
|
||||
use syn::{parse_quote, ItemFn, Type};
|
||||
|
||||
#[test]
|
||||
@@ -519,6 +558,22 @@ mod tests {
|
||||
assert!(!has_non_unit_return(&empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_parser_type_uses_option_and_vec_inner_types() {
|
||||
// req: form/004
|
||||
let required: Type = parse_quote!(Email);
|
||||
let optional: Type = parse_quote!(Option<Email>);
|
||||
let multiple: Type = parse_quote!(Vec<Email>);
|
||||
|
||||
let required_parser = parser_type(&required);
|
||||
let optional_parser = parser_type(&optional);
|
||||
let multiple_parser = parser_type(&multiple);
|
||||
|
||||
assert_eq!(quote!(#required_parser).to_string(), quote!(#required).to_string());
|
||||
assert_eq!(quote!(#optional_parser).to_string(), quote!(#required).to_string());
|
||||
assert_eq!(quote!(#multiple_parser).to_string(), quote!(#required).to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_param_matches_form_type_not_name_suffix() {
|
||||
// req: form/004 req: form/006
|
||||
|
||||
Reference in New Issue
Block a user