crates/nornir-zone/src/lib.rs
//! # nornir-zone
//!
//! The ONE shared **`Zone`** primitive for the nordisk constellation — the
//! platform-level **environment axis** every product understands.
//!
//! A [`Zone`] is an isolated environment/namespace a full default-infrastructure
//! stack is populated into — like `dev`/`staging`/`prod` in ops. It is a
//! **platform concept**, exactly like a JetBrains "project" is understood by every
//! IDE in the suite: `korp` populates a `demo` zone with the SAME plumbing `dwarves`
//! uses for a `test` zone or `nornir` for a `prod` zone.
//!
//! ## Orthogonal to "mode" (never conflate)
//! - **Mode** = HOW an app runs (korp: cases / analysis / infra).
//! - **Zone** = WHICH environment its infra + data live in (demo / test / prod).
//!
//! "korp analysis-mode against the prod zone" is a valid combination. `demo` is
//! merely the **default zone value** — NOT a mode and NOT a "demo button".
//!
//! ## Why it exists (LAW #5 — DRY)
//! `korp` grew a `korp::zone::Zone` (commit `a053966`) as the seed. Zone is a
//! platform concept every product needs, so the ONE definition lives **here** and
//! korp + every app (`nornir` / `dwarves` / `holger` / `modgunn` / `ordning`)
//! re-export/consume it — the korp-local twin is retired. This is a **true leaf**:
//! the enum + pure namespacing helpers, std-only, ZERO runtime deps, no domain
//! coupling (an app supplies its own **base port** and **app name**; the leaf owns
//! only the per-zone *offset* / *suffix* / *namespace* math).
//!
//! ## What a zone scopes (so two zones never collide)
//! Populating two zones at once would clobber each other if they shared a container
//! name, a published port, or a data namespace. A zone therefore scopes:
//! - the **container-name suffix** ([`Zone::name_suffix`]) — empty for [`Zone::Demo`]
//! (byte-identical to a pre-zone name), `-test` / `-prod` for the rest;
//! - the **published-port offset** ([`Zone::port_offset`]) — demo `+0`, test `+1`,
//! prod `+2`, added to an app's base port; and
//! - the **data namespace** ([`Zone::namespace`]) — `<app>_<zone>` (e.g.
//! `korp_demo`), the graph/warehouse/table prefix an app's population lands in.
//!
//! The enum is **extensible**: add a variant + its arm in [`Zone::ALL`] and every
//! `match` here (each exhaustive) forces the scoping helpers to follow. The default
//! is [`Zone::Demo`] so every pre-zone caller keeps landing where it did.
#![forbid(unsafe_code)]
/// **Introspection / emit marker** — record one functional-status row for the
/// nornir test matrix. Wraps `nornir_testmatrix::functional_status` behind the
/// optional `testmatrix` feature: ON, it emits a real row nornir reads back; OFF,
/// it is a compiled-out `#[inline]` no-op with NO nornir dependency, so the lean
/// default build never pulls it. Mirrors the sibling edda leaves.
#[inline]
pub fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(component, check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (component, check, ok, detail);
}
}
/// One **environment** the constellation's default infrastructure + data can be
/// populated into. Orthogonal to any app's "mode"; [`Zone::Demo`] is the default
/// value (what a pre-zone caller, passing no zone, lands in).
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum Zone {
/// The throwaway **demonstration** environment — the DEFAULT. Empty container
/// suffix (pre-zone-identical), port offset `0`, namespace `<app>_demo`.
#[default]
Demo,
/// A **test/staging** environment — isolated container/port/namespace so a test
/// populate never clobbers `demo` (or `prod`).
Test,
/// The **production** environment.
Prod,
}
impl Zone {
/// Every zone, in canonical display order (extend here when adding a variant —
/// the exhaustive `match`es below then force every helper to cover it).
pub const ALL: [Zone; 3] = [Zone::Demo, Zone::Test, Zone::Prod];
/// The stable machine key (`demo` / `test` / `prod`) — used in state JSON, the
/// CLI, and container/namespace scoping. Lowercase, stable, never localized.
pub fn key(self) -> &'static str {
match self {
Zone::Demo => "demo",
Zone::Test => "test",
Zone::Prod => "prod",
}
}
/// The human title shown on an app's zone picker (currently == [`key`](Self::key),
/// kept separate so a product can localize the label without touching the key).
pub fn title(self) -> &'static str {
match self {
Zone::Demo => "demo",
Zone::Test => "test",
Zone::Prod => "prod",
}
}
/// Parse a zone key (the canonical key + a few friendly aliases). Unknown →
/// `None`; the caller decides the fallback (usually [`Zone::Demo`]).
pub fn from_key(s: &str) -> Option<Zone> {
match s.trim().to_ascii_lowercase().as_str() {
"demo" | "demonstration" | "dev" => Some(Zone::Demo),
"test" | "staging" | "stage" => Some(Zone::Test),
"prod" | "production" | "live" => Some(Zone::Prod),
_ => None,
}
}
/// The published-port **OFFSET** added to an app's base port so zones don't
/// collide on the host: demo `+0`, test `+1`, prod `+2`. An app computes its
/// per-zone port as `base + zone.port_offset()` (see [`port_on`](Self::port_on)).
pub fn port_offset(self) -> u16 {
match self {
Zone::Demo => 0,
Zone::Test => 1,
Zone::Prod => 2,
}
}
/// An app's per-zone **published port** for a `base` port: `base +
/// `[`port_offset`](Self::port_offset). Demo returns `base` unchanged (pre-zone
/// parity); test/prod step up by one each. Saturating so a base near `u16::MAX`
/// never wraps.
pub fn port_on(self, base: u16) -> u16 {
base.saturating_add(self.port_offset())
}
/// The **container-name suffix** for this zone — empty for [`Zone::Demo`] (so a
/// managed container keeps its pre-zone name, byte-identical), `-test` / `-prod`
/// for the rest. An app names its per-zone container `<base-name><suffix>`
/// (see [`container_name`](Self::container_name)).
pub fn name_suffix(self) -> &'static str {
match self {
Zone::Demo => "",
Zone::Test => "-test",
Zone::Prod => "-prod",
}
}
/// An app's per-zone **container name** for a `base` name: `<base><suffix>`.
/// Demo returns `base` unchanged (pre-zone parity), test/prod append the suffix
/// (e.g. `korp-falkordb` → `korp-falkordb-test`).
pub fn container_name(self, base: &str) -> String {
format!("{base}{}", self.name_suffix())
}
/// The **data namespace** for an `app` in this zone: `<app>_<zone-key>` — the
/// graph / warehouse / table prefix the app's population lands in
/// (`namespace("korp")` → `korp_demo` / `korp_test` / `korp_prod`). Distinct per
/// zone so populating one never overwrites another; this is the ONE data-scoping
/// helper every product shares (korp's old `graph_name()` == `namespace("korp")`).
pub fn namespace(self, app: &str) -> String {
format!("{app}_{}", self.key())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// **RED-when-broken: the default zone is Demo** (every pre-zone caller keeps
/// landing where it did). If the default drifted, existing populate call-sites
/// would silently retarget to another environment.
#[test]
fn default_zone_is_demo() {
assert_eq!(Zone::default(), Zone::Demo);
assert_eq!(Zone::default().key(), "demo");
functional_status(
"nornir-zone",
"default_is_demo",
Zone::default() == Zone::Demo,
"the default zone is Demo — pre-zone callers land unchanged",
);
}
/// **RED-when-broken: zones DON'T COLLIDE** — every zone has a distinct
/// container suffix, port offset, and data namespace. If two zones shared any of
/// these, populating one would clobber the other (the whole reason zones scope).
/// Also pins Demo to pre-zone-identical scoping (no suffix, offset 0).
#[test]
fn zones_do_not_collide() {
use std::collections::BTreeSet;
let names: BTreeSet<&str> = Zone::ALL.iter().map(|z| z.name_suffix()).collect();
let ports: BTreeSet<u16> = Zone::ALL.iter().map(|z| z.port_offset()).collect();
let spaces: BTreeSet<String> = Zone::ALL.iter().map(|z| z.namespace("korp")).collect();
assert_eq!(names.len(), Zone::ALL.len(), "distinct container suffixes");
assert_eq!(ports.len(), Zone::ALL.len(), "distinct published-port offsets");
assert_eq!(spaces.len(), Zone::ALL.len(), "distinct data namespaces");
// Demo stays byte-identical to the pre-zone default.
assert_eq!(Zone::Demo.name_suffix(), "");
assert_eq!(Zone::Demo.port_offset(), 0);
assert_eq!(Zone::Demo.namespace("korp"), "korp_demo");
// ...and the derived per-app helpers compose the base with the zone scope.
assert_eq!(Zone::Demo.port_on(6379), 6379, "demo keeps the base port");
assert_eq!(Zone::Test.port_on(6379), 6380, "test steps the port up by one");
assert_eq!(Zone::Prod.port_on(6379), 6381, "prod steps the port up by two");
assert_eq!(Zone::Demo.container_name("korp-falkordb"), "korp-falkordb");
assert_eq!(Zone::Test.container_name("korp-falkordb"), "korp-falkordb-test");
functional_status(
"nornir-zone",
"zones_do_not_collide",
names.len() == Zone::ALL.len()
&& ports.len() == Zone::ALL.len()
&& spaces.len() == Zone::ALL.len(),
"every zone has a distinct container suffix / port offset / data namespace — no clobber",
);
}
/// Keys round-trip through `from_key`, and a few friendly aliases resolve.
#[test]
fn keys_round_trip_and_aliases_resolve() {
for z in Zone::ALL {
assert_eq!(Zone::from_key(z.key()), Some(z));
}
assert_eq!(Zone::from_key("staging"), Some(Zone::Test));
assert_eq!(Zone::from_key("production"), Some(Zone::Prod));
assert_eq!(Zone::from_key(" DEV "), Some(Zone::Demo), "trims + case-folds");
assert_eq!(Zone::from_key("bogus"), None);
}
}