feat(derive): scope component handler checks
Allow #[slhx::component("name")] to validate only the generated handles for that hemplate component, so component modules can be checked independently without implementing unrelated handles.
req: component/003
req: component/005
This commit is contained in:
+28
-8
@@ -124,7 +124,12 @@ pub fn form(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
|
let component_name = if attr.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(parse_macro_input!(attr as LitStr).value())
|
||||||
|
};
|
||||||
let module = parse_macro_input!(item as ItemMod);
|
let module = parse_macro_input!(item as ItemMod);
|
||||||
let Some((_, items)) = &module.content else {
|
let Some((_, items)) = &module.content else {
|
||||||
return quote!(
|
return quote!(
|
||||||
@@ -139,7 +144,8 @@ pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
if !syms_path.exists() {
|
if !syms_path.exists() {
|
||||||
return quote!(#module).into();
|
return quote!(#module).into();
|
||||||
}
|
}
|
||||||
let missing = missing_component_handlers(&syms_path, items);
|
let component_filter = component_name.as_deref();
|
||||||
|
let missing = missing_component_handlers(&syms_path, component_filter, items);
|
||||||
if !missing.is_empty() {
|
if !missing.is_empty() {
|
||||||
let message = format!(
|
let message = format!(
|
||||||
"#[slhx::component] missing handler implementation(s): {}",
|
"#[slhx::component] missing handler implementation(s): {}",
|
||||||
@@ -495,9 +501,9 @@ fn handler_arg_names(function: &ItemFn) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn missing_component_handlers(path: &PathBuf, items: &[Item]) -> Vec<String> {
|
fn missing_component_handlers(path: &PathBuf, component: Option<&str>, items: &[Item]) -> Vec<String> {
|
||||||
let implemented = component_handler_names(items);
|
let implemented = component_handler_names(items);
|
||||||
syms_handles(path)
|
syms_handles(path, component)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|handle| !implemented.contains(handle))
|
.filter(|handle| !implemented.contains(handle))
|
||||||
.collect()
|
.collect()
|
||||||
@@ -522,7 +528,7 @@ fn has_handler_attr(function: &ItemFn) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn syms_handles(path: &PathBuf) -> Vec<String> {
|
fn syms_handles(path: &PathBuf, component: Option<&str>) -> Vec<String> {
|
||||||
let Ok(syms) = std::fs::read_to_string(path) else {
|
let Ok(syms) = std::fs::read_to_string(path) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
@@ -532,11 +538,24 @@ fn syms_handles(path: &PathBuf) -> Vec<String> {
|
|||||||
if !matches!(fields.next(), Some("handle")) {
|
if !matches!(fields.next(), Some("handle")) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
fields.nth(1).map(ToOwned::to_owned)
|
let symbol = fields.next()?;
|
||||||
|
if let Some(component) = component {
|
||||||
|
if symbol_component(symbol) != Some(component) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.next().map(ToOwned::to_owned)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn symbol_component(symbol: &str) -> Option<&str> {
|
||||||
|
let path = symbol.split_once("::")?.0;
|
||||||
|
path.rsplit_once('/')
|
||||||
|
.map_or(path, |(_, stem)| stem)
|
||||||
|
.strip_suffix(".heml")
|
||||||
|
}
|
||||||
|
|
||||||
fn compile_error(message: &str) -> TokenStream {
|
fn compile_error(message: &str) -> TokenStream {
|
||||||
format!("compile_error!({message:?});")
|
format!("compile_error!({message:?});")
|
||||||
.parse()
|
.parse()
|
||||||
@@ -632,7 +651,7 @@ mod tests {
|
|||||||
let path = std::env::temp_dir().join("slhx-derive-component-test.syms");
|
let path = std::env::temp_dir().join("slhx-derive-component-test.syms");
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
&path,
|
&path,
|
||||||
"slhx-syms-v1\nhandle\ttemplates/a.heml::create\tcreate\t1\nhandle\ttemplates/a.heml::delete\tdelete\t2\n",
|
"slhx-syms-v1\nhandle\ttemplates/a.heml::create\tcreate\t1\nhandle\ttemplates/a.heml::delete\tdelete\t2\nhandle\ttemplates/other.heml::archive\tarchive\t3\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let module: syn::ItemMod = parse_quote! {
|
let module: syn::ItemMod = parse_quote! {
|
||||||
@@ -644,7 +663,8 @@ mod tests {
|
|||||||
let (_, items) = module.content.expect("inline module");
|
let (_, items) = module.content.expect("inline module");
|
||||||
|
|
||||||
assert_eq!(component_handler_names(&items), vec!["create"]);
|
assert_eq!(component_handler_names(&items), vec!["create"]);
|
||||||
assert_eq!(missing_component_handlers(&path, &items), vec!["delete"]);
|
assert_eq!(missing_component_handlers(&path, None, &items), vec!["delete", "archive"]);
|
||||||
|
assert_eq!(missing_component_handlers(&path, Some("a"), &items), vec!["delete"]);
|
||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,60 @@ mod todos {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn component_macro_can_validate_one_generated_component() {
|
||||||
|
// req: component/003 req: component/005 req: test/003
|
||||||
|
let fixture = Fixture::new("slhx-derive-component-scoped-pass");
|
||||||
|
fixture.write(
|
||||||
|
"Cargo.toml",
|
||||||
|
&format!(
|
||||||
|
r#"[package]
|
||||||
|
name = "slhx-derive-component-scoped-pass"
|
||||||
|
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/todos.heml::create\tcreate\t1\nhandle\ttemplates/admin.heml::delete\tdelete\t2\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
fixture.write(
|
||||||
|
"src/lib.rs",
|
||||||
|
r#"#[slhx::component("todos")]
|
||||||
|
mod todos {
|
||||||
|
#[slhx::handler]
|
||||||
|
fn create() -> impl slhx::IntoEffect {
|
||||||
|
slhx::event("created", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = check_fixture(&fixture);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"fixture failed to compile:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handler_macro_reports_unknown_handle_and_bad_shape() {
|
fn handler_macro_reports_unknown_handle_and_bad_shape() {
|
||||||
// req: derive_handler/001 req: test/003
|
// req: derive_handler/001 req: test/003
|
||||||
|
|||||||
Reference in New Issue
Block a user