nornir 0.4.29

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
//! `nornir-workspace.toml` parsing.
//!
//! ```toml
//! [workspace]
//! name = "holger-suite"
//!
//! [repos.holger]
//! path = "../holger"
//!
//! [repos.znippy]
//! git = "https://codeberg.org/nordisk/znippy"
//! # optional:
//! # branch = "main"
//! ```

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WorkspaceDescriptor {
    pub workspace: WorkspaceMeta,
    #[serde(default)]
    pub repos: BTreeMap<String, RepoSpec>,
    /// Absolute path of the descriptor file's parent directory. Set by
    /// [`WorkspaceDescriptor::load`]; ignored for round-trip.
    #[serde(skip)]
    pub descriptor_dir: PathBuf,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WorkspaceMeta {
    pub name: String,
    /// Opt-in deep-scan: on each republish, knowledge-scan the members' **entire
    /// transitive dependency closure** (cargo fetch + metadata → gatling-fanned
    /// syn scans) into the warehouse under repo `deps`. See `crate::deepscan`.
    #[serde(default)]
    pub deep_scan: bool,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RepoSpec {
    #[serde(default)]
    pub path: Option<String>,
    #[serde(default)]
    pub git: Option<String>,
    #[serde(default)]
    pub branch: Option<String>,
}

#[derive(Debug, Clone)]
pub enum RepoSource {
    Path(PathBuf),
    Git { url: String, branch: Option<String> },
}

impl RepoSpec {
    pub fn source(&self, descriptor_dir: &Path) -> Result<RepoSource> {
        match (&self.path, &self.git) {
            (Some(p), None) => {
                let raw = PathBuf::from(p);
                let resolved = if raw.is_absolute() { raw } else { descriptor_dir.join(raw) };
                Ok(RepoSource::Path(resolved))
            }
            (None, Some(url)) => Ok(RepoSource::Git {
                url: url.clone(),
                branch: self.branch.clone(),
            }),
            (Some(_), Some(_)) => Err(anyhow!("repo spec has both `path` and `git`; choose one")),
            (None, None) => Err(anyhow!("repo spec has neither `path` nor `git`")),
        }
    }
}

impl WorkspaceDescriptor {
    pub fn load(path: &Path) -> Result<Self> {
        let text = std::fs::read_to_string(path)
            .with_context(|| format!("read workspace descriptor {}", path.display()))?;
        let mut d: WorkspaceDescriptor = toml::from_str(&text)
            .with_context(|| format!("parse workspace descriptor {}", path.display()))?;
        d.descriptor_dir = path
            .parent()
            .ok_or_else(|| anyhow!("descriptor path has no parent"))?
            .canonicalize()
            .with_context(|| format!("canonicalize descriptor parent of {}", path.display()))?;
        Ok(d)
    }

    /// Resolve every repo's source. Returns `(name, source)` pairs in
    /// deterministic (BTreeMap) order.
    pub fn sources(&self) -> Result<Vec<(String, RepoSource)>> {
        self.repos
            .iter()
            .map(|(name, spec)| spec.source(&self.descriptor_dir).map(|s| (name.clone(), s)))
            .collect()
    }
}