vespera_macro 0.3.1

Procedural macros for Vespera - automatic route discovery and OpenAPI generation
Documentation
use std::collections::{HashMap, HashSet};

use syn::Type;
use vespera_core::route::{Parameter, ParameterLocation};

use crate::parser::schema::parse_type_to_schema_ref;

pub(super) fn parse_path_extractor(
    ty: &Type,
    path_params: &[String],
    known_schemas: &HashSet<&str>,
    struct_definitions: &HashMap<&str, &str>,
) -> Option<Vec<Parameter>> {
    let Type::Path(type_path) = ty else {
        return None;
    };
    let segment = type_path.path.segments.last()?;
    if segment.ident != "Path" {
        return None;
    }
    let syn::PathArguments::AngleBracketed(args) = &segment.arguments else {
        return None;
    };
    let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() else {
        return None;
    };

    if let Type::Tuple(tuple) = inner_ty {
        let parameters = tuple
            .elems
            .iter()
            .enumerate()
            .filter_map(|(idx, elem_ty)| {
                path_params.get(idx).map(|param_name| Parameter {
                    name: param_name.clone(),
                    r#in: ParameterLocation::Path,
                    description: None,
                    required: Some(true),
                    schema: Some(parse_type_to_schema_ref(
                        elem_ty,
                        known_schemas,
                        struct_definitions,
                    )),
                    example: None,
                })
            })
            .collect::<Vec<_>>();
        return (!parameters.is_empty()).then_some(parameters);
    }

    (path_params.len() == 1).then(|| {
        vec![Parameter {
            name: path_params[0].clone(),
            r#in: ParameterLocation::Path,
            description: None,
            required: Some(true),
            schema: Some(parse_type_to_schema_ref(
                inner_ty,
                known_schemas,
                struct_definitions,
            )),
            example: None,
        }]
    })
}

pub(super) fn parse_bare_path_parameter(
    param_name: &str,
    ty: &Type,
    path_param_set: &HashSet<&str>,
    known_schemas: &HashSet<&str>,
    struct_definitions: &HashMap<&str, &str>,
) -> Option<Vec<Parameter>> {
    path_param_set.contains(param_name).then(|| {
        vec![Parameter {
            name: param_name.to_string(),
            r#in: ParameterLocation::Path,
            description: None,
            required: Some(true),
            schema: Some(parse_type_to_schema_ref(
                ty,
                known_schemas,
                struct_definitions,
            )),
            example: None,
        }]
    })
}

#[cfg(test)]
mod tests {
    use std::collections::{HashMap, HashSet};

    use vespera_core::route::ParameterLocation;

    use super::parse_path_extractor;
    use crate::parser::parameters::parse_function_parameter;

    #[test]
    fn path_param_by_name_match() {
        let func: syn::ItemFn = syn::parse_str("fn test(user_id: i32) {}").unwrap();
        let path_params = vec!["user_id".to_string()];
        let path_param_set: HashSet<&str> = path_params.iter().map(String::as_str).collect();

        for arg in &func.sig.inputs {
            let result = parse_function_parameter(
                arg,
                &path_params,
                &path_param_set,
                &HashSet::new(),
                &HashMap::new(),
            );
            assert!(result.is_some(), "Expected path parameter by name match");
            let params = result.unwrap();
            assert_eq!(params.len(), 1);
            assert_eq!(params[0].r#in, ParameterLocation::Path);
            assert_eq!(params[0].name, "user_id");
        }
    }

    #[test]
    fn path_extractor_rejects_missing_or_non_type_generic() {
        let bare: syn::Type = syn::parse_quote!(Path);
        assert!(parse_path_extractor(&bare, &[], &HashSet::new(), &HashMap::new()).is_none());
        let lifetime: syn::Type = syn::parse_quote!(Path<'static>);
        assert!(parse_path_extractor(&lifetime, &[], &HashSet::new(), &HashMap::new()).is_none());
    }
}