use super::sse_frame::SseFrame;
pub struct SseParser {
buffer: String,
frame_event: Option<String>,
frame_data: Option<String>,
}
impl SseParser {
pub fn new() -> Self {
Self {
buffer: String::new(),
frame_event: None,
frame_data: None,
}
}
pub fn feed(&mut self, chunk: &[u8]) -> Vec<SseFrame> {
let text = String::from_utf8_lossy(chunk);
self.buffer.push_str(&text);
let buffer = std::mem::take(&mut self.buffer);
let mut lines: Vec<String> = Vec::new();
let mut rest = buffer.as_str();
while let Some(pos) = rest.find('\n') {
lines.push(rest[..pos].to_string());
rest = &rest[pos + 1..];
}
self.buffer = rest.to_string();
self.parse_frames(lines)
}
fn parse_frames(&mut self, lines: Vec<String>) -> Vec<SseFrame> {
let mut frames = Vec::new();
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() {
if let Some(data) = self.frame_data.take() {
frames.push(SseFrame {
event: self.frame_event.take(),
data,
});
}
continue;
}
if let Some((key, value)) = Self::parse_sse_line(trimmed) {
match key {
"event" => {
self.frame_event = Some(value.to_string());
}
"data" => {
if value.is_empty() {
continue;
}
self.frame_data
.get_or_insert_with(String::new)
.push_str(value);
}
_ => {} }
}
}
frames
}
fn parse_sse_line(line: &str) -> Option<(&str, &str)> {
if let Some(pos) = line.find(':') {
let key = line[..pos].trim();
let value = line[pos + 1..].trim_start_matches(' ');
Some((key, value))
} else {
None
}
}
}
impl Default for SseParser {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_single_frame() {
let mut parser = SseParser::new();
let frames = parser.feed(b"event: message_delta\ndata: {\"text\": \"hello\"}\n\n");
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].event, Some("message_delta".into()));
assert_eq!(frames[0].data, "{\"text\": \"hello\"}");
}
#[test]
fn handles_split_across_chunks() {
let mut parser = SseParser::new();
let frames1 = parser.feed(b"event: delta\ndata: {\"t");
assert_eq!(frames1.len(), 0);
let frames2 = parser.feed(b": \"hello\"}\n\n");
assert_eq!(frames2.len(), 1);
assert_eq!(frames2[0].event, Some("delta".into()));
assert_eq!(frames2[0].data, "{\"t: \"hello\"}");
}
#[test]
fn multi_line_data() {
let mut parser = SseParser::new();
let frames = parser.feed(b"data: line1\ndata: line2\n\n");
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].data, "line1line2");
}
#[test]
fn data_only_no_event() {
let mut parser = SseParser::new();
let frames = parser.feed(b"data: [DONE]\n\n");
assert_eq!(frames.len(), 1);
assert_eq!(frames[0].event, None);
assert_eq!(frames[0].data, "[DONE]");
}
#[test]
fn multiple_frames_in_one_chunk() {
let mut parser = SseParser::new();
let frames = parser.feed(b"data: f1\n\ndata: f2\n\n");
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].data, "f1");
assert_eq!(frames[1].data, "f2");
}
}