aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Analizador de Server-Sent Events. Recibe bytes en trozos de cualquier tamaño (incluso partidos a mitad de
//! línea o de carácter) y entrega cada evento completo.

#[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 {
    /// Añade bytes y deja en `out` los eventos que quedaron completos.
    pub fn push(&mut self, chunk: &[u8], out: &mut Vec<SseEvent>) {
        self.buffer.extend_from_slice(chunk);
        // Se corta solo en `\n`, que nunca aparece dentro de un carácter UTF-8 multibyte.
        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());
    }
}