use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Fault {
#[error("no match")]
NoMatch,
#[error("denied: {reason}")]
Denied { reason: String },
#[error("backend error: {reason}")]
BackendError { reason: String },
#[error("flush miss")]
FlushMiss,
#[error("post-compaction bootstrap fault")]
PostCompactionBootstrap,
#[error("pinned invariant miss")]
PinnedInvariantMiss,
#[error("duplicate tool")]
DuplicateTool,
#[error("refetch")]
Refetch,
}
impl Fault {
pub fn is_no_match(&self) -> bool {
matches!(self, Fault::NoMatch)
}
pub fn is_denied(&self) -> bool {
matches!(self, Fault::Denied { .. })
}
pub fn code(&self) -> &'static str {
match self {
Fault::NoMatch => "no_match",
Fault::Denied { .. } => "denied",
Fault::BackendError { .. } => "backend_error",
Fault::FlushMiss => "flush_miss",
Fault::PostCompactionBootstrap => "post_compaction_bootstrap",
Fault::PinnedInvariantMiss => "pinned_invariant_miss",
Fault::DuplicateTool => "duplicate_tool",
Fault::Refetch => "refetch",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_match_is_its_own_thing() {
let f = Fault::NoMatch;
assert!(f.is_no_match());
assert!(!f.is_denied());
assert_eq!(f.code(), "no_match");
assert_eq!(f.to_string(), "no match");
}
#[test]
fn denied_carries_reason_string() {
let f = Fault::Denied {
reason: "policy rejected".into(),
};
assert!(f.is_denied());
assert!(f.to_string().contains("policy rejected"));
assert_eq!(f.code(), "denied");
}
#[test]
fn all_codes_are_unique_snake_case() {
let faults = [
Fault::NoMatch,
Fault::Denied { reason: "x".into() },
Fault::BackendError { reason: "y".into() },
Fault::FlushMiss,
Fault::PostCompactionBootstrap,
Fault::PinnedInvariantMiss,
Fault::DuplicateTool,
Fault::Refetch,
];
let codes: Vec<&'static str> = faults.iter().map(Fault::code).collect();
let mut seen = codes.clone();
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), codes.len(), "reason codes must be unique");
for c in &codes {
assert!(!c.is_empty());
assert_eq!(c.to_ascii_lowercase(), *c, "codes must be snake_case");
}
}
#[test]
fn fault_round_trips_through_serde() {
let f = Fault::Denied {
reason: "policy".into(),
};
let json = serde_json::to_string(&f).unwrap();
assert!(json.contains("\"kind\":\"denied\""));
let back: Fault = serde_json::from_str(&json).unwrap();
assert_eq!(back, f);
}
}