uptrakit_openapi_client/
sse.rs1#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct RawSseEvent {
12 pub event_type: String,
14 pub data: String,
16 pub id: Option<String>,
18}
19
20#[derive(Debug, thiserror::Error)]
22pub enum SseError {
23 #[error("stream read error: {0}")]
24 Transport(#[from] reqwest::Error),
25}
26
27pub fn parse_sse_stream(
35 response: reqwest::Response,
36) -> impl futures_util::Stream<Item = Result<RawSseEvent, SseError>> {
37 futures_util::stream::unfold(
38 (response, String::new()),
39 |(mut response, mut buffer)| async move {
40 loop {
41 if let Some(pos) = find_event_boundary(&buffer) {
43 let event_text = buffer[..pos].to_string();
44 let skip = if buffer[pos..].starts_with("\r\n\r\n") {
46 4
47 } else if buffer[pos..].starts_with("\n\n") {
48 2
49 } else {
50 2
52 };
53 buffer = buffer[pos + skip..].to_string();
54
55 if let Some(event) = parse_event(&event_text) {
56 return Some((Ok(event), (response, buffer)));
57 }
58 continue;
60 }
61
62 match response.chunk().await {
64 Ok(Some(bytes)) => {
65 let text = String::from_utf8_lossy(&bytes);
66 buffer.push_str(&text);
67 }
68 Ok(None) => {
69 if !buffer.trim().is_empty() {
71 let event_text = std::mem::take(&mut buffer);
72 if let Some(event) = parse_event(&event_text) {
73 return Some((Ok(event), (response, buffer)));
74 }
75 }
76 return None;
77 }
78 Err(e) => {
79 return Some((Err(SseError::Transport(e)), (response, buffer)));
80 }
81 }
82 }
83 },
84 )
85}
86
87fn find_event_boundary(s: &str) -> Option<usize> {
89 if let Some(pos) = s.find("\r\n\r\n") {
91 if let Some(nn_pos) = s.find("\n\n") {
93 return Some(nn_pos.min(pos));
94 }
95 return Some(pos);
96 }
97 if let Some(pos) = s.find("\n\n") {
98 return Some(pos);
99 }
100 s.find("\r\r")
101}
102
103fn parse_event(text: &str) -> Option<RawSseEvent> {
106 let mut event_type = None;
107 let mut data_parts: Vec<&str> = Vec::new();
108 let mut id = None;
109
110 for line in text.lines() {
111 if line.starts_with(':') {
112 continue;
114 }
115
116 if let Some(value) = line.strip_prefix("event:") {
117 event_type = Some(value.trim().to_string());
118 } else if let Some(value) = line.strip_prefix("data:") {
119 data_parts.push(value.strip_prefix(' ').unwrap_or(value));
120 } else if let Some(value) = line.strip_prefix("id:") {
121 id = Some(value.trim().to_string());
122 }
123 }
125
126 if data_parts.is_empty() {
127 return None;
128 }
129
130 Some(RawSseEvent {
131 event_type: event_type.unwrap_or_else(|| "message".to_string()),
132 data: data_parts.join("\n"),
133 id,
134 })
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn parse_single_event() {
143 let text = "event: output\ndata: hello world";
144 let event = parse_event(text).expect("should parse");
145 assert_eq!(event.event_type, "output");
146 assert_eq!(event.data, "hello world");
147 assert_eq!(event.id, None);
148 }
149
150 #[test]
151 fn parse_event_default_type() {
152 let text = "data: just data";
153 let event = parse_event(text).expect("should parse");
154 assert_eq!(event.event_type, "message");
155 assert_eq!(event.data, "just data");
156 }
157
158 #[test]
159 fn parse_event_with_id() {
160 let text = "event: completed\ndata: {\"status\":\"done\"}\nid: 42";
161 let event = parse_event(text).expect("should parse");
162 assert_eq!(event.event_type, "completed");
163 assert_eq!(event.data, "{\"status\":\"done\"}");
164 assert_eq!(event.id.as_deref(), Some("42"));
165 }
166
167 #[test]
168 fn parse_event_multi_data_lines() {
169 let text = "data: line1\ndata: line2\ndata: line3";
170 let event = parse_event(text).expect("should parse");
171 assert_eq!(event.data, "line1\nline2\nline3");
172 }
173
174 #[test]
175 fn parse_event_skips_comments() {
176 let text = ": this is a comment\nevent: output\ndata: payload";
177 let event = parse_event(text).expect("should parse");
178 assert_eq!(event.event_type, "output");
179 assert_eq!(event.data, "payload");
180 }
181
182 #[test]
183 fn parse_event_no_data_returns_none() {
184 let text = "event: output\n: comment only";
185 assert!(parse_event(text).is_none());
186 }
187
188 #[test]
189 fn find_boundary_double_newline() {
190 assert_eq!(find_event_boundary("data: hello\n\ndata: world"), Some(11));
191 }
192
193 #[test]
194 fn find_boundary_crlf() {
195 assert_eq!(
196 find_event_boundary("data: hello\r\n\r\ndata: world"),
197 Some(11)
198 );
199 }
200
201 #[test]
202 fn find_boundary_none() {
203 assert_eq!(find_event_boundary("data: hello\n"), None);
204 }
205}