use syn::{FnArg, ItemFn, Result, ReturnType, Type};
pub fn validate_function_signature(func: &ItemFn) -> Result<()> {
if func.sig.receiver().is_some() {
return Err(syn::Error::new_spanned(
&func.sig,
"#[component] can only be applied to standalone functions, not methods",
));
}
validate_return_type(&func.sig.output)?;
for param in &func.sig.inputs {
validate_parameter(param)?;
}
Ok(())
}
fn validate_return_type(output: &ReturnType) -> Result<()> {
match output {
ReturnType::Default => {
Err(syn::Error::new_spanned(
output,
"#[component] function must return a value (not unit type)",
))
}
ReturnType::Type(_, ty) => {
if is_result_type(ty) {
return Ok(());
}
Ok(())
}
}
}
pub fn is_result_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Result";
}
}
false
}
fn validate_parameter(param: &FnArg) -> Result<()> {
match param {
FnArg::Receiver(_) => {
Err(syn::Error::new_spanned(
param,
"#[component] cannot be used on methods",
))
}
FnArg::Typed(pat_type) => {
let ty = &*pat_type.ty;
if !is_config_type(ty) && !is_component_type(ty) {
return Err(syn::Error::new_spanned(
ty,
"Parameter must be Config<T> or Component<T>",
));
}
Ok(())
}
}
}
pub fn is_config_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Config";
}
}
false
}
pub fn is_component_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Component";
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use syn::parse2;
#[test]
fn test_validate_standalone_function() {
let input = quote! {
fn create_component(Config(config): Config<MyConfig>) -> MyComponent {
MyComponent::new(config)
}
};
let func = parse2::<ItemFn>(input).unwrap();
assert!(validate_function_signature(&func).is_ok());
}
#[test]
fn test_reject_method() {
let input = quote! {
fn create_component(&self) -> MyComponent {
MyComponent
}
};
let func = parse2::<ItemFn>(input).unwrap();
assert!(validate_function_signature(&func).is_err());
}
#[test]
fn test_reject_invalid_parameter() {
let input = quote! {
fn create_component(invalid: String) -> MyComponent {
MyComponent
}
};
let func = parse2::<ItemFn>(input).unwrap();
assert!(validate_function_signature(&func).is_err());
}
#[test]
fn test_accept_result_return_type() {
let input = quote! {
fn create_component() -> Result<MyComponent, Error> {
Ok(MyComponent)
}
};
let func = parse2::<ItemFn>(input).unwrap();
assert!(validate_function_signature(&func).is_ok());
}
}