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 proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use syn::{parse_macro_input, ItemFn};
|
use syn::{parse_macro_input, FnArg, ItemFn, ReturnType, Type};
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn handler(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
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();
|
let name = function.sig.ident.to_string();
|
||||||
|
|
||||||
if let Some(syms_path) = syms_path() {
|
if let Some(syms_path) = syms_path() {
|
||||||
if syms_path.exists() && !syms_contains_handle(&syms_path, &name) {
|
if syms_path.exists() {
|
||||||
let message = format!(
|
if !syms_contains_handle(&syms_path, &name) {
|
||||||
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
|
let message = format!(
|
||||||
);
|
"unknown slhx handle `{name}`; add `data-slhx-handle=\"{name}\"` to a template or rename this handler"
|
||||||
return quote!(
|
);
|
||||||
#function
|
return quote!(
|
||||||
compile_error!(#message);
|
#function
|
||||||
)
|
compile_error!(#message);
|
||||||
.into();
|
)
|
||||||
|
.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))
|
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 {
|
fn syms_contains_handle(path: &PathBuf, ident: &str) -> bool {
|
||||||
let Ok(syms) = std::fs::read_to_string(path) else {
|
let Ok(syms) = std::fs::read_to_string(path) else {
|
||||||
return true;
|
return true;
|
||||||
@@ -91,7 +124,8 @@ fn compile_error(message: &str) -> TokenStream {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::syms_contains_handle;
|
use super::{has_form_param, has_non_unit_return, syms_contains_handle};
|
||||||
|
use syn::parse_quote;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn syms_lookup_matches_handle_ident() {
|
fn syms_lookup_matches_handle_ident() {
|
||||||
@@ -107,4 +141,17 @@ mod tests {
|
|||||||
|
|
||||||
let _ = std::fs::remove_file(path);
|
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