1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct HealthCheck {
12 pub record_id: Uuid,
14 #[serde(rename = "type")]
16 pub check_type: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub name: Option<String>,
20 pub config: serde_json::Value,
22 pub interval: u32,
24 pub timeout: u32,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub retries: Option<u32>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub retry_interval: Option<u32>,
32 pub last_status: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub last_checked_at: Option<i64>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub last_error: Option<String>,
41 pub created_at: i64,
43 pub updated_at: i64,
45}
46
47impl HealthCheck {
48 pub fn typed_config(&self) -> Option<HealthCheckConfig> {
50 serde_json::from_value(serde_json::json!({
51 "type": self.check_type,
52 "config": self.config,
53 }))
54 .ok()
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(tag = "type", content = "config", rename_all = "lowercase")]
61pub enum HealthCheckConfig {
62 Http(HttpCheck),
64 Tcp(TcpCheck),
66 Ping(PingCheck),
68}
69
70#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
72pub struct HttpCheck {
73 pub url: String,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub method: Option<String>,
78 #[serde(
80 rename = "expectedStatus",
81 default,
82 skip_serializing_if = "Option::is_none"
83 )]
84 pub expected_status: Option<Vec<u16>>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub headers: Option<BTreeMap<String, String>>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub body: Option<String>,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct TcpCheck {
96 pub host: String,
98 pub port: u16,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct PingCheck {
105 pub address: String,
107}
108
109#[derive(Debug, Clone, Serialize)]
114pub struct NewHealthCheck {
115 pub record_id: Uuid,
117 #[serde(flatten)]
119 pub config: HealthCheckConfig,
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub name: Option<String>,
123 pub interval: u32,
125 pub timeout: u32,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub retries: Option<u32>,
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub retry_interval: Option<u32>,
133}
134
135#[derive(Debug, Clone, Default, Serialize)]
140pub struct HealthCheckPatch {
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub name: Option<String>,
144 #[serde(skip_serializing_if = "Option::is_none")]
147 pub config: Option<serde_json::Value>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub interval: Option<u32>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub timeout: Option<u32>,
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub retries: Option<u32>,
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub retry_interval: Option<u32>,
160}
161
162impl HealthCheckPatch {
163 pub fn is_empty(&self) -> bool {
165 self.name.is_none()
166 && self.config.is_none()
167 && self.interval.is_none()
168 && self.timeout.is_none()
169 && self.retries.is_none()
170 && self.retry_interval.is_none()
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn new_check_wire_shape() {
180 let check = NewHealthCheck {
181 record_id: Uuid::nil(),
182 config: HealthCheckConfig::Tcp(TcpCheck {
183 host: "192.0.2.1".into(),
184 port: 443,
185 }),
186 name: None,
187 interval: 60,
188 timeout: 5,
189 retries: None,
190 retry_interval: None,
191 };
192 assert_eq!(
193 serde_json::to_value(&check).unwrap(),
194 serde_json::json!({
195 "record_id": "00000000-0000-0000-0000-000000000000",
196 "type": "tcp",
197 "config": { "host": "192.0.2.1", "port": 443 },
198 "interval": 60,
199 "timeout": 5,
200 })
201 );
202 }
203
204 #[test]
205 fn typed_config_from_response() {
206 let check: HealthCheck = serde_json::from_value(serde_json::json!({
207 "record_id": "00000000-0000-0000-0000-000000000000",
208 "type": "http",
209 "config": { "url": "https://example.com/health", "expectedStatus": [200, 204] },
210 "interval": 60, "timeout": 5,
211 "last_status": "UNKNOWN", "last_checked_at": null, "last_error": null,
212 "created_at": 1, "updated_at": 1
213 }))
214 .unwrap();
215 let Some(HealthCheckConfig::Http(http)) = check.typed_config() else {
216 panic!("expected http config")
217 };
218 assert_eq!(http.expected_status, Some(vec![200, 204]));
219 }
220}