wsb 0.258.6441

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! What a workspace tells the stamp about itself: `<scan root>/.wsb/stamp.json`.
//!
//! Two things a template can ask for are not facts about any project and not
//! environment variables, and the stamp cannot discover them: which file holds
//! the workspace's own declaration (`{{ declared.* }}`), and where the
//! workspace builds (`{{ build.root_from_here }}`). And `wsb local` needs to
//! know which variable selects a template's local branch. The workspace names
//! them here, committed, so every worktree renders the same way.
//!
//! ```json
//! {
//!   "declared": "workspace.json",
//!   "build_root": ".wsb/disposable/build",
//!   "local": {
//!     "variable": "LOCAL_LIBS",
//!     "prepare": ["make", "local-deps"],
//!     "release": ["make", "release-local-deps"]
//!   }
//! }
//! ```
//!
//! `declared` and `build_root` are paths relative to the scan root. A template
//! that asks for one the file does not name is refused, naming the file — a
//! stamp that guessed would render a path nobody chose.

use crate::prelude::*;

/// Where the configuration lives, relative to the scan root.
pub const FILE: &str = ".wsb/stamp.json";

#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StampConfig {
    /// The JSON file `{{ declared.* }}` reads.
    pub declared: Option<PathBuf>,
    /// The directory `{{ build.root_from_here }}` names.
    pub build_root: Option<PathBuf>,
    /// How this workspace renders against its own working copies.
    pub local: Option<LocalRendering>,
}

/// The local render: `wsb local on` renders every template with `variable`
/// set, so a template's `{% if env.<variable> %}` branch names the working
/// copy next door instead of a published version.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalRendering {
    /// The environment variable whose presence selects the local branch.
    pub variable: String,
    /// What to run, in the scan root, when a local render names something
    /// under the build root — the build that produces what it names. Run after
    /// the render, and the render is taken back if it fails.
    #[serde(default)]
    pub prepare: Option<Vec<String>>,
    /// What to run, in the scan root, when the tree stops being local — so
    /// what `prepare` built may be swept again.
    #[serde(default)]
    pub release: Option<Vec<String>>,
}

impl StampConfig {
    /// The scan root's configuration, or `None` when it has none.
    pub fn load(scan_root: &Path) -> Result<Option<Self>> {
        let path = scan_root.join(FILE);
        let text = match std::fs::read_to_string(&path) {
            Ok(text) => text,
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(error) => return Err(error).with_context(|| format!("cannot read {}", path.display())),
        };
        serde_json::from_str(&text)
            .map(Some)
            .with_context(|| format!("{} is not a valid stamp configuration", path.display()))
    }

    /// The file `{{ declared.* }}` reads.
    pub fn declared_file(scan_root: &Path) -> Result<PathBuf> {
        Self::named(scan_root, "declared", |config| config.declared.as_ref())
    }

    /// The directory `{{ build.root_from_here }}` names.
    pub fn build_root(scan_root: &Path) -> Result<PathBuf> {
        Self::named(scan_root, "build_root", |config| config.build_root.as_ref())
    }

    /// How this workspace renders locally.
    pub fn local(scan_root: &Path) -> Result<LocalRendering> {
        let path = scan_root.join(FILE);
        match Self::load(scan_root)? {
            None => bail!(
                "a local render needs the variable that selects it, declared as \"local\" in {}, \
                 and there is no such file",
                path.display()
            ),
            Some(config) => config.local.ok_or_else(|| {
                anyhow!(
                    "a local render needs the variable that selects it, and {} declares no \"local\"",
                    path.display()
                )
            }),
        }
    }

    fn named(
        scan_root: &Path,
        key: &str,
        pick: impl Fn(&Self) -> Option<&PathBuf>,
    ) -> Result<PathBuf> {
        let path = scan_root.join(FILE);
        match Self::load(scan_root)? {
            None => bail!(
                "a template asks for what {} would name as \"{key}\", and there is no {}",
                FILE,
                path.display()
            ),
            Some(config) => match pick(&config) {
                Some(relative) => Ok(scan_root.join(relative)),
                None => bail!(
                    "a template asks for what {} names as \"{key}\", and it names none",
                    path.display()
                ),
            },
        }
    }
}

#[cfg(test)]
#[path = "stamp_config_tests.rs"]
mod tests;