use bytes::{Bytes, BytesMut};
use std::collections::HashMap;
use super::SseEvent;
const BUFFER_CAPACITY: usize = 1024;
#[derive(Debug)]
pub struct SseParser {
pub buffer: BytesMut,
}
impl SseParser {
pub fn new() -> Self {
Self {
buffer: BytesMut::with_capacity(BUFFER_CAPACITY),
}
}
pub fn process_new_chunk(&mut self, bytes: Bytes) -> Vec<SseEvent> {
self.buffer.extend_from_slice(&bytes);
let mut lines = Vec::new();
while let Some(pos) = self.buffer.iter().position(|&b| b == b'\n') {
let line = self.buffer.split_to(pos + 1).freeze();
lines.push(line);
}
let mut events = Vec::new();
let mut current_message_lines: Vec<Bytes> = Vec::new();
for line in lines {
current_message_lines.push(line);
if current_message_lines.len() >= 2
&& current_message_lines
.last()
.is_some_and(|b| b.as_ref() == b"\n")
{
let message_lines: Vec<_> = current_message_lines
.drain(..current_message_lines.len() - 1)
.filter(|l| l.as_ref() != b"\n") .collect();
if let Some(event) = self.parse_sse_message(&message_lines) {
events.push(event);
}
}
}
if !current_message_lines.is_empty() {
let mut new_buf = BytesMut::new();
for line in current_message_lines.into_iter() {
new_buf.extend_from_slice(&line);
}
new_buf.extend_from_slice(&self.buffer);
self.buffer = new_buf;
}
events
}
fn parse_sse_message(&self, lines: &[Bytes]) -> Option<SseEvent> {
let mut fields: HashMap<String, String> = HashMap::new();
let mut data_parts: Vec<String> = Vec::new();
let mut saw_data_line = false;
for line_bytes in lines {
let line_str = String::from_utf8_lossy(line_bytes);
if line_str.is_empty() || line_str.starts_with(':') {
continue;
}
let (key, value) = if let Some(value) = line_str.strip_prefix("data: ") {
("data", value.to_string())
} else if line_str.trim_end_matches('\n') == "data:"
|| line_str.trim_end_matches('\n').is_empty() && line_str.starts_with("data:")
{
("data", String::new())
} else if let Some(value) = line_str.strip_prefix("event: ") {
("event", value.trim().to_string())
} else if let Some(value) = line_str.strip_prefix("id: ") {
("id", value.trim().to_string())
} else if let Some(value) = line_str.strip_prefix("retry: ") {
("retry", value.trim().to_string())
} else {
continue;
};
if key == "data" {
saw_data_line = true;
if !value.is_empty() && value != "\n" {
data_parts.push(value);
}
} else {
fields.insert(key.to_string(), value);
}
}
let data = if data_parts.is_empty() {
None
} else {
let full_data = data_parts.join("\n");
Some(Bytes::copy_from_slice(full_data.as_bytes())) };
let event = fields.get("event").cloned();
let id = fields.get("id").cloned();
let retry = fields
.get("retry")
.and_then(|r| r.trim().parse::<u64>().ok());
if data.is_none() && id.is_none() && retry.is_none() && event.is_none() && !saw_data_line {
return None;
}
Some(SseEvent {
event,
data,
id,
retry,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
#[test]
fn test_single_data_event() {
let mut parser = SseParser::new();
let input = Bytes::from("data: hello\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
assert_eq!(
events[0].data.as_deref(),
Some(Bytes::from("hello\n").as_ref())
);
assert!(events[0].event.is_none());
assert!(events[0].id.is_none());
}
#[test]
fn test_event_with_id_and_data() {
let mut parser = SseParser::new();
let input = Bytes::from("event: message\nid: 123\ndata: hello\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
assert_eq!(events[0].event.as_deref(), Some("message"));
assert_eq!(events[0].id.as_deref(), Some("123"));
assert_eq!(
events[0].data.as_deref(),
Some(Bytes::from("hello\n").as_ref())
);
}
#[test]
fn test_event_chunks_in_different_orders() {
let mut parser = SseParser::new();
let input = Bytes::from("data: hello\nevent: message\nid: 123\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
assert_eq!(events[0].event.as_deref(), Some("message"));
assert_eq!(events[0].id.as_deref(), Some("123"));
assert_eq!(
events[0].data.as_deref(),
Some(Bytes::from("hello\n").as_ref())
);
}
#[test]
fn test_comment_line_ignored() {
let mut parser = SseParser::new();
let input = Bytes::from(": this is a comment\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 0);
}
#[test]
fn test_event_with_empty_data() {
let mut parser = SseParser::new();
let input = Bytes::from("data:\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
assert!(events[0].data.is_none());
assert!(events[0].id.is_none());
assert!(events[0].retry.is_none());
}
#[test]
fn test_priming_event_with_id_and_retry() {
let mut parser = SseParser::new();
let input = Bytes::from("id: event-1\nretry: 500\ndata: \n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
assert_eq!(events[0].id.as_deref(), Some("event-1"));
assert_eq!(events[0].retry, Some(500));
assert!(events[0].data.is_none());
}
#[test]
fn test_partial_chunks() {
let mut parser = SseParser::new();
let part1 = Bytes::from("data: hello");
let part2 = Bytes::from(" world\n\n");
let events1 = parser.process_new_chunk(part1);
assert_eq!(events1.len(), 0);
let events2 = parser.process_new_chunk(part2);
assert_eq!(events2.len(), 1);
assert_eq!(
events2[0].data.as_deref(),
Some(Bytes::from("hello world\n").as_ref())
);
}
#[test]
fn test_data_line_across_chunks_with_preceding_event_line() {
let mut parser = SseParser::new();
let part1 = Bytes::from("event: message\ndata: ");
let part2 = Bytes::from("hello\n\n");
let events1 = parser.process_new_chunk(part1);
assert_eq!(events1.len(), 0);
let events2 = parser.process_new_chunk(part2);
assert_eq!(events2.len(), 1);
assert_eq!(events2[0].event.as_deref(), Some("message"));
assert_eq!(
events2[0].data.as_deref(),
Some(Bytes::from("hello\n").as_ref())
);
}
#[test]
fn test_data_line_across_multiple_chunks_with_preceding_event() {
let mut parser = SseParser::new();
let part1 = Bytes::from("event: message\ndata: alpha bra");
let part2 = Bytes::from("vo charlie del");
let part3 = Bytes::from("ta echo foxtrot\n\n");
let events1 = parser.process_new_chunk(part1);
assert_eq!(events1.len(), 0);
let events2 = parser.process_new_chunk(part2);
assert_eq!(events2.len(), 0);
let events3 = parser.process_new_chunk(part3);
assert_eq!(events3.len(), 1);
assert_eq!(events3[0].event.as_deref(), Some("message"));
assert_eq!(
events3[0].data.as_deref(),
Some(Bytes::from("alpha bravo charlie delta echo foxtrot\n").as_ref())
);
}
#[test]
fn test_multiple_events_split_across_chunks() {
let mut parser = SseParser::new();
let part1 = Bytes::from("data: one\n\ndata: t");
let part2 = Bytes::from("wo\n\n");
let events1 = parser.process_new_chunk(part1);
assert_eq!(events1.len(), 1);
assert_eq!(
events1[0].data.as_deref(),
Some(Bytes::from("one\n").as_ref())
);
let events2 = parser.process_new_chunk(part2);
assert_eq!(events2.len(), 1);
assert_eq!(
events2[0].data.as_deref(),
Some(Bytes::from("two\n").as_ref())
);
}
#[test]
fn test_malformed_lines() {
let mut parser = SseParser::new();
let input = Bytes::from("something invalid\ndata: ok\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
assert_eq!(
events[0].data.as_deref(),
Some(Bytes::from("ok\n").as_ref())
);
}
#[test]
fn test_multiple_events_in_one_chunk() {
let mut parser = SseParser::new();
let input = Bytes::from("data: first\n\ndata: second\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 2);
assert_eq!(
events[0].data.as_deref(),
Some(Bytes::from("first\n").as_ref())
);
assert_eq!(
events[1].data.as_deref(),
Some(Bytes::from("second\n").as_ref())
);
}
#[test]
fn test_basic_sse_event() {
let mut parser = SseParser::new();
let input = Bytes::from("event: message\ndata: Hello\nid: 1\nretry: 5000\n\n");
let events = parser.process_new_chunk(input);
assert_eq!(events.len(), 1);
let event = &events[0];
assert_eq!(event.event.as_deref(), Some("message"));
assert_eq!(event.data.as_deref(), Some(Bytes::from("Hello\n").as_ref()));
assert_eq!(event.id.as_deref(), Some("1"));
assert_eq!(event.retry, Some(5000));
}
}