use std::collections::{HashMap, HashSet};
use syn::{FnArg, Pat, PatType};
use vespera_core::route::Parameter;
use super::extractors::unwrap_validated_type;
mod header;
mod path;
mod query;
mod shared;
pub fn parse_function_parameter(
arg: &FnArg,
path_params: &[String],
path_param_set: &HashSet<&str>,
known_schemas: &HashSet<&str>,
struct_definitions: &HashMap<&str, &str>,
) -> Option<Vec<Parameter>> {
match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(PatType { pat, ty, .. }) => {
let param_name = extract_param_name(pat.as_ref())?;
let ty = unwrap_validated_type(ty.as_ref());
if let Some(parameters) = header::parse_option_typed_header(¶m_name, ty) {
return Some(parameters);
}
if let Some(parameters) =
path::parse_path_extractor(ty, path_params, known_schemas, struct_definitions)
{
return Some(parameters);
}
if let Some(parameters) =
query::parse_query_extractor(¶m_name, ty, known_schemas, struct_definitions)
{
return Some(parameters);
}
if let Some(parameters) =
header::parse_header_extractor(¶m_name, ty, known_schemas, struct_definitions)
{
return Some(parameters);
}
path::parse_bare_path_parameter(
¶m_name,
ty,
path_param_set,
known_schemas,
struct_definitions,
)
}
}
}
fn extract_param_name(pat: &Pat) -> Option<String> {
match pat {
Pat::Ident(ident) => Some(ident.ident.to_string()),
Pat::TupleStruct(tuple_struct) if tuple_struct.elems.len() == 1 => {
extract_param_name(&tuple_struct.elems[0])
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use insta::{assert_debug_snapshot, with_settings};
use rstest::rstest;
use vespera_core::route::ParameterLocation;
use super::*;
fn setup_test_data(
func_src: &str,
) -> (HashSet<&'static str>, HashMap<&'static str, &'static str>) {
let mut struct_definitions = HashMap::new();
let mut known_schemas = HashSet::new();
if func_src.contains("QueryParams") {
known_schemas.insert("QueryParams");
struct_definitions.insert(
"QueryParams",
r"pub struct QueryParams { pub page: i32, pub limit: Option<i32> }",
);
}
if func_src.contains("User") {
known_schemas.insert("User");
struct_definitions.insert("User", r"pub struct User { pub id: i32, pub name: String }");
}
(known_schemas, struct_definitions)
}
#[rstest]
#[case("fn test(params: Path<(String, i32)>) {}", vec!["user_id".to_string(), "count".to_string()], vec![vec![ParameterLocation::Path, ParameterLocation::Path]], "path_tuple")]
#[case("fn show(Path(id): Path<i32>) {}", vec!["item_id".to_string()], vec![vec![ParameterLocation::Path]], "path_single")]
#[case("fn test(Query(params): Query<HashMap<String, String>>) {}", vec![], vec![vec![]], "query_hashmap")]
#[case("fn test(TypedHeader(user_agent): TypedHeader<UserAgent>, count: i32) {}", vec![], vec![vec![ParameterLocation::Header], vec![]], "typed_header_and_arg")]
#[case("fn test(TypedHeader(user_agent): TypedHeader<UserAgent>, content_type: Option<TypedHeader<ContentType>>, authorization: Option<TypedHeader<Authorization<Bearer>>>) {}", vec![], vec![vec![ParameterLocation::Header], vec![ParameterLocation::Header], vec![ParameterLocation::Header]], "typed_header_multi")]
#[case("fn test(user_agent: TypedHeader<UserAgent>, count: i32) {}", vec![], vec![vec![ParameterLocation::Header], vec![]], "header_value_and_arg")]
#[case("fn test(&self, id: i32) {}", vec![], vec![vec![], vec![]], "method_receiver")]
#[case("fn test(Path((a, b)): Path<(i32, String)>) {}", vec![], vec![vec![]], "path_tuple_destructure")]
#[case("fn test(params: Query<QueryParams>) {}", vec![], vec![vec![ParameterLocation::Query, ParameterLocation::Query]], "query_struct")]
#[case("fn test(Validated(Query(params)): Validated<Query<QueryParams>>) {}", vec![], vec![vec![ParameterLocation::Query, ParameterLocation::Query]], "validated_query_struct")]
#[case("fn test(Validated(Path(id)): Validated<Path<i32>>) {}", vec!["item_id".to_string()], vec![vec![ParameterLocation::Path]], "validated_path_single")]
#[case("fn test(body: Json<User>) {}", vec![], vec![vec![]], "json_body")]
#[case("fn test(params: Query<UnknownType>) {}", vec![], vec![vec![]], "query_unknown")]
#[case("fn test(params: Query<BTreeMap<String, String>>) {}", vec![], vec![vec![]], "query_map")]
#[case("fn test(user: Query<User>) {}", vec![], vec![vec![ParameterLocation::Query, ParameterLocation::Query]], "query_user")]
#[case("fn test(custom: Header<CustomHeader>) {}", vec![], vec![vec![ParameterLocation::Header]], "header_custom")]
#[case("fn test(input: Form<User>) {}", vec![], vec![vec![]], "form_body")]
#[case("fn test(upload: TypedMultipart<UploadRequest>) {}", vec![], vec![vec![]], "typed_multipart_body")]
#[case("fn test(multipart: Multipart) {}", vec![], vec![vec![]], "raw_multipart_body")]
fn parse_function_parameter_cases(
#[case] func_src: &str,
#[case] path_params: Vec<String>,
#[case] expected_locations: Vec<Vec<ParameterLocation>>,
#[case] suffix: &str,
) {
let func: syn::ItemFn = syn::parse_str(func_src).unwrap();
let (known_schemas, struct_definitions) = setup_test_data(func_src);
let path_param_set: HashSet<&str> = path_params.iter().map(String::as_str).collect();
let mut parameters = Vec::new();
for (idx, arg) in func.sig.inputs.iter().enumerate() {
let result = parse_function_parameter(
arg,
&path_params,
&path_param_set,
&known_schemas,
&struct_definitions,
);
let expected = expected_locations
.get(idx)
.unwrap_or_else(|| expected_locations.last().unwrap());
if expected.is_empty() {
assert!(
result.is_none(),
"Expected None at arg index {idx}, func: {func_src}"
);
continue;
}
let params = result.as_ref().expect("Expected Some parameters");
let got_locs: Vec<ParameterLocation> = params.iter().map(|p| p.r#in).collect();
assert_eq!(
got_locs, *expected,
"Location mismatch at arg index {idx}, func: {func_src}"
);
parameters.extend(params.clone());
}
with_settings!({ snapshot_path => "snapshots", snapshot_suffix => format!("params_{suffix}") }, {
assert_debug_snapshot!(parameters);
});
}
#[rstest]
#[case("fn test(id: Query<i32>) {}", vec![])]
#[case("fn test(auth: Header<String>) {}", vec![])]
#[case("fn test(params: Query<Vec<i32>>) {}", vec![])]
#[case("fn test(params: Query<Option<String>>) {}", vec![])]
#[case("fn test(Path([a]): Path<[i32; 1]>) {}", vec![])]
#[case("fn test(id: Path<i32>) {}", vec!["user_id".to_string(), "post_id".to_string()])]
#[case("fn test((x, y): (i32, i32)) {}", vec![])]
fn parse_function_parameter_wrong_cases(
#[case] func_src: &str,
#[case] path_params: Vec<String>,
) {
let func: syn::ItemFn = syn::parse_str(func_src).unwrap();
let (mut known_schemas, mut struct_definitions) = setup_test_data(func_src);
struct_definitions.insert("User", "pub struct User { pub id: i32 }");
known_schemas.insert("CustomHeader");
let path_param_set: HashSet<&str> = path_params.iter().map(String::as_str).collect();
for (idx, arg) in func.sig.inputs.iter().enumerate() {
let result = parse_function_parameter(
arg,
&path_params,
&path_param_set,
&known_schemas,
&struct_definitions,
);
assert!(
result.is_none(),
"Expected None at arg index {idx}, func: {func_src}, got: {result:?}"
);
}
}
}