Skip to main content

zai_rs/model/
sse_parser.rs

1//! Shared SSE (Server-Sent Events) line parsing utilities.
2//!
3//! Extracts the common logic of buffering raw byte chunks, splitting on `\n`,
4//! trimming `\r\n`, and yielding `data: ` prefixed payload lines.
5
6/// Incremental SSE event parser.
7///
8/// Unlike [`extract_sse_data_lines`], this parser follows event boundaries and
9/// joins multiple `data:` lines in the same event with `\n`.
10#[derive(Debug, Default)]
11pub struct SseEventParser {
12    buf: Vec<u8>,
13    event_data: Vec<Vec<u8>>,
14}
15
16impl SseEventParser {
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Push a transport byte chunk and return completed SSE event payloads.
22    pub fn push(&mut self, new_bytes: &[u8]) -> Vec<Vec<u8>> {
23        self.buf.extend_from_slice(new_bytes);
24        let mut events = Vec::new();
25
26        while let Some(newline_index) = self.buf.iter().position(|&b| b == b'\n') {
27            let mut line = self.buf.drain(..=newline_index).collect::<Vec<_>>();
28            if line.ends_with(b"\n") {
29                line.pop();
30            }
31            if line.ends_with(b"\r") {
32                line.pop();
33            }
34
35            if line.is_empty() {
36                if !self.event_data.is_empty() {
37                    events.push(join_event_data(&self.event_data));
38                    self.event_data.clear();
39                }
40                continue;
41            }
42
43            if line.starts_with(b":") {
44                continue;
45            }
46
47            if let Some(rest) = line.strip_prefix(b"data:") {
48                self.event_data.push(trim_one_leading_space(rest).to_vec());
49            }
50        }
51
52        events
53    }
54}
55
56fn trim_one_leading_space(bytes: &[u8]) -> &[u8] {
57    bytes.strip_prefix(b" ").unwrap_or(bytes)
58}
59
60fn join_event_data(lines: &[Vec<u8>]) -> Vec<u8> {
61    let mut event = Vec::new();
62    for (idx, line) in lines.iter().enumerate() {
63        if idx > 0 {
64            event.push(b'\n');
65        }
66        event.extend_from_slice(line);
67    }
68    event
69}
70
71/// Process a new chunk of bytes, extract completed SSE data lines.
72///
73/// Appends `new_bytes` to `buf`, then extracts all complete lines (delimited
74/// by `\n`). For each line, it:
75/// - Strips trailing `\r` and `\n`
76/// - Skips empty lines
77/// - Strips the `"data: "` prefix and yields the remaining bytes
78///
79/// Returns a vector of data payload slices (borrowed from `buf`).
80/// Lines that are not prefixed with `"data: "` are silently skipped.
81///
82/// If a `data: [DONE]` line is encountered, it is yielded as a
83/// `[b"[DONE]"]` entry so the caller can detect stream termination.
84pub fn extract_sse_data_lines(buf: &mut Vec<u8>, new_bytes: &[u8]) -> Vec<Vec<u8>> {
85    buf.extend_from_slice(new_bytes);
86    let mut results = Vec::new();
87
88    let Some(last_newline) = buf.iter().rposition(|&b| b == b'\n') else {
89        return results;
90    };
91
92    let completed = &buf[..=last_newline];
93    for line_with_nl in completed.split_inclusive(|&b| b == b'\n') {
94        let mut line = line_with_nl;
95        if let Some(line_without_nl) = line.strip_suffix(b"\n") {
96            line = line_without_nl;
97        }
98        if let Some(line_without_cr) = line.strip_suffix(b"\r") {
99            line = line_without_cr;
100        }
101        if line.is_empty() {
102            continue;
103        }
104        const PREFIX: &[u8] = b"data: ";
105        if let Some(rest) = line.strip_prefix(PREFIX) {
106            results.push(rest.to_vec());
107        }
108    }
109
110    buf.drain(..=last_newline);
111
112    results
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_single_complete_line() {
121        let mut buf = Vec::new();
122        let lines = extract_sse_data_lines(&mut buf, b"data: hello\n");
123        assert_eq!(lines.len(), 1);
124        assert_eq!(lines[0], b"hello");
125    }
126
127    #[test]
128    fn test_partial_then_complete() {
129        let mut buf = Vec::new();
130        let lines1 = extract_sse_data_lines(&mut buf, b"data: hel");
131        assert!(lines1.is_empty());
132
133        let lines2 = extract_sse_data_lines(&mut buf, b"lo\n");
134        assert_eq!(lines2.len(), 1);
135        assert_eq!(lines2[0], b"hello");
136    }
137
138    #[test]
139    fn test_crlf_line_endings() {
140        let mut buf = Vec::new();
141        let lines = extract_sse_data_lines(&mut buf, b"data: world\r\n");
142        assert_eq!(lines.len(), 1);
143        assert_eq!(lines[0], b"world");
144    }
145
146    #[test]
147    fn test_multiple_events_in_one_chunk() {
148        let mut buf = Vec::new();
149        let lines = extract_sse_data_lines(&mut buf, b"data: first\n\ndata: second\n");
150        assert_eq!(lines.len(), 2);
151        assert_eq!(lines[0], b"first");
152        assert_eq!(lines[1], b"second");
153    }
154
155    #[test]
156    fn test_done_marker() {
157        let mut buf = Vec::new();
158        let lines = extract_sse_data_lines(&mut buf, b"data: [DONE]\n");
159        assert_eq!(lines.len(), 1);
160        assert_eq!(lines[0], b"[DONE]");
161    }
162
163    #[test]
164    fn test_non_data_lines_skipped() {
165        let mut buf = Vec::new();
166        let lines = extract_sse_data_lines(&mut buf, b": comment\nid: 123\ndata: payload\n");
167        assert_eq!(lines.len(), 1);
168        assert_eq!(lines[0], b"payload");
169    }
170
171    #[test]
172    fn test_empty_lines_ignored() {
173        let mut buf = Vec::new();
174        let lines = extract_sse_data_lines(&mut buf, b"\n\n\ndata: hello\n\n");
175        assert_eq!(lines.len(), 1);
176        assert_eq!(lines[0], b"hello");
177    }
178
179    #[test]
180    fn event_parser_yields_complete_events() {
181        let mut parser = SseEventParser::new();
182        assert!(parser.push(b"data: hel").is_empty());
183        assert_eq!(parser.push(b"lo\r\n\r\n"), vec![b"hello".to_vec()]);
184    }
185
186    #[test]
187    fn event_parser_joins_multi_data_lines() {
188        let mut parser = SseEventParser::new();
189        let events = parser.push(b"data: {\"a\":\ndata: 1}\n\n");
190        assert_eq!(events, vec![b"{\"a\":\n1}".to_vec()]);
191    }
192
193    #[test]
194    fn event_parser_ignores_comments_and_non_data_fields() {
195        let mut parser = SseEventParser::new();
196        let events = parser.push(b": keepalive\nid: 1\ndata: payload\n\n");
197        assert_eq!(events, vec![b"payload".to_vec()]);
198    }
199}