1use serde::{Deserialize, Serialize};
4
5use crate::ports::model_runtime::{ModelRuntimeError, RuntimeErrorEnvelope};
6
7use super::AppEvent;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct ServerSummary {
16 pub id: String,
18 pub model_id: String,
20 pub model_name: String,
22 pub port: u16,
24 pub healthy: Option<bool>,
26}
27
28impl ServerSummary {
29 pub fn parsed_model_id(&self) -> Option<u32> {
35 self.model_id.parse::<u32>().ok()
36 }
37}
38
39pub trait ServerEvents: Send + Sync {
78 fn started(&self, server: &ServerSummary);
80
81 fn stopping(&self, server: &ServerSummary);
83
84 fn stopped(&self, server: &ServerSummary);
86
87 fn snapshot(&self, servers: &[ServerSummary]);
89
90 fn error(&self, server: &ServerSummary, error: &ModelRuntimeError);
92}
93
94#[derive(Debug, Clone, Copy, Default)]
99pub struct NoopServerEvents;
100
101impl ServerEvents for NoopServerEvents {
102 fn started(&self, _server: &ServerSummary) {}
103 fn stopping(&self, _server: &ServerSummary) {}
104 fn stopped(&self, _server: &ServerSummary) {}
105 fn snapshot(&self, _servers: &[ServerSummary]) {}
106 fn error(&self, _server: &ServerSummary, _error: &ModelRuntimeError) {}
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct ServerSnapshotEntry {
113 pub model_id: i64,
115 pub model_name: String,
117 pub port: u16,
119 pub started_at: u64,
121 pub healthy: bool,
123}
124
125impl AppEvent {
126 pub fn server_started(model_id: i64, model_name: impl Into<String>, port: u16) -> Self {
128 Self::ServerStarted {
129 model_id,
130 model_name: model_name.into(),
131 port,
132 }
133 }
134
135 pub fn server_stopped(model_id: i64, model_name: impl Into<String>) -> Self {
137 Self::ServerStopped {
138 model_id,
139 model_name: model_name.into(),
140 }
141 }
142
143 pub fn server_error(
145 model_id: Option<i64>,
146 model_name: impl Into<String>,
147 error: RuntimeErrorEnvelope,
148 ) -> Self {
149 Self::ServerError {
150 model_id,
151 model_name: model_name.into(),
152 error,
153 }
154 }
155
156 pub const fn server_snapshot(servers: Vec<ServerSnapshotEntry>) -> Self {
158 Self::ServerSnapshot { servers }
159 }
160
161 pub fn from_server_started(server: &ServerSummary) -> Self {
163 let model_id = server.model_id.parse::<i64>().unwrap_or(0);
164 Self::server_started(model_id, &server.model_name, server.port)
165 }
166
167 pub fn from_server_stopped(server: &ServerSummary) -> Self {
169 let model_id = server.model_id.parse::<i64>().unwrap_or(0);
170 Self::server_stopped(model_id, &server.model_name)
171 }
172
173 pub fn from_server_error(server: &ServerSummary, error: RuntimeErrorEnvelope) -> Self {
175 let model_id = server.model_id.parse::<i64>().ok();
176 Self::server_error(model_id, &server.model_name, error)
177 }
178
179 pub fn from_server_snapshot(servers: &[ServerSummary]) -> Self {
181 let started_at = std::time::SystemTime::now()
182 .duration_since(std::time::UNIX_EPOCH)
183 .unwrap()
184 .as_secs();
185 let entries: Vec<ServerSnapshotEntry> = servers
186 .iter()
187 .map(|s| ServerSnapshotEntry {
188 model_id: s.model_id.parse::<i64>().unwrap_or(0),
189 model_name: s.model_name.clone(),
190 port: s.port,
191 started_at,
192 healthy: s.healthy.unwrap_or(false),
193 })
194 .collect();
195 Self::server_snapshot(entries)
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 fn make_server(id: &str, model_id: &str, name: &str, port: u16) -> ServerSummary {
204 ServerSummary {
205 id: id.to_string(),
206 model_id: model_id.to_string(),
207 model_name: name.to_string(),
208 port,
209 healthy: Some(true),
210 }
211 }
212
213 #[test]
214 fn test_from_server_started() {
215 let server = make_server("srv-1", "42", "test-model", 8080);
216 let event = AppEvent::from_server_started(&server);
217 match event {
218 AppEvent::ServerStarted {
219 model_id,
220 model_name,
221 port,
222 } => {
223 assert_eq!(model_id, 42);
224 assert_eq!(model_name, "test-model");
225 assert_eq!(port, 8080);
226 }
227 _ => panic!("expected ServerStarted"),
228 }
229 }
230
231 #[test]
232 fn test_from_server_stopped() {
233 let server = make_server("srv-1", "42", "test-model", 8080);
234 let event = AppEvent::from_server_stopped(&server);
235 match event {
236 AppEvent::ServerStopped {
237 model_id,
238 model_name,
239 } => {
240 assert_eq!(model_id, 42);
241 assert_eq!(model_name, "test-model");
242 }
243 _ => panic!("expected ServerStopped"),
244 }
245 }
246
247 #[test]
248 fn test_from_server_error() {
249 let server = make_server("srv-1", "42", "test-model", 8080);
250 let runtime_err = ModelRuntimeError::Internal("something failed".to_string());
251 let event = AppEvent::from_server_error(&server, RuntimeErrorEnvelope::from(&runtime_err));
252 match event {
253 AppEvent::ServerError {
254 model_id,
255 model_name,
256 error,
257 } => {
258 assert_eq!(model_id, Some(42));
259 assert_eq!(model_name, "test-model");
260 assert_eq!(error.message, "Internal error: something failed");
261 assert_eq!(error.r#type, "server_error");
262 assert!(!error.retryable);
263 }
264 _ => panic!("expected ServerError"),
265 }
266 }
267
268 #[test]
269 fn test_from_server_error_invalid_model_id() {
270 let server = make_server("srv-1", "abc", "test-model", 8080);
271 let runtime_err = ModelRuntimeError::Internal("something failed".to_string());
272 let event = AppEvent::from_server_error(&server, RuntimeErrorEnvelope::from(&runtime_err));
273 match event {
274 AppEvent::ServerError {
275 model_id,
276 model_name,
277 error,
278 } => {
279 assert_eq!(model_id, None);
280 assert_eq!(model_name, "test-model");
281 assert_eq!(error.message, "Internal error: something failed");
282 }
283 _ => panic!("expected ServerError"),
284 }
285 }
286
287 #[test]
288 fn test_from_server_snapshot() {
289 let servers = vec![
290 make_server("srv-a", "1", "model-a", 9001),
291 make_server("srv-b", "2", "model-b", 9002),
292 ];
293 let event = AppEvent::from_server_snapshot(&servers);
294 match event {
295 AppEvent::ServerSnapshot { servers: entries } => {
296 assert_eq!(entries.len(), 2);
297 assert_eq!(entries[0].model_id, 1);
298 assert_eq!(entries[0].port, 9001);
299 assert_eq!(entries[1].model_id, 2);
300 assert_eq!(entries[1].port, 9002);
301 }
302 _ => panic!("expected ServerSnapshot"),
303 }
304 }
305}