use std::{fs, path::PathBuf};
use syn::{spanned::Spanned, FnArg, ImplItem, Item, Pat, Visibility};
const SKIP_MARKER: &str = "no-param-check";
fn remote_services_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src").join("remote").join("services")
}
#[test]
fn every_remote_param_is_forwarded_in_body() {
let dir = remote_services_dir();
let entries = fs::read_dir(&dir).expect("remote services dir readable");
let mut failures: Vec<String> = Vec::new();
let mut files_checked = 0usize;
let mut params_checked = 0usize;
for entry in entries {
let path = entry.expect("dir entry").path();
if path.extension().and_then(|s| s.to_str()) != Some("rs") || !path.is_file() {
continue;
}
files_checked += 1;
let src = fs::read_to_string(&path).expect("read remote service file");
let lines: Vec<&str> = src.lines().collect();
let file_ast = syn::parse_file(&src).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
let rel = path.file_name().and_then(|s| s.to_str()).unwrap_or("?");
for item in &file_ast.items {
let Item::Impl(item_impl) = item else { continue };
for impl_item in &item_impl.items {
let ImplItem::Fn(fn_item) = impl_item else { continue };
let is_pub = matches!(fn_item.vis, Visibility::Public(_));
let is_async = fn_item.sig.asyncness.is_some();
if !(is_pub && is_async) {
continue;
}
let fn_line_1based = fn_item.sig.fn_token.span().start().line;
let fn_line_idx = fn_line_1based.saturating_sub(1);
let look_back = fn_line_idx.saturating_sub(8)..fn_line_idx;
let opted_out = lines[look_back].iter().any(|l| {
let t = l.trim_start();
t.starts_with("//") && t.contains(SKIP_MARKER)
});
if opted_out {
continue;
}
let body_start = fn_item.block.span().start().line.saturating_sub(1);
let body_end = fn_item.block.span().end().line.min(lines.len());
let body = lines[body_start..body_end].join("\n");
for arg in &fn_item.sig.inputs {
let FnArg::Typed(pat_type) = arg else { continue };
let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else { continue };
let name = pat_ident.ident.to_string();
params_checked += 1;
let key = format!("\"{name}\"");
if !body.contains(&key) {
failures.push(format!(
"remote/services/{rel}:{fn_line_1based} fn `{}` — param `{name}` not forwarded (no {key} key in body; add a `// {SKIP_MARKER}` comment if intentional)",
fn_item.sig.ident,
));
}
}
}
}
}
assert!(files_checked > 0, "no .rs files in {} — guard would silently pass", remote_services_dir().display());
assert!(params_checked >= 50, "only {params_checked} remote params discovered — parser probably regressed");
if !failures.is_empty() {
let count = failures.len();
let body = failures.join("\n ");
panic!("{count} remote param(s) not forwarded into the request body:\n {body}");
}
}