1use serde::Serialize;
8use std::time::Duration;
9
10#[derive(Debug, Clone)]
13pub struct CheckContext {
14 pub timeout: Duration,
16}
17
18impl Default for CheckContext {
19 fn default() -> Self {
20 Self {
21 timeout: Duration::from_secs(10),
22 }
23 }
24}
25
26#[derive(Debug, Clone, Serialize)]
28#[serde(tag = "status", rename_all = "snake_case")]
29pub enum ProbeStatus {
30 Pass,
32 Fail { reason: String },
34 Skip { reason: String },
36}
37
38#[derive(Debug, Clone, Serialize)]
41pub struct Probe {
42 pub name: &'static str,
44 #[serde(flatten)]
46 pub status: ProbeStatus,
47 pub elapsed_ms: u64,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub hint: Option<String>,
52}
53
54impl Probe {
55 pub fn pass(name: &'static str, elapsed: Duration) -> Self {
57 Self {
58 name,
59 status: ProbeStatus::Pass,
60 elapsed_ms: elapsed.as_millis() as u64,
61 hint: None,
62 }
63 }
64
65 pub fn fail(name: &'static str, elapsed: Duration, reason: impl Into<String>) -> Self {
67 Self {
68 name,
69 status: ProbeStatus::Fail {
70 reason: reason.into(),
71 },
72 elapsed_ms: elapsed.as_millis() as u64,
73 hint: None,
74 }
75 }
76
77 pub fn fail_hint(
79 name: &'static str,
80 elapsed: Duration,
81 reason: impl Into<String>,
82 hint: impl Into<String>,
83 ) -> Self {
84 Self {
85 name,
86 status: ProbeStatus::Fail {
87 reason: reason.into(),
88 },
89 elapsed_ms: elapsed.as_millis() as u64,
90 hint: Some(hint.into()),
91 }
92 }
93
94 pub fn skip(name: &'static str, reason: impl Into<String>) -> Self {
96 Self {
97 name,
98 status: ProbeStatus::Skip {
99 reason: reason.into(),
100 },
101 elapsed_ms: 0,
102 hint: None,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Default)]
109pub struct CheckReport {
110 pub probes: Vec<Probe>,
112}
113
114impl CheckReport {
115 pub fn single(probe: Probe) -> Self {
117 Self {
118 probes: vec![probe],
119 }
120 }
121
122 pub fn not_implemented() -> Self {
124 Self::single(Probe::skip("check", "no check implemented"))
125 }
126
127 pub fn failed_count(&self) -> usize {
129 self.probes
130 .iter()
131 .filter(|p| matches!(p.status, ProbeStatus::Fail { .. }))
132 .count()
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::time::Duration;
140
141 #[test]
142 fn pass_and_fail_constructors_set_status_and_elapsed() {
143 let p = Probe::pass("read", Duration::from_millis(42));
144 assert_eq!(p.name, "read");
145 assert!(matches!(p.status, ProbeStatus::Pass));
146 assert_eq!(p.elapsed_ms, 42);
147 assert!(p.hint.is_none());
148
149 let f = Probe::fail_hint("auth", Duration::from_millis(5), "bad token", "set TOKEN");
150 assert!(matches!(f.status, ProbeStatus::Fail { .. }));
151 assert_eq!(f.hint.as_deref(), Some("set TOKEN"));
152 }
153
154 #[test]
155 fn not_implemented_is_a_single_skip() {
156 let r = CheckReport::not_implemented();
157 assert_eq!(r.probes.len(), 1);
158 assert!(matches!(r.probes[0].status, ProbeStatus::Skip { .. }));
159 assert_eq!(r.failed_count(), 0);
160 }
161
162 #[test]
163 fn failed_count_counts_only_fail() {
164 let r = CheckReport {
165 probes: vec![
166 Probe::pass("a", Duration::ZERO),
167 Probe::fail("b", Duration::ZERO, "x"),
168 Probe::skip("c", "n/a"),
169 Probe::fail("d", Duration::ZERO, "y"),
170 ],
171 };
172 assert_eq!(r.failed_count(), 2);
173 }
174
175 #[test]
176 fn probe_serializes_status_inline() {
177 let p = Probe::fail("auth", Duration::from_millis(1), "nope");
178 let v = serde_json::to_value(&p).unwrap();
179 assert_eq!(v["name"], "auth");
180 assert_eq!(v["status"], "fail");
181 assert_eq!(v["reason"], "nope");
182 assert_eq!(v["elapsed_ms"], 1);
183 }
184}