feat(derive): require form parameter for form handles

Emit handle-form facts from slhx-build and have #[slhx::handler] reject generated form handlers that do not accept slhx::Form<_>. Cover the boundary with compile-fail fixtures.

req: form/004

req: form/006

req: test/003
This commit is contained in:
slhx agent
2026-05-25 21:23:37 +02:00
parent 3ea596a218
commit dca26511cc
3 changed files with 81 additions and 2 deletions
+25 -2
View File
@@ -30,6 +30,16 @@ pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
)
.into();
}
if handle_requires_form(&syms_path, &name) && !has_form_param(&function) {
let message = format!(
"slhx handler `{name}` handles a generated form and must accept slhx::Form<_>"
);
return quote!(
#function
compile_error!(#message);
)
.into();
}
}
}
@@ -125,6 +135,17 @@ fn syms_contains_handle(path: &PathBuf, ident: &str) -> bool {
})
}
fn handle_requires_form(path: &PathBuf, ident: &str) -> bool {
let Ok(syms) = std::fs::read_to_string(path) else {
return false;
};
syms.lines().any(|line| {
let mut fields = line.split('\t');
matches!(fields.next(), Some("handle_form"))
&& fields.next().is_some_and(|handle_ident| handle_ident == ident)
})
}
fn compile_error(message: &str) -> TokenStream {
format!("compile_error!({message:?});")
.parse()
@@ -133,7 +154,7 @@ fn compile_error(message: &str) -> TokenStream {
#[cfg(test)]
mod tests {
use super::{has_form_param, has_non_unit_return, syms_contains_handle};
use super::{handle_requires_form, has_form_param, has_non_unit_return, syms_contains_handle};
use syn::parse_quote;
#[test]
@@ -141,12 +162,14 @@ mod tests {
let path = std::env::temp_dir().join("slhx-derive-syms-test.syms");
std::fs::write(
&path,
"slhx-syms-v1\nslot\ttemplates/a.heml::count\tcount\t1\nhandle\ttemplates/a.heml::create\tcreate\t2\n",
"slhx-syms-v1\nslot\ttemplates/a.heml::count\tcount\t1\nhandle\ttemplates/a.heml::create\tcreate\t2\nhandle_form\tcreate\tnew_todo\n",
)
.unwrap();
assert!(syms_contains_handle(&path, "create"));
assert!(!syms_contains_handle(&path, "missing"));
assert!(handle_requires_form(&path, "create"));
assert!(!handle_requires_form(&path, "missing"));
let _ = std::fs::remove_file(path);
}