#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SseEvent {
pub event: Option<String>,
pub data: String,
}
#[derive(Default)]
pub struct SseParser {
buffer: Vec<u8>,
event: Option<String>,
data: Vec<String>,
}
impl SseParser {
pub fn push(&mut self, chunk: &[u8], out: &mut Vec<SseEvent>) {
self.buffer.extend_from_slice(chunk);
while let Some(position) = self.buffer.iter().position(|byte| *byte == b'\n') {
let mut line: Vec<u8> = self.buffer.drain(..=position).collect();
line.pop();
if line.last() == Some(&b'\r') {
line.pop();
}
self.line(&String::from_utf8_lossy(&line), out);
}
}
fn line(&mut self, line: &str, out: &mut Vec<SseEvent>) {
if line.is_empty() {
if self.event.is_some() || !self.data.is_empty() {
out.push(SseEvent { event: self.event.take(), data: std::mem::take(&mut self.data).join("\n") });
}
return;
}
if line.starts_with(':') {
return;
}
let (field, value) =
line.split_once(':').map_or((line, ""), |(field, value)| (field, value.strip_prefix(' ').unwrap_or(value)));
match field {
"event" => self.event = Some(value.to_owned()),
"data" => self.data.push(value.to_owned()),
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(chunks: &[&[u8]]) -> Vec<SseEvent> {
let (mut parser, mut out) = (SseParser::default(), Vec::new());
for chunk in chunks {
parser.push(chunk, &mut out);
}
out
}
#[test]
fn evento_con_nombre_y_datos_multilinea() {
let events = parse(&[b"event: delta\ndata: uno\ndata: dos\n\n"]);
assert_eq!(events, [SseEvent { event: Some("delta".into()), data: "uno\ndos".into() }]);
}
#[test]
fn trozos_partidos_en_cualquier_punto() {
let whole = "event: a\ndata: {\"t\":\"ñandú\"}\n\ndata: [DONE]\n\n".as_bytes();
let expected = parse(&[whole]);
for split in 1..whole.len() {
assert_eq!(parse(&[&whole[..split], &whole[split..]]), expected, "corte en {split}");
}
assert_eq!(expected.len(), 2);
assert_eq!(expected[0].data, "{\"t\":\"ñandú\"}");
}
#[test]
fn ignora_comentarios_y_acepta_crlf() {
let events = parse(&[b": ping\r\ndata: x\r\n\r\n"]);
assert_eq!(events, [SseEvent { event: None, data: "x".into() }]);
}
#[test]
fn sin_linea_en_blanco_el_evento_queda_pendiente() {
assert!(parse(&[b"data: incompleto\n"]).is_empty());
}
}