use std::collections::HashMap;
use std::path::Path;
use crate::error::{MacroResult, err_call_site};
pub fn scan_route_folder(folder_path: &Path) -> MacroResult<Vec<(std::path::PathBuf, u64)>> {
crate::file_utils::collect_files_with_mtimes(folder_path).map_err(|e| err_call_site(format!("vespera! macro: failed to scan route folder '{}': {}. Verify the folder exists and is readable.", folder_path.display(), e)))
}
pub fn fingerprints_from_scan(scanned: &[(std::path::PathBuf, u64)]) -> HashMap<String, u64> {
scanned
.iter()
.filter(|(file, _)| file.extension().is_some_and(|e| e == "rs"))
.map(|(file, mtime)| (file.display().to_string(), *mtime))
.collect()
}
#[cfg(test)]
mod tests {
use std::fs;
use rstest::rstest;
use tempfile::TempDir;
use super::*;
use crate::collector::collect_metadata;
use crate::file_utils::normalize_path_key;
use crate::route_impl::StoredRouteInfo;
fn create_temp_file(dir: &TempDir, filename: &str, content: &str) -> std::path::PathBuf {
let file_path = dir.path().join(filename);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).expect("Failed to create parent directory");
}
fs::write(&file_path, content).expect("Failed to write temp file");
file_path
}
#[rstest]
#[case("src/routes/users.rs", "/work/src/routes/users.rs", "/work")]
#[case("src\\routes\\users.rs", "/work/src/routes/users.rs", "/work")]
#[case(
"src/./routes/../routes/users.rs",
"/work/src/routes/users.rs",
"/work"
)]
#[case("src/routes/users.rs", "/work/extra/../src/routes/users.rs", "/work")]
fn normalize_path_key_matches_equivalent_paths(
#[case] stored: &str,
#[case] walked: &str,
#[case] cwd: &str,
) {
let cwd = Path::new(cwd);
assert_eq!(
normalize_path_key(stored, cwd),
normalize_path_key(walked, cwd),
"stored={stored:?} and walked={walked:?} must produce the same key"
);
}
#[test]
fn normalize_path_key_distinguishes_different_files() {
let cwd = Path::new("/work");
assert_ne!(
normalize_path_key("src/routes/users.rs", cwd),
normalize_path_key("src/routes/posts.rs", cwd),
);
}
#[cfg(windows)]
#[test]
fn normalize_path_key_windows_verbatim_prefix_and_case() {
let cwd = Path::new("C:\\work");
assert_eq!(
normalize_path_key("\\\\?\\C:\\Work\\Src\\Users.RS", cwd),
normalize_path_key("c:/work/src/users.rs", cwd),
);
}
#[test]
fn fast_path_matches_cwd_relative_storage_paths_without_parsing() {
let unique = format!("vespera_fastpath_lock_{}", std::process::id());
let abs_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("target")
.join(&unique);
fs::create_dir_all(&abs_dir).expect("create test route dir");
fs::write(
abs_dir.join("users.rs"),
"this is deliberately not rust {{{",
)
.expect("write route file");
let relative_stored_path = format!("../../target/{unique}/users.rs");
let route_storage = vec![StoredRouteInfo {
fn_name: "get_users".to_string(),
method: None,
custom_path: None,
error_status: None,
typed_responses: None,
tags: None,
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: None,
fn_sig_str: "async fn get_users() -> String".to_string(),
file_path: Some(relative_stored_path),
}];
let result = collect_metadata(&abs_dir, "routes", &route_storage);
fs::remove_dir_all(&abs_dir).ok();
let (metadata, file_asts) = result.expect(
"fast path must match the relative storage path WITHOUT parsing — \
a parse error here means key normalization regressed and the \
slow path ran against the invalid file",
);
assert_eq!(metadata.routes.len(), 1, "route must come from storage");
assert!(
file_asts.is_empty(),
"fast path must not parse any file ASTs"
);
}
#[test]
fn fast_path_defaults_missing_method_to_get() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let file_path = create_temp_file(&temp_dir, "items.rs", "// placeholder\n");
let route_storage = vec![StoredRouteInfo {
fn_name: "list_items".to_string(),
method: None, custom_path: None,
error_status: None,
typed_responses: None,
tags: None,
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: None,
fn_sig_str: "async fn list_items() -> String".to_string(),
file_path: Some(file_path.display().to_string()),
}];
let (metadata, _) = collect_metadata(temp_dir.path(), "routes", &route_storage).unwrap();
assert_eq!(metadata.routes.len(), 1);
assert_eq!(
metadata.routes[0].method, "get",
"missing method must default to GET — \"\" silently drops the route"
);
drop(temp_dir);
}
#[test]
fn test_collect_metadata_fast_path_with_route_storage() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let folder_name = "routes";
let file_path = create_temp_file(
&temp_dir,
"users.rs",
r#"
pub async fn get_users() -> String {
"users".to_string()
}
"#,
);
let file_path_str = file_path.display().to_string();
let route_storage = vec![StoredRouteInfo {
fn_name: "get_users".to_string(),
method: Some("get".to_string()),
custom_path: None,
error_status: None,
typed_responses: None,
tags: Some(vec!["users".to_string()]),
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: Some("Get all users".to_string()),
fn_sig_str: "async fn get_users() -> String".to_string(),
file_path: Some(file_path_str.clone()),
}];
let (metadata, file_asts) =
collect_metadata(temp_dir.path(), folder_name, &route_storage).unwrap();
assert_eq!(metadata.routes.len(), 1);
let route = &metadata.routes[0];
assert_eq!(route.function_name, "get_users");
assert_eq!(route.method, "get");
assert_eq!(route.tags, Some(vec!["users".to_string()]));
assert_eq!(route.description, Some("Get all users".to_string()));
assert_eq!(route.module_path, "routes::users");
assert!(
file_asts.is_empty(),
"Fast path should not populate file_asts"
);
drop(temp_dir);
}
#[test]
fn test_collect_metadata_fast_path_with_custom_path() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let folder_name = "routes";
let file_path = create_temp_file(
&temp_dir,
"users.rs",
r#"
pub async fn get_user() -> String {
"user".to_string()
}
"#,
);
let file_path_str = file_path.display().to_string();
let route_storage = vec![StoredRouteInfo {
fn_name: "get_user".to_string(),
method: Some("get".to_string()),
custom_path: Some("/{id}".to_string()),
error_status: Some(vec![404]),
typed_responses: None,
tags: None,
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: None,
fn_sig_str: "async fn get_user(id: i32) -> String".to_string(),
file_path: Some(file_path_str.clone()),
}];
let (metadata, _) = collect_metadata(temp_dir.path(), folder_name, &route_storage).unwrap();
assert_eq!(metadata.routes.len(), 1);
let route = &metadata.routes[0];
assert_eq!(route.path, "/users/{id}");
assert!(route.error_status.is_some());
assert_eq!(route.error_status.as_ref().unwrap(), &vec![404]);
drop(temp_dir);
}
#[test]
fn test_collect_metadata_fast_path_empty_folder_name() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let folder_name = "";
let file_path = create_temp_file(
&temp_dir,
"users.rs",
r#"
pub async fn list_users() -> String {
"list".to_string()
}
"#,
);
let file_path_str = file_path.display().to_string();
let route_storage = vec![StoredRouteInfo {
fn_name: "list_users".to_string(),
method: Some("get".to_string()),
custom_path: None,
error_status: None,
typed_responses: None,
tags: None,
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: None,
fn_sig_str: "async fn list_users() -> String".to_string(),
file_path: Some(file_path_str),
}];
let (metadata, _) = collect_metadata(temp_dir.path(), folder_name, &route_storage).unwrap();
assert_eq!(metadata.routes.len(), 1);
let route = &metadata.routes[0];
assert_eq!(route.module_path, "users");
drop(temp_dir);
}
#[test]
fn test_collect_metadata_fast_path_uses_stored_description() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let folder_name = "routes";
let file_path = create_temp_file(&temp_dir, "items.rs", "// placeholder\n");
let file_path_str = file_path.display().to_string();
let route_storage = vec![StoredRouteInfo {
fn_name: "get_items".to_string(),
method: Some("get".to_string()),
custom_path: None,
error_status: None,
typed_responses: None,
tags: None,
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: Some("List all items".to_string()),
fn_sig_str:
"/// List all items\npub async fn get_items() -> String { \"items\".to_string() }"
.to_string(),
file_path: Some(file_path_str.clone()),
}];
let (metadata, _) = collect_metadata(temp_dir.path(), folder_name, &route_storage).unwrap();
assert_eq!(metadata.routes.len(), 1);
assert_eq!(
metadata.routes[0].description,
Some("List all items".to_string())
);
let route_storage_none = vec![StoredRouteInfo {
fn_name: "get_items".to_string(),
method: Some("get".to_string()),
custom_path: None,
error_status: None,
typed_responses: None,
tags: None,
security: None,
headers: Vec::new(),
success_status: None,
operation_id: None,
summary: None,
request_example: None,
response_example: None,
deprecated: false,
description: None,
fn_sig_str: "async fn get_items() -> String".to_string(),
file_path: Some(file_path_str),
}];
let (metadata, _) =
collect_metadata(temp_dir.path(), folder_name, &route_storage_none).unwrap();
assert_eq!(metadata.routes[0].description, None);
drop(temp_dir);
}
#[test]
fn test_collect_file_fingerprints_skips_non_rs_files() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
create_temp_file(&temp_dir, "valid.rs", "pub fn hello() {}");
create_temp_file(&temp_dir, "readme.txt", "This is a readme");
create_temp_file(&temp_dir, "data.json", "{}");
create_temp_file(&temp_dir, "script.py", "print('hello')");
let fingerprints = fingerprints_from_scan(&scan_route_folder(temp_dir.path()).unwrap());
assert_eq!(
fingerprints.len(),
1,
"Only .rs files should be fingerprinted"
);
let keys: Vec<&String> = fingerprints.keys().collect();
assert!(
keys[0].ends_with("valid.rs"),
"The only fingerprinted file should be valid.rs"
);
drop(temp_dir);
}
#[test]
fn scan_route_folder_reports_missing_folder() {
let missing = Path::new("target/vespera-missing-route-folder");
let error = scan_route_folder(missing).expect_err("missing folder must fail scanning");
let message = error.to_string();
assert!(message.contains("failed to scan route folder"));
assert!(message.contains("vespera-missing-route-folder"));
}
}