use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TriageDecision {
Black,
Red,
Yellow,
Green,
White,
}
impl TriageDecision {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Black => "black",
Self::Red => "red",
Self::Yellow => "yellow",
Self::Green => "green",
Self::White => "white",
}
}
#[must_use]
pub fn parse_decision(s: &str) -> Option<Self> {
match s {
"black" | "Black" | "TriageDecision::Black" => Some(Self::Black),
"red" | "Red" | "TriageDecision::Red" => Some(Self::Red),
"yellow" | "Yellow" | "TriageDecision::Yellow" => Some(Self::Yellow),
"green" | "Green" | "TriageDecision::Green" => Some(Self::Green),
"white" | "White" | "TriageDecision::White" => Some(Self::White),
_ => None,
}
}
#[must_use]
pub const fn mandates_rollback(self) -> bool {
matches!(self, Self::Black | Self::Red)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ServerSideEnforcementMode {
FrictionOnly,
Structural,
}
impl ServerSideEnforcementMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::FrictionOnly => "friction-only",
Self::Structural => "structural",
}
}
#[must_use]
pub fn parse_mode(s: &str) -> Option<Self> {
match s {
"friction-only" | "friction_only" | "FrictionOnly" => Some(Self::FrictionOnly),
"structural" | "Structural" => Some(Self::Structural),
_ => None,
}
}
#[must_use]
pub const fn is_structural(self) -> bool {
matches!(self, Self::Structural)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn triage_decision_str_roundtrip() {
for variant in [
TriageDecision::Black,
TriageDecision::Red,
TriageDecision::Yellow,
TriageDecision::Green,
TriageDecision::White,
] {
let s = variant.as_str();
let back = TriageDecision::parse_decision(s).expect("kebab roundtrip");
assert_eq!(back, variant);
}
}
#[test]
fn triage_decision_parses_all_forms() {
assert_eq!(
TriageDecision::parse_decision("Black"),
Some(TriageDecision::Black)
);
assert_eq!(
TriageDecision::parse_decision("red"),
Some(TriageDecision::Red)
);
assert_eq!(
TriageDecision::parse_decision("TriageDecision::Yellow"),
Some(TriageDecision::Yellow)
);
assert_eq!(TriageDecision::parse_decision("unknown"), None);
}
#[test]
fn triage_decision_serde_kebab_case() {
let s = serde_json::to_string(&TriageDecision::Red).unwrap();
assert_eq!(s, "\"red\"");
let v: TriageDecision = serde_json::from_str("\"yellow\"").unwrap();
assert_eq!(v, TriageDecision::Yellow);
}
#[test]
fn rollback_mandate_matches_adr026() {
assert!(TriageDecision::Black.mandates_rollback());
assert!(TriageDecision::Red.mandates_rollback());
assert!(!TriageDecision::Yellow.mandates_rollback());
assert!(!TriageDecision::Green.mandates_rollback());
assert!(!TriageDecision::White.mandates_rollback());
}
#[test]
fn enforcement_mode_str_roundtrip() {
for variant in [
ServerSideEnforcementMode::FrictionOnly,
ServerSideEnforcementMode::Structural,
] {
let s = variant.as_str();
let back = ServerSideEnforcementMode::parse_mode(s).expect("kebab roundtrip");
assert_eq!(back, variant);
}
}
#[test]
fn enforcement_mode_is_structural() {
assert!(!ServerSideEnforcementMode::FrictionOnly.is_structural());
assert!(ServerSideEnforcementMode::Structural.is_structural());
}
#[test]
fn enforcement_mode_default_is_friction_only_in_adr() {
let default_mode = ServerSideEnforcementMode::FrictionOnly;
assert!(!default_mode.is_structural());
}
#[test]
fn triage_decision_rejects_empty_string() {
assert_eq!(TriageDecision::parse_decision(""), None);
}
#[test]
fn triage_decision_rejects_whitespace_padded() {
assert_eq!(TriageDecision::parse_decision("black "), None);
assert_eq!(TriageDecision::parse_decision(" black"), None);
}
#[test]
fn triage_decision_rejects_uppercase_ascii() {
assert_eq!(TriageDecision::parse_decision("BLACK"), None);
assert_eq!(TriageDecision::parse_decision("RED"), None);
}
#[test]
fn triage_decision_serde_rejects_unknown_variant() {
let result: Result<TriageDecision, _> = serde_json::from_str("\"purple\"");
assert!(
result.is_err(),
"serde should reject unknown variant 'purple', got Ok"
);
}
#[test]
fn triage_decision_serde_rejects_uppercase_variant() {
let result: Result<TriageDecision, _> = serde_json::from_str("\"BLACK\"");
assert!(
result.is_err(),
"serde should reject 'BLACK'; canonical form is 'black'"
);
}
#[test]
fn triage_decision_mandates_rollback_is_exhaustive_over_all_variants() {
let cases: &[(TriageDecision, bool)] = &[
(TriageDecision::Black, true),
(TriageDecision::Red, true),
(TriageDecision::Yellow, false),
(TriageDecision::Green, false),
(TriageDecision::White, false),
];
for (variant, expected) in cases {
assert_eq!(
variant.mandates_rollback(),
*expected,
"{variant:?}.mandates_rollback() should be {expected}"
);
}
}
#[test]
fn enforcement_mode_rejects_empty_string() {
assert_eq!(ServerSideEnforcementMode::parse_mode(""), None);
}
#[test]
fn enforcement_mode_rejects_whitespace_padded() {
assert_eq!(ServerSideEnforcementMode::parse_mode("structural "), None);
assert_eq!(
ServerSideEnforcementMode::parse_mode(" friction-only"),
None
);
}
#[test]
fn enforcement_mode_rejects_uppercase_ascii() {
assert_eq!(ServerSideEnforcementMode::parse_mode("STRUCTURAL"), None);
assert_eq!(ServerSideEnforcementMode::parse_mode("FRICTION-ONLY"), None);
}
#[test]
fn triage_decision_parse_is_stable_under_null_byte() {
assert_eq!(TriageDecision::parse_decision("\0"), None);
assert_eq!(TriageDecision::parse_decision("black\0"), None);
}
}