synaps_cli/events/
format.rs1use super::types::Event;
2
3fn regex_strip_event_close(s: &str) -> String {
5 let mut result = String::with_capacity(s.len());
6 let chars: Vec<char> = s.chars().collect();
7 let lower_chars: Vec<char> = s.to_lowercase().chars().collect();
8 let mut i = 0;
9 while i < chars.len() {
10 if i + 7 < chars.len() && lower_chars[i] == '<' && lower_chars[i + 1] == '/' {
12 let mut j = i + 2;
14 while j < chars.len() && lower_chars[j] == ' ' { j += 1; }
15 if j + 5 <= chars.len()
16 && lower_chars[j] == 'e'
17 && lower_chars[j + 1] == 'v'
18 && lower_chars[j + 2] == 'e'
19 && lower_chars[j + 3] == 'n'
20 && lower_chars[j + 4] == 't'
21 {
22 let mut k = j + 5;
23 while k < chars.len() && lower_chars[k] == ' ' { k += 1; }
24 if k < chars.len() && chars[k] == '>' {
25 i = k + 1; continue;
27 }
28 }
29 }
30 result.push(chars[i]);
31 i += 1;
32 }
33 result
34}
35
36pub fn format_event_for_agent(event: &Event) -> String {
41 let sev = event
42 .content
43 .severity
44 .as_ref()
45 .map(|s| s.as_str())
46 .unwrap_or("medium");
47
48 let channel_attr = match &event.channel {
49 Some(ch) => format!(" channel=\"{}\"", ch.name.replace('"', "'")),
50 None => String::new(),
51 };
52
53 let safe_text = regex_strip_event_close(&event.content.text);
55 let safe_source = event.source.source_type.replace('"', "'");
56 let safe_content_type = event.content.content_type.replace('"', "'");
57
58 let mut out = format!(
59 "<event id=\"{}\" type=\"{}\" severity=\"{}\" source=\"{}\"{}>{}",
60 event.id, safe_content_type, sev, safe_source, channel_attr, safe_text
61 );
62
63 if let Some(data) = &event.content.data {
64 let data_str = serde_json::to_string(data).unwrap_or_default();
65 let truncated: String = data_str.chars().take(1000).collect();
67 let safe_data = regex_strip_event_close(&truncated);
69 out.push_str(&format!("\nData: {}", safe_data));
70 }
71
72 out.push_str("</event>");
73 out
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::events::types::{EventChannel, Severity};
80 use serde_json::json;
81
82 #[test]
83 fn format_without_channel() {
84 let e = Event::simple("cli", "ping", Some(Severity::Low));
85 let s = format_event_for_agent(&e);
86 assert!(s.starts_with("<event id="));
87 assert!(s.contains("type=\"message\""));
88 assert!(s.contains("severity=\"low\""));
89 assert!(s.contains("source=\"cli\""));
90 assert!(s.contains("ping"));
91 assert!(s.ends_with("</event>"));
92 }
93
94 #[test]
95 fn format_with_channel() {
96 let mut e = Event::simple("uptime-kuma", "Jellyfin is DOWN. Status 503.", Some(Severity::High));
97 e.content.content_type = "alert".into();
98 e.channel = Some(EventChannel {
99 id: "1".into(),
100 name: "alerts".into(),
101 });
102 let s = format_event_for_agent(&e);
103 assert!(s.contains("source=\"uptime-kuma\""));
104 assert!(s.contains("channel=\"alerts\""));
105 assert!(s.contains("severity=\"high\""));
106 assert!(s.contains("Jellyfin is DOWN. Status 503."));
107 assert!(s.ends_with("</event>"));
108 }
109
110 #[test]
111 fn format_defaults_to_medium_when_no_severity() {
112 let e = Event::simple("cli", "hi", None);
113 let s = format_event_for_agent(&e);
114 assert!(s.contains("severity=\"medium\""));
115 }
116
117 #[test]
118 fn format_appends_data() {
119 let mut e = Event::simple("system", "boom", Some(Severity::Critical));
120 e.content.data = Some(json!({"code": 500}));
121 let s = format_event_for_agent(&e);
122 assert!(s.contains("boom"));
123 assert!(s.contains("\nData: "));
124 assert!(s.contains("\"code\":500"));
125 }
126}