use std::path::{Path, PathBuf};
use sonda_core::compiler::expand::InMemoryPackResolver;
use sonda_core::packs::MetricPackDef;
use tracing::{info, warn};
pub fn build_search_path() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Ok(env_val) = std::env::var("SONDA_PACK_PATH") {
for segment in env_val.split(':') {
let trimmed = segment.trim();
if !trimmed.is_empty() {
dirs.push(PathBuf::from(trimmed));
}
}
}
dirs
}
pub fn load_pack_resolver(search_path: &[PathBuf]) -> InMemoryPackResolver {
let mut resolver = InMemoryPackResolver::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut count = 0usize;
for dir in search_path {
if !dir.is_dir() {
continue;
}
let read_dir = match std::fs::read_dir(dir) {
Ok(rd) => rd,
Err(e) => {
warn!(dir = %dir.display(), error = %e, "cannot read pack directory");
continue;
}
};
for entry in read_dir.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let ext = path.extension().and_then(|e| e.to_str());
if ext != Some("yaml") && ext != Some("yml") {
continue;
}
match load_one(&path) {
Ok((name, file_stem, pack)) => {
if seen.insert(name.clone()) {
resolver.insert(name, pack.clone());
}
if seen.insert(file_stem.clone()) {
resolver.insert(file_stem, pack);
}
count += 1;
}
Err(e) => {
warn!(path = %path.display(), error = %e, "skipping pack file");
}
}
}
}
if count > 0 {
info!(packs_loaded = count, "loaded pack definitions");
}
resolver
}
fn load_one(path: &Path) -> Result<(String, String, MetricPackDef), String> {
let yaml = std::fs::read_to_string(path).map_err(|e| format!("read failed: {e}"))?;
let pack: MetricPackDef =
serde_yaml_ng::from_str(&yaml).map_err(|e| format!("YAML parse failed: {e}"))?;
let file_stem = path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
.unwrap_or_default();
Ok((pack.name.clone(), file_stem, pack))
}
#[cfg(test)]
mod tests {
use super::*;
use sonda_core::compiler::expand::PackResolver;
use tempfile::TempDir;
fn write_pack(dir: &Path, filename: &str, name: &str) {
let body = format!(
"name: {name}\ndescription: t\ncategory: test\nmetrics:\n - name: m\n generator: {{ type: constant, value: 1.0 }}\n"
);
std::fs::write(dir.join(filename), body).expect("write pack");
}
#[test]
fn build_search_path_empty_when_unset() {
unsafe {
std::env::remove_var("SONDA_PACK_PATH");
}
assert!(build_search_path().is_empty());
}
#[test]
fn load_resolver_registers_pack_under_name_and_file_stem() {
let dir = TempDir::new().expect("tmpdir");
write_pack(dir.path(), "srlinux-gnmi-bgp.yaml", "srlinux_gnmi_bgp");
let resolver = load_pack_resolver(&[dir.path().to_path_buf()]);
assert!(resolver.resolve("srlinux_gnmi_bgp").is_ok());
assert!(resolver.resolve("srlinux-gnmi-bgp").is_ok());
}
#[test]
fn load_resolver_skips_non_yaml_and_missing_dirs() {
let dir = TempDir::new().expect("tmpdir");
std::fs::write(dir.path().join("README.md"), "ignored").expect("md");
write_pack(dir.path(), "real_pack.yaml", "real_pack");
let missing = dir.path().join("does-not-exist");
let resolver = load_pack_resolver(&[missing, dir.path().to_path_buf()]);
assert!(resolver.resolve("real_pack").is_ok());
}
#[test]
fn load_resolver_first_match_wins_across_dirs() {
let a = TempDir::new().expect("tmpdir-a");
let b = TempDir::new().expect("tmpdir-b");
write_pack(a.path(), "p.yaml", "shared_name");
write_pack(b.path(), "p.yaml", "shared_name");
let resolver = load_pack_resolver(&[a.path().to_path_buf(), b.path().to_path_buf()]);
assert!(resolver.resolve("shared_name").is_ok());
}
}