use serde_json::{Map, Value, json};
#[derive(Debug, Clone)]
pub enum CheckStatus {
Pass,
Skip(String),
Warning(String),
Fail(String),
}
impl CheckStatus {
pub fn is_healthy_on_wire(&self) -> bool {
matches!(self, CheckStatus::Pass | CheckStatus::Skip(_))
}
pub fn is_fatal(&self) -> bool {
matches!(self, CheckStatus::Fail(_))
}
pub fn is_skip(&self) -> bool {
matches!(self, CheckStatus::Skip(_))
}
}
#[derive(Debug, Clone)]
pub struct Check {
pub name: &'static str,
pub status: CheckStatus,
pub summary: String,
pub details: Map<String, Value>,
pub payload_extras: Map<String, Value>,
}
impl Check {
pub fn pass(name: &'static str, summary: impl Into<String>) -> Self {
Self {
name,
status: CheckStatus::Pass,
summary: summary.into(),
details: Map::new(),
payload_extras: Map::new(),
}
}
pub fn skip(name: &'static str, summary: impl Into<String>, reason: impl Into<String>) -> Self {
let mut details = Map::new();
details.insert("skipped".into(), Value::Bool(true));
Self {
name,
status: CheckStatus::Skip(reason.into()),
summary: summary.into(),
details,
payload_extras: Map::new(),
}
}
pub fn warning(
name: &'static str,
summary: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self {
name,
status: CheckStatus::Warning(reason.into()),
summary: summary.into(),
details: Map::new(),
payload_extras: Map::new(),
}
}
pub fn fail(name: &'static str, summary: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
name,
status: CheckStatus::Fail(reason.into()),
summary: summary.into(),
details: Map::new(),
payload_extras: Map::new(),
}
}
pub fn with_detail(mut self, key: &str, value: impl Into<Value>) -> Self {
self.details.insert(key.to_string(), value.into());
self
}
pub fn with_details(mut self, details: Map<String, Value>) -> Self {
self.details = details;
self
}
pub fn with_payload_extra(mut self, key: &str, value: impl Into<Value>) -> Self {
self.payload_extras.insert(key.to_string(), value.into());
self
}
pub fn to_wire(&self) -> Value {
let mut obj = Map::new();
obj.insert("check".into(), self.name.into());
obj.insert("healthy".into(), self.status.is_healthy_on_wire().into());
for (k, v) in &self.details {
obj.insert(k.clone(), v.clone());
}
Value::Object(obj)
}
pub fn to_streaming_json(&self) -> Value {
let (status, reason) = match &self.status {
CheckStatus::Pass => ("pass", None),
CheckStatus::Skip(r) => ("skip", Some(r.as_str())),
CheckStatus::Warning(r) => ("warning", Some(r.as_str())),
CheckStatus::Fail(r) => ("fail", Some(r.as_str())),
};
let mut obj = json!({
"name": self.name,
"status": status,
"summary": self.summary,
"details": Value::Object(self.details.clone()),
});
if let Some(r) = reason {
obj["reason"] = Value::String(r.to_string());
}
obj
}
pub fn from_streaming_json(
value: &Value,
name_resolver: impl FnOnce(&str) -> Option<&'static str>,
) -> Option<Self> {
let name_str = value.get("name")?.as_str()?;
let name = name_resolver(name_str)?;
let status_str = value.get("status")?.as_str()?;
let reason = value
.get("reason")
.and_then(Value::as_str)
.map(str::to_string);
let status = match (status_str, reason) {
("pass", _) => CheckStatus::Pass,
("skip", Some(r)) => CheckStatus::Skip(r),
("warning", Some(r)) => CheckStatus::Warning(r),
("fail", Some(r)) => CheckStatus::Fail(r),
_ => return None,
};
let summary = value.get("summary")?.as_str()?.to_string();
let details = value
.get("details")
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
Some(Self {
name,
status,
summary,
details,
payload_extras: Map::new(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverallResult {
Healthy,
Degraded,
Failing,
}
impl OverallResult {
pub fn from_checks(checks: &[Check]) -> Self {
if checks.iter().any(|c| c.status.is_fatal()) {
OverallResult::Failing
} else if checks
.iter()
.any(|c| matches!(c.status, CheckStatus::Warning(_)))
{
OverallResult::Degraded
} else {
OverallResult::Healthy
}
}
pub fn is_healthy_top_level(self) -> bool {
!matches!(self, OverallResult::Failing)
}
pub fn label(self) -> &'static str {
match self {
OverallResult::Healthy => "HEALTHY",
OverallResult::Degraded => "DEGRADED",
OverallResult::Failing => "FAILING",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pass_is_healthy_on_wire() {
assert!(Check::pass("x", "ok").status.is_healthy_on_wire());
}
#[test]
fn warning_is_unhealthy_on_wire_but_not_fatal() {
let s = CheckStatus::Warning("w".into());
assert!(!s.is_healthy_on_wire());
assert!(!s.is_fatal());
}
#[test]
fn fail_is_unhealthy_and_fatal() {
let s = CheckStatus::Fail("f".into());
assert!(!s.is_healthy_on_wire());
assert!(s.is_fatal());
}
#[test]
fn skip_is_healthy_on_wire_and_not_fatal() {
let s = CheckStatus::Skip("reason".into());
assert!(s.is_healthy_on_wire());
assert!(!s.is_fatal());
assert!(s.is_skip());
}
#[test]
fn skip_does_not_change_overall_result() {
let with_skip = vec![Check::pass("a", ""), Check::skip("b", "", "r")];
assert_eq!(
OverallResult::from_checks(&with_skip),
OverallResult::Healthy
);
}
#[test]
fn skip_constructor_marks_skipped_detail() {
let c = Check::skip("memory", "not available", "platform mismatch");
assert_eq!(
c.details.get("skipped").and_then(Value::as_bool),
Some(true)
);
assert!(matches!(c.status, CheckStatus::Skip(_)));
}
#[test]
fn overall_from_checks() {
let healthy = vec![Check::pass("a", "")];
assert_eq!(OverallResult::from_checks(&healthy), OverallResult::Healthy);
let degraded = vec![Check::pass("a", ""), Check::warning("b", "", "x")];
assert_eq!(
OverallResult::from_checks(°raded),
OverallResult::Degraded
);
let failing = vec![Check::warning("a", "", "x"), Check::fail("b", "", "y")];
assert_eq!(OverallResult::from_checks(&failing), OverallResult::Failing);
}
#[test]
fn overall_top_level_healthy_only_true_when_not_failing() {
assert!(OverallResult::Healthy.is_healthy_top_level());
assert!(OverallResult::Degraded.is_healthy_top_level());
assert!(!OverallResult::Failing.is_healthy_top_level());
}
#[test]
fn check_to_wire_pass() {
let c = Check::pass("db_connect", "ok").with_detail("latency_ms", 3);
let v = c.to_wire();
assert_eq!(v["check"], "db_connect");
assert_eq!(v["healthy"], true);
assert_eq!(v["latency_ms"], 3);
}
#[test]
fn check_to_wire_warning_marks_unhealthy() {
let c = Check::warning("disk_free", "20% used", "below threshold");
let v = c.to_wire();
assert_eq!(v["healthy"], false);
}
}