use std::fs;
use std::path::{Path, PathBuf};
const NUMERIC_PATH_PARAMS: &[&str] = &["player_id", "days", "seconds", "nb"];
#[test]
fn dynamic_path_segments_are_encoded() {
let api_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/api");
let mut files = Vec::new();
collect_rs_files(&api_dir, &mut files);
assert!(
!files.is_empty(),
"no source files under {}",
api_dir.display()
);
let mut violations = Vec::new();
for file in &files {
let text = fs::read_to_string(file).expect("source file is readable");
scan_file(&text, file, &api_dir, &mut violations);
}
assert!(violations.is_empty(), "{}", report(&violations));
}
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in fs::read_dir(dir).expect("api dir is readable") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
fn scan_file(text: &str, file: &Path, api_dir: &Path, out: &mut Vec<String>) {
let rel = file.strip_prefix(api_dir).unwrap_or(file);
for (idx, line) in text.lines().enumerate() {
if line.trim_start().starts_with("//") {
continue; }
for name in named_path_interpolations(line) {
if !NUMERIC_PATH_PARAMS.contains(&name.as_str()) {
out.push(format!("{}:{}: {{{name}}}", rel.display(), idx + 1));
}
}
}
}
fn named_path_interpolations(line: &str) -> Vec<String> {
let mut names = Vec::new();
let mut rest = line;
while let Some(pos) = rest.find("\"/") {
rest = &rest[pos + 1..]; let end = rest.find('"').unwrap_or(rest.len());
extract_named(&rest[..end], &mut names);
rest = &rest[end..];
}
names
}
fn extract_named(literal: &str, names: &mut Vec<String>) {
let mut rest = literal;
while let Some(open) = rest.find('{') {
rest = &rest[open + 1..];
if let Some(stripped) = rest.strip_prefix('{') {
rest = stripped; continue;
}
let Some(close) = rest.find('}') else { break };
let ident: String = rest[..close]
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
if ident.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_') {
names.push(ident);
}
rest = &rest[close + 1..];
}
}
fn report(violations: &[String]) -> String {
format!(
"unencoded dynamic path segment(s) found — wrap the value with \
`http::segment(...)` and a positional `{{}}` placeholder, or, if it is \
a numeric id, add it to NUMERIC_PATH_PARAMS:\n{}",
violations.join("\n")
)
}