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\"");
}
}