1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
12#[serde(rename_all = "snake_case")]
13pub enum HealthStatus {
14 Healthy,
16 Degraded,
18 Unhealthy,
20 Unknown,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
27pub struct HealthCheck {
28 pub status: HealthStatus,
30 pub component: String,
32 pub message: Option<String>,
34 pub response_time_ms: Option<i64>,
36 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
38 pub metadata: Option<HashMap<String, serde_json::Value>>,
39}
40
41impl HealthCheck {
42 pub fn healthy(component: impl Into<String>) -> Self {
44 Self {
45 status: HealthStatus::Healthy,
46 component: component.into(),
47 message: None,
48 response_time_ms: None,
49 metadata: None,
50 }
51 }
52
53 pub fn degraded(component: impl Into<String>, message: impl Into<String>) -> Self {
55 Self {
56 status: HealthStatus::Degraded,
57 component: component.into(),
58 message: Some(message.into()),
59 response_time_ms: None,
60 metadata: None,
61 }
62 }
63
64 pub fn unhealthy(component: impl Into<String>, message: impl Into<String>) -> Self {
66 Self {
67 status: HealthStatus::Unhealthy,
68 component: component.into(),
69 message: Some(message.into()),
70 response_time_ms: None,
71 metadata: None,
72 }
73 }
74
75 pub fn with_response_time(mut self, ms: i64) -> Self {
77 self.response_time_ms = Some(ms);
78 self
79 }
80
81 pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
83 self.metadata
84 .get_or_insert_with(HashMap::new)
85 .insert(key.into(), value);
86 self
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 #[test]
95 fn healthy_check_has_correct_status() {
96 let check = HealthCheck::healthy("db");
97 assert_eq!(check.status, HealthStatus::Healthy);
98 assert_eq!(check.component, "db");
99 assert!(check.message.is_none());
100 }
101
102 #[test]
103 fn degraded_check_has_message() {
104 let check = HealthCheck::degraded("cache", "high latency");
105 assert_eq!(check.status, HealthStatus::Degraded);
106 assert_eq!(check.message.as_deref(), Some("high latency"));
107 }
108
109 #[test]
110 fn unhealthy_check_has_message() {
111 let check = HealthCheck::unhealthy("db", "connection refused");
112 assert_eq!(check.status, HealthStatus::Unhealthy);
113 assert_eq!(check.message.as_deref(), Some("connection refused"));
114 }
115
116 #[test]
117 fn with_response_time_sets_field() {
118 let check = HealthCheck::healthy("db").with_response_time(42);
119 assert_eq!(check.response_time_ms, Some(42));
120 }
121
122 #[test]
123 fn with_metadata_adds_entries() {
124 let check = HealthCheck::healthy("db")
125 .with_metadata("version", serde_json::json!("15.2"))
126 .with_metadata("connections", serde_json::json!(10));
127 let meta = check.metadata.unwrap();
128 assert_eq!(meta.len(), 2);
129 assert_eq!(meta["version"], serde_json::json!("15.2"));
130 }
131
132 #[test]
133 fn health_status_serde_roundtrip() {
134 let statuses = vec![
135 HealthStatus::Healthy,
136 HealthStatus::Degraded,
137 HealthStatus::Unhealthy,
138 HealthStatus::Unknown,
139 ];
140 for s in statuses {
141 let json = serde_json::to_string(&s).unwrap();
142 let d: HealthStatus = serde_json::from_str(&json).unwrap();
143 assert_eq!(s, d);
144 }
145 }
146
147 #[test]
148 fn health_check_serde_roundtrip() {
149 let check = HealthCheck::healthy("db")
150 .with_response_time(5)
151 .with_metadata("pool_size", serde_json::json!(20));
152 let json = serde_json::to_string(&check).unwrap();
153 let d: HealthCheck = serde_json::from_str(&json).unwrap();
154 assert_eq!(check, d);
155 }
156}