use serde::Serialize;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct CheckContext {
pub timeout: Duration,
}
impl Default for CheckContext {
fn default() -> Self {
Self {
timeout: Duration::from_secs(10),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum ProbeStatus {
Pass,
Fail { reason: String },
Skip { reason: String },
}
#[derive(Debug, Clone, Serialize)]
pub struct Probe {
pub name: &'static str,
#[serde(flatten)]
pub status: ProbeStatus,
pub elapsed_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
}
impl Probe {
pub fn pass(name: &'static str, elapsed: Duration) -> Self {
Self {
name,
status: ProbeStatus::Pass,
elapsed_ms: elapsed.as_millis() as u64,
hint: None,
}
}
pub fn fail(name: &'static str, elapsed: Duration, reason: impl Into<String>) -> Self {
Self {
name,
status: ProbeStatus::Fail {
reason: reason.into(),
},
elapsed_ms: elapsed.as_millis() as u64,
hint: None,
}
}
pub fn fail_hint(
name: &'static str,
elapsed: Duration,
reason: impl Into<String>,
hint: impl Into<String>,
) -> Self {
Self {
name,
status: ProbeStatus::Fail {
reason: reason.into(),
},
elapsed_ms: elapsed.as_millis() as u64,
hint: Some(hint.into()),
}
}
pub fn skip(name: &'static str, reason: impl Into<String>) -> Self {
Self {
name,
status: ProbeStatus::Skip {
reason: reason.into(),
},
elapsed_ms: 0,
hint: None,
}
}
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct CheckReport {
pub probes: Vec<Probe>,
}
impl CheckReport {
pub fn single(probe: Probe) -> Self {
Self {
probes: vec![probe],
}
}
pub fn not_implemented() -> Self {
Self::single(Probe::skip("check", "no check implemented"))
}
pub fn failed_count(&self) -> usize {
self.probes
.iter()
.filter(|p| matches!(p.status, ProbeStatus::Fail { .. }))
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn pass_and_fail_constructors_set_status_and_elapsed() {
let p = Probe::pass("read", Duration::from_millis(42));
assert_eq!(p.name, "read");
assert!(matches!(p.status, ProbeStatus::Pass));
assert_eq!(p.elapsed_ms, 42);
assert!(p.hint.is_none());
let f = Probe::fail_hint("auth", Duration::from_millis(5), "bad token", "set TOKEN");
assert!(matches!(f.status, ProbeStatus::Fail { .. }));
assert_eq!(f.hint.as_deref(), Some("set TOKEN"));
}
#[test]
fn not_implemented_is_a_single_skip() {
let r = CheckReport::not_implemented();
assert_eq!(r.probes.len(), 1);
assert!(matches!(r.probes[0].status, ProbeStatus::Skip { .. }));
assert_eq!(r.failed_count(), 0);
}
#[test]
fn failed_count_counts_only_fail() {
let r = CheckReport {
probes: vec![
Probe::pass("a", Duration::ZERO),
Probe::fail("b", Duration::ZERO, "x"),
Probe::skip("c", "n/a"),
Probe::fail("d", Duration::ZERO, "y"),
],
};
assert_eq!(r.failed_count(), 2);
}
#[test]
fn probe_serializes_status_inline() {
let p = Probe::fail("auth", Duration::from_millis(1), "nope");
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["name"], "auth");
assert_eq!(v["status"], "fail");
assert_eq!(v["reason"], "nope");
assert_eq!(v["elapsed_ms"], 1);
}
}