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:
@@ -515,6 +515,9 @@ fn __slhx_attr(tag: &str, attr: &str) -> Option<::std::string::String> {
|
|||||||
let res = &form.resource;
|
let res = &form.resource;
|
||||||
out.push_str(&format!("form\t{}\t{}\t{}\n", res.symbol, res.ident, res.id));
|
out.push_str(&format!("form\t{}\t{}\t{}\n", res.symbol, res.ident, res.id));
|
||||||
}
|
}
|
||||||
|
for (handle_ident, form_ident) in &self.handle_forms {
|
||||||
|
out.push_str(&format!("handle_form\t{handle_ident}\t{form_ident}\n"));
|
||||||
|
}
|
||||||
for res in self.atoms.values() {
|
for res in self.atoms.values() {
|
||||||
out.push_str(&format!("atom\t{}\t{}\t{}\n", res.symbol, res.ident, res.id));
|
out.push_str(&format!("atom\t{}\t{}\t{}\n", res.symbol, res.ident, res.id));
|
||||||
}
|
}
|
||||||
@@ -914,6 +917,7 @@ mod tests {
|
|||||||
let syms = std::fs::read_to_string(out.join("slhx.syms")).unwrap();
|
let syms = std::fs::read_to_string(out.join("slhx.syms")).unwrap();
|
||||||
assert!(syms.contains("atom\t"));
|
assert!(syms.contains("atom\t"));
|
||||||
assert!(syms.contains("\tfilter\t"));
|
assert!(syms.contains("\tfilter\t"));
|
||||||
|
assert!(syms.contains("handle_form\tcreate\tnew_todo\n"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&base);
|
let _ = std::fs::remove_dir_all(&base);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-2
@@ -30,6 +30,16 @@ pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
)
|
)
|
||||||
.into();
|
.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 {
|
fn compile_error(message: &str) -> TokenStream {
|
||||||
format!("compile_error!({message:?});")
|
format!("compile_error!({message:?});")
|
||||||
.parse()
|
.parse()
|
||||||
@@ -133,7 +154,7 @@ fn compile_error(message: &str) -> TokenStream {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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;
|
use syn::parse_quote;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -141,12 +162,14 @@ mod tests {
|
|||||||
let path = std::env::temp_dir().join("slhx-derive-syms-test.syms");
|
let path = std::env::temp_dir().join("slhx-derive-syms-test.syms");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
&path,
|
&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();
|
.unwrap();
|
||||||
|
|
||||||
assert!(syms_contains_handle(&path, "create"));
|
assert!(syms_contains_handle(&path, "create"));
|
||||||
assert!(!syms_contains_handle(&path, "missing"));
|
assert!(!syms_contains_handle(&path, "missing"));
|
||||||
|
assert!(handle_requires_form(&path, "create"));
|
||||||
|
assert!(!handle_requires_form(&path, "missing"));
|
||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,58 @@ fn known() {}
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_handle_requires_form_parameter() {
|
||||||
|
// req: form/004 req: form/006 req: test/003
|
||||||
|
let fixture = Fixture::new("slhx-derive-form-handler-fail");
|
||||||
|
fixture.write(
|
||||||
|
"Cargo.toml",
|
||||||
|
&format!(
|
||||||
|
r#"[package]
|
||||||
|
name = "slhx-derive-form-handler-fail"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
slhx = {{ path = {:?} }}
|
||||||
|
"#,
|
||||||
|
repo_path("slhx")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
fixture.write(
|
||||||
|
"build.rs",
|
||||||
|
r#"fn main() {
|
||||||
|
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||||
|
std::fs::write(
|
||||||
|
out.join("slhx.syms"),
|
||||||
|
"slhx-syms-v1\nhandle\ttemplates/app.heml::create\tcreate\t1\nhandle_form\tcreate\tnew_todo\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
fixture.write(
|
||||||
|
"src/lib.rs",
|
||||||
|
r#"#[slhx::handler]
|
||||||
|
fn create() -> impl slhx::IntoEffect {
|
||||||
|
slhx::EffectBatch::default()
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = check_fixture(&fixture);
|
||||||
|
|
||||||
|
assert!(!output.status.success(), "fixture unexpectedly compiled");
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("slhx handler `create` handles a generated form and must accept slhx::Form<_>"),
|
||||||
|
"missing form-handler diagnostic in stderr:\n{stderr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn surface_macro_reports_missing_generated_include() {
|
fn surface_macro_reports_missing_generated_include() {
|
||||||
// req: build/004 req: test/003
|
// req: build/004 req: test/003
|
||||||
|
|||||||
Reference in New Issue
Block a user