use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DevStateScope {
Supervisor,
Coord,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Eval {
True,
False,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DevState {
SlotsEmpty,
LegacyExeFallback,
LkgStale,
DistStale,
DistMissing,
SccacheDegraded,
PrimaryDown,
MainRed,
SiblingDrifted,
DeployPending,
}
impl DevState {
pub fn as_str(&self) -> &'static str {
match self {
DevState::SlotsEmpty => "SLOTS_EMPTY",
DevState::LegacyExeFallback => "LEGACY_EXE_FALLBACK",
DevState::LkgStale => "LKG_STALE",
DevState::DistStale => "DIST_STALE",
DevState::DistMissing => "DIST_MISSING",
DevState::SccacheDegraded => "SCCACHE_DEGRADED",
DevState::PrimaryDown => "PRIMARY_DOWN",
DevState::MainRed => "MAIN_RED",
DevState::SiblingDrifted => "SIBLING_DRIFTED",
DevState::DeployPending => "DEPLOY_PENDING",
}
}
pub fn scope(&self) -> DevStateScope {
match self {
DevState::SlotsEmpty
| DevState::LegacyExeFallback
| DevState::LkgStale
| DevState::DistStale
| DevState::DistMissing
| DevState::SccacheDegraded
| DevState::PrimaryDown => DevStateScope::Supervisor,
DevState::MainRed | DevState::SiblingDrifted | DevState::DeployPending => {
DevStateScope::Coord
}
}
}
pub fn all() -> &'static [DevState] {
&[
DevState::SlotsEmpty,
DevState::LegacyExeFallback,
DevState::LkgStale,
DevState::DistStale,
DevState::DistMissing,
DevState::SccacheDegraded,
DevState::PrimaryDown,
DevState::MainRed,
DevState::SiblingDrifted,
DevState::DeployPending,
]
}
}
impl fmt::Display for DevState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseDevStateError(pub String);
impl fmt::Display for ParseDevStateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unrecognized dev-state id: {}", self.0)
}
}
impl std::error::Error for ParseDevStateError {}
impl FromStr for DevState {
type Err = ParseDevStateError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"SLOTS_EMPTY" => Ok(DevState::SlotsEmpty),
"LEGACY_EXE_FALLBACK" => Ok(DevState::LegacyExeFallback),
"LKG_STALE" => Ok(DevState::LkgStale),
"DIST_STALE" => Ok(DevState::DistStale),
"DIST_MISSING" => Ok(DevState::DistMissing),
"SCCACHE_DEGRADED" => Ok(DevState::SccacheDegraded),
"PRIMARY_DOWN" => Ok(DevState::PrimaryDown),
"MAIN_RED" => Ok(DevState::MainRed),
"SIBLING_DRIFTED" => Ok(DevState::SiblingDrifted),
"DEPLOY_PENDING" => Ok(DevState::DeployPending),
other => Err(ParseDevStateError(other.to_string())),
}
}
}
impl<'a> TryFrom<&'a str> for DevState {
type Error = ParseDevStateError;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
s.parse()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct DevStateEval {
pub state: DevState,
pub value: Eval,
}
impl DevStateEval {
pub fn new(state: DevState, value: Eval) -> Self {
Self { state, value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn as_str_from_str_round_trip_for_every_variant() {
for &state in DevState::all() {
let s = state.as_str();
let parsed = DevState::from_str(s).expect("round-trip");
assert_eq!(parsed, state, "round-trip mismatch for {s}");
assert_eq!(DevState::try_from(s).unwrap(), state);
assert_eq!(format!("{state}"), s);
}
}
#[test]
fn from_str_rejects_unknown() {
let err = DevState::from_str("NOPE").unwrap_err();
assert_eq!(err, ParseDevStateError("NOPE".to_string()));
}
#[test]
fn serde_round_trip_for_every_variant() {
for &state in DevState::all() {
let json = serde_json::to_string(&state).expect("serialize");
let back: DevState = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, state);
}
}
#[test]
fn eval_serde_round_trip_and_wire_form() {
let cases = [
(Eval::True, "\"true\""),
(Eval::False, "\"false\""),
(Eval::Unknown, "\"unknown\""),
];
for (eval, wire) in cases {
assert_eq!(serde_json::to_string(&eval).unwrap(), wire);
let back: Eval = serde_json::from_str(wire).unwrap();
assert_eq!(back, eval);
}
}
#[test]
fn scope_assignment_matches_seed_table() {
for s in [
DevState::SlotsEmpty,
DevState::LegacyExeFallback,
DevState::LkgStale,
DevState::DistStale,
DevState::DistMissing,
DevState::SccacheDegraded,
DevState::PrimaryDown,
] {
assert_eq!(s.scope(), DevStateScope::Supervisor, "{s}");
}
for s in [
DevState::MainRed,
DevState::SiblingDrifted,
DevState::DeployPending,
] {
assert_eq!(s.scope(), DevStateScope::Coord, "{s}");
}
}
#[test]
fn dev_state_eval_serde_round_trip() {
let e = DevStateEval::new(DevState::LegacyExeFallback, Eval::True);
let json = serde_json::to_string(&e).expect("serialize");
let back: DevStateEval = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, e);
}
#[test]
fn matches_supervisor_phase1_literals() {
const SUPERVISOR_PHASE1: &[&str] = &[
"SLOTS_EMPTY",
"LEGACY_EXE_FALLBACK",
"LKG_STALE",
"DIST_STALE",
"DIST_MISSING",
"PRIMARY_DOWN",
];
for &lit in SUPERVISOR_PHASE1 {
let state = DevState::from_str(lit)
.unwrap_or_else(|_| panic!("shared registry missing supervisor literal {lit}"));
assert_eq!(state.as_str(), lit, "byte-for-byte mismatch for {lit}");
}
}
}