use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct CwdMatch {
pub id: String,
pub root_path: PathBuf,
pub status_body: serde_json::Value,
}
pub async fn resolve_cwd_indexes(client: &reqwest::Client, base: &str) -> Result<Vec<CwdMatch>> {
let cwd = std::env::current_dir().context("could not determine current directory")?;
let cwd = std::fs::canonicalize(&cwd).unwrap_or(cwd);
resolve_indexes_for_cwd(client, base, &cwd).await
}
pub async fn resolve_indexes_for_cwd(
client: &reqwest::Client,
base: &str,
cwd: &Path,
) -> Result<Vec<CwdMatch>> {
let list_url = format!("{base}/indexes");
let list_body: serde_json::Value = client
.get(&list_url)
.send()
.await
.with_context(|| format!("could not reach daemon at {base}"))?
.error_for_status()
.with_context(|| format!("daemon returned an error for {list_url}"))?
.json()
.await
.context("could not parse /indexes response")?;
let empty: Vec<serde_json::Value> = Vec::new();
let ids: Vec<String> = list_body
.get("indexes")
.and_then(|v| v.as_array())
.unwrap_or(&empty)
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
let mut matches: Vec<CwdMatch> = Vec::new();
for id in ids {
let url = format!("{base}/indexes/{id}/status");
let resp = match client.get(&url).send().await {
Ok(r) if r.status().is_success() => r,
_ => continue,
};
let body: serde_json::Value = match resp.json().await {
Ok(b) => b,
Err(_) => continue,
};
let root_str = match body.get("root_path").and_then(|v| v.as_str()) {
Some(s) => s,
None => continue,
};
let root = PathBuf::from(root_str);
let canonical_root = std::fs::canonicalize(&root).unwrap_or_else(|_| root.clone());
if cwd_is_under(cwd, &canonical_root) {
matches.push(CwdMatch {
id,
root_path: root,
status_body: body,
});
}
}
matches.sort_by(|a, b| {
let la = a.root_path.as_os_str().len();
let lb = b.root_path.as_os_str().len();
la.cmp(&lb).then_with(|| {
a.root_path
.to_string_lossy()
.cmp(&b.root_path.to_string_lossy())
})
});
Ok(matches)
}
pub fn cwd_is_under(cwd: &Path, root: &Path) -> bool {
cwd.starts_with(root)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cwd_under_helper_exact_match() {
assert!(cwd_is_under(Path::new("/proj"), Path::new("/proj")));
}
#[test]
fn cwd_under_helper_ancestor_match() {
assert!(cwd_is_under(Path::new("/proj/a/b"), Path::new("/proj")));
}
#[test]
fn cwd_under_helper_non_ancestor() {
assert!(!cwd_is_under(Path::new("/other"), Path::new("/proj")));
}
#[test]
fn cwd_under_helper_partial_component_no_match() {
assert!(!cwd_is_under(Path::new("/projfoo/bar"), Path::new("/proj")));
}
#[test]
fn sort_by_root_path_length_shortest_first() {
let make_match = |root: &str| CwdMatch {
id: root.to_string(),
root_path: PathBuf::from(root),
status_body: serde_json::json!({}),
};
let mut matches = [make_match("/project/sub"), make_match("/project")];
matches.sort_by(|a, b| {
let la = a.root_path.as_os_str().len();
let lb = b.root_path.as_os_str().len();
la.cmp(&lb).then_with(|| {
a.root_path
.to_string_lossy()
.cmp(&b.root_path.to_string_lossy())
})
});
assert_eq!(matches[0].root_path, PathBuf::from("/project"));
assert_eq!(matches[1].root_path, PathBuf::from("/project/sub"));
}
#[test]
fn no_match_when_cwd_outside_all_roots() {
let cwd = Path::new("/home/user/other");
let roots = ["/home/user/project", "/opt/work"];
let matches: Vec<_> = roots
.iter()
.filter(|r| cwd_is_under(cwd, Path::new(r)))
.collect();
assert!(matches.is_empty());
}
#[test]
fn multiple_roots_covering_cwd() {
let cwd = Path::new("/ws/pkg/src/main.rs");
let roots = ["/ws", "/ws/pkg", "/other"];
let matches: Vec<_> = roots
.iter()
.filter(|r| cwd_is_under(cwd, Path::new(r)))
.collect();
assert_eq!(matches.len(), 2);
assert!(matches.iter().any(|r| **r == "/ws"));
assert!(matches.iter().any(|r| **r == "/ws/pkg"));
}
}