Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-build-thing/src/verbs.rs

//! **Typ bears its verbs** — the kind → action table, as DATA in edda.
//!
//! The artifact's typ on the receipt decides what a pane or a CLI may DO with
//! it. This table used to be hardcoded in dwarves' `build_scene.rs` (kind →
//! pane); it lives here so the forge's CLI, the ⚒ pane and a build thing's own
//! [`actions`](crate::plugin::BuildThing::actions) refinement read one list.
//!
//! | typ | verb |
//! |---|---|
//! | exe, service | RUN |
//! | container | RUN |
//! | kernel | BOOT |
//! | iso, installer, vm | BOOT |
//! | lib, wasm, package | — (nothing to do with it alone) |
//!
//! `UNPACK` is the verb of a znippy archive — a payload kind, not an
//! [`ArtifactKind`] row of its own today; [`Action::Unpack`] exists so the
//! table is complete when a build thing refines with it.

use serde::{Deserialize, Serialize};

use crate::variant::ArtifactKind;

/// What may be done with a built artifact of some typ.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Action {
    /// Execute it (an exe, a service, a container).
    Run,
    /// Boot it (a kernel, an iso, an installer medium, a vm image).
    Boot,
    /// Unpack it (a znippy archive).
    Unpack,
}

impl Action {
    /// The verb as a button says it: `RUN` / `BOOT` / `UNPACK`.
    pub fn verb(self) -> &'static str {
        match self {
            Action::Run => "RUN",
            Action::Boot => "BOOT",
            Action::Unpack => "UNPACK",
        }
    }
}

/// **The table.** Every [`ArtifactKind`] row and its verbs, in one place.
pub const VERBS: &[(ArtifactKind, &[Action])] = &[
    (ArtifactKind::Exe, &[Action::Run]),
    (ArtifactKind::Service, &[Action::Run]),
    (ArtifactKind::Container, &[Action::Run]),
    (ArtifactKind::Kernel, &[Action::Boot]),
    (ArtifactKind::Iso, &[Action::Boot]),
    (ArtifactKind::Installer, &[Action::Boot]),
    (ArtifactKind::Vm, &[Action::Boot]),
    (ArtifactKind::Lib, &[]),
    (ArtifactKind::Wasm, &[]),
    // A produced FILE SET is INSTALLED by its ecosystem's own tool (`pip install`),
    // on a machine the forge does not own. There is no verb here for the same
    // reason `lib` has none: the empty list is the honest answer, and inventing an
    // INSTALL action would be a button that could only ever mean "install it
    // somewhere unspecified".
    (ArtifactKind::Package, &[]),
];

/// The verbs a typ bears — the default every build thing starts from.
pub fn actions_for(kind: ArtifactKind) -> Vec<Action> {
    VERBS
        .iter()
        .find(|(k, _)| *k == kind)
        .map(|(_, a)| a.to_vec())
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// ⚠ **This test enumerated the kinds BY HAND until 2026-09-06, and that made
    /// it hollow for the only case it exists to catch.** `actions_for` ends in
    /// `unwrap_or_default()`, so a kind with no row answers the empty list —
    /// indistinguishable from `lib`, which has no verbs on purpose. A tenth kind
    /// arriving would have been missing from `VERBS` *and* from the hand-written
    /// list here, and the test would have passed. It did: `Package` landed with no
    /// row and this test was green until the sweep below replaced the list with
    /// [`ArtifactKind::ALL`].
    #[test]
    fn every_kind_has_a_row_and_the_verbs_are_the_ones_the_pane_hardcoded() {
        for kind in ArtifactKind::ALL {
            assert!(
                VERBS.iter().any(|(k, _)| k == kind),
                "{kind:?} has no row in VERBS — `actions_for` would answer the empty \
                 list by ACCIDENT, and nothing would say which"
            );
        }
        assert_eq!(actions_for(ArtifactKind::Exe), vec![Action::Run]);
        assert_eq!(actions_for(ArtifactKind::Container), vec![Action::Run]);
        assert_eq!(actions_for(ArtifactKind::Kernel), vec![Action::Boot]);
        assert_eq!(actions_for(ArtifactKind::Iso), vec![Action::Boot]);
        assert_eq!(actions_for(ArtifactKind::Installer), vec![Action::Boot]);
        assert!(actions_for(ArtifactKind::Lib).is_empty());
        assert!(actions_for(ArtifactKind::Wasm).is_empty());
        assert!(actions_for(ArtifactKind::Package).is_empty());
        assert_eq!(Action::Unpack.verb(), "UNPACK");
        assert_eq!(serde_json::to_string(&Action::Boot).unwrap(), "\"boot\"");
    }
}