feat(derive): reject empty handlers
req: derive_handler/001
This commit is contained in:
+58
-11
@@ -1,7 +1,7 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use std::path::PathBuf;
|
||||
use syn::{parse_macro_input, ItemFn};
|
||||
use syn::{parse_macro_input, FnArg, ItemFn, ReturnType, Type};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
@@ -9,15 +9,27 @@ pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let name = function.sig.ident.to_string();
|
||||
|
||||
if let Some(syms_path) = syms_path() {
|
||||
if syms_path.exists() && !syms_contains_handle(&syms_path, &name) {
|
||||
let message = format!(
|
||||
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
|
||||
);
|
||||
return quote!(
|
||||
#function
|
||||
compile_error!(#message);
|
||||
)
|
||||
.into();
|
||||
if syms_path.exists() {
|
||||
if !syms_contains_handle(&syms_path, &name) {
|
||||
let message = format!(
|
||||
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
|
||||
);
|
||||
return quote!(
|
||||
#function
|
||||
compile_error!(#message);
|
||||
)
|
||||
.into();
|
||||
}
|
||||
if !has_form_param(&function) && !has_non_unit_return(&function) {
|
||||
let message = format!(
|
||||
"slhx handler `{name}` must accept a form/context parameter or return a value implementing IntoEffect"
|
||||
);
|
||||
return quote!(
|
||||
#function
|
||||
compile_error!(#message);
|
||||
)
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +84,27 @@ fn generated_path(file: &str) -> Option<PathBuf> {
|
||||
std::env::var_os("OUT_DIR").map(|out_dir| PathBuf::from(out_dir).join(file))
|
||||
}
|
||||
|
||||
fn has_form_param(function: &ItemFn) -> bool {
|
||||
function.sig.inputs.iter().any(|arg| match arg {
|
||||
FnArg::Typed(arg) => type_ends_with(&arg.ty, "Form"),
|
||||
FnArg::Receiver(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
fn has_non_unit_return(function: &ItemFn) -> bool {
|
||||
match &function.sig.output {
|
||||
ReturnType::Default => false,
|
||||
ReturnType::Type(_, ty) => !matches!(ty.as_ref(), Type::Tuple(tuple) if tuple.elems.is_empty()),
|
||||
}
|
||||
}
|
||||
|
||||
fn type_ends_with(ty: &Type, ident: &str) -> bool {
|
||||
match ty {
|
||||
Type::Path(path) => path.path.segments.last().is_some_and(|segment| segment.ident == ident),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn syms_contains_handle(path: &PathBuf, ident: &str) -> bool {
|
||||
let Ok(syms) = std::fs::read_to_string(path) else {
|
||||
return true;
|
||||
@@ -91,7 +124,8 @@ fn compile_error(message: &str) -> TokenStream {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::syms_contains_handle;
|
||||
use super::{has_form_param, has_non_unit_return, syms_contains_handle};
|
||||
use syn::parse_quote;
|
||||
|
||||
#[test]
|
||||
fn syms_lookup_matches_handle_ident() {
|
||||
@@ -107,4 +141,17 @@ mod tests {
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_shape_accepts_form_or_effect_return() {
|
||||
// req: derive_handler/001
|
||||
let with_form = parse_quote!(fn save(form: slhx::Form<String>) {});
|
||||
let with_return = parse_quote!(fn ping() -> impl slhx::IntoEffect { slhx::EffectBatch::default() });
|
||||
let empty = parse_quote!(fn noop() {});
|
||||
|
||||
assert!(has_form_param(&with_form));
|
||||
assert!(has_non_unit_return(&with_return));
|
||||
assert!(!has_form_param(&empty));
|
||||
assert!(!has_non_unit_return(&empty));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user