#[derive(Default)]
pub(crate) struct SseDecoder {
buffer: String,
}
impl SseDecoder {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn push(&mut self, chunk: &str) -> Vec<String> {
self.buffer.push_str(chunk);
let mut payloads = Vec::new();
while let Some(newline) = self.buffer.find('\n') {
let line = self.buffer[..newline].to_string();
self.buffer.drain(..=newline);
if let Some(data) = data_field(&line)
&& !data.is_empty()
{
payloads.push(data.to_string());
}
}
payloads
}
}
fn data_field(line: &str) -> Option<&str> {
line.trim_end_matches('\r')
.strip_prefix("data:")
.map(str::trim_start)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn yields_data_payloads_and_skips_everything_else() {
let mut decoder = SseDecoder::new();
let payloads = decoder.push(concat!(
"event: message_start\n",
"data: {\"a\":1}\n",
": a comment\n",
"id: 7\n",
"\n",
"data:{\"b\":2}\n",
));
assert_eq!(payloads, vec!["{\"a\":1}", "{\"b\":2}"]);
}
#[test]
fn holds_a_partial_line_until_the_rest_arrives() {
let mut decoder = SseDecoder::new();
assert!(decoder.push("data: {\"a\":").is_empty());
assert!(decoder.push("1").is_empty());
assert_eq!(decoder.push("}\n"), vec!["{\"a\":1}"]);
}
#[test]
fn a_line_split_across_chunks_mid_field_still_decodes() {
let mut decoder = SseDecoder::new();
assert!(decoder.push("dat").is_empty());
assert_eq!(decoder.push("a: {\"x\":true}\n"), vec!["{\"x\":true}"]);
}
#[test]
fn the_terminator_is_passed_through_for_the_accumulator_to_recognize() {
let mut decoder = SseDecoder::new();
assert_eq!(decoder.push("data: [DONE]\n"), vec!["[DONE]"]);
assert!(decoder.push("data: \n").is_empty());
}
#[test]
fn carriage_returns_are_tolerated() {
let mut decoder = SseDecoder::new();
assert_eq!(decoder.push("data: {\"a\":1}\r\n"), vec!["{\"a\":1}"]);
}
}