use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Nornir {
#[serde(default)]
pub guard: Guard,
#[serde(default)]
pub storage: Storage,
#[serde(default)]
pub repo: BTreeMap<String, Repo>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Guard {
#[serde(default)]
pub forbidden: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Storage {
#[serde(default)]
pub kind: String,
#[serde(default)]
pub local_path: String,
#[serde(default)]
pub remote_url: String,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Repo {
#[serde(default)] pub remote: String,
#[serde(default)] pub history: String,
#[serde(default)] pub readme: String,
#[serde(default)] pub publish_order: Vec<Vec<String>>,
#[serde(default)] pub gates: Gates,
#[serde(default)] pub bench: BenchSpec,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Gates {
#[serde(default)] pub no_path_patches: bool,
#[serde(default)] pub nexus_floor: bool,
#[serde(default)] pub no_regression: bool,
#[serde(default)] pub max_regression_pct: f64,
#[serde(default)] pub integration_roundtrip: Vec<String>,
#[serde(default)] pub docs_fresh: bool,
#[serde(default)] pub guard_intact: bool,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct BenchSpec {
#[serde(default)]
pub required_results: Vec<String>,
#[serde(default)]
pub network_required: bool,
}
impl BenchSpec {
pub fn missing_in<'a>(&'a self, run: &crate::bench::BenchRun) -> Vec<&'a str> {
let produced: std::collections::HashSet<&str> =
run.results.iter().map(|r| r.name.as_str()).collect();
let mut missing: Vec<&str> = self
.required_results
.iter()
.map(|s| s.as_str())
.filter(|n| !produced.contains(n))
.collect();
missing.sort();
missing
}
pub fn validate(&self, run: &crate::bench::BenchRun) -> anyhow::Result<()> {
let missing = self.missing_in(run);
if !missing.is_empty() {
anyhow::bail!(
"bench corpus is missing {} required result(s): [{}]",
missing.len(),
missing.join(", ")
);
}
Ok(())
}
}
pub struct Loaded {
pub nornir: Nornir,
pub config_path: PathBuf,
pub workspace_root: PathBuf,
}
impl Loaded {
pub fn warehouse_root(&self) -> PathBuf {
let storage = &self.nornir.storage;
if storage.local_path.is_empty() {
self.workspace_root.join("workspace_holger/.nornir/warehouse")
} else {
self.workspace_root.join(&storage.local_path).join("warehouse")
}
}
}
impl Nornir {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
toml::from_str(&text).with_context(|| format!("parse {}", path.display()))
}
pub fn repo_dir(workspace_root: &Path, name: &str) -> PathBuf {
workspace_root.join(name)
}
}
pub fn repo_dir_resolved(workspace_root: &Path, name: &str) -> PathBuf {
let exact = Nornir::repo_dir(workspace_root, name);
if exact.exists() {
return exact;
}
if let Ok(entries) = std::fs::read_dir(workspace_root) {
for entry in entries.flatten() {
if entry.file_name().to_string_lossy().eq_ignore_ascii_case(name)
&& entry.path().is_dir()
{
return entry.path();
}
}
}
exact
}
pub fn discover(start: &Path) -> Result<Loaded> {
let mut cur = start
.canonicalize()
.unwrap_or_else(|_| start.to_path_buf());
loop {
let candidate = cur.join("workspace_holger/release/nornir.toml");
if candidate.exists() {
let nornir = Nornir::load(&candidate)?;
return Ok(Loaded {
nornir,
config_path: candidate,
workspace_root: cur,
});
}
if !cur.pop() {
return Err(anyhow!(
"could not find workspace_holger/release/nornir.toml from {}",
start.display()
));
}
}
}
pub fn load_explicit(config_path: &Path) -> Result<Loaded> {
let nornir = Nornir::load(config_path)?;
let workspace_root = config_path
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.ok_or_else(|| anyhow!("config path lacks grandparent dirs: {}", config_path.display()))?
.to_path_buf();
Ok(Loaded {
nornir,
config_path: config_path.to_path_buf(),
workspace_root,
})
}