diff --git a/crates/switchyard-translation/src/sse.rs b/crates/switchyard-translation/src/sse.rs index dce5a3611..439df5d45 100644 --- a/crates/switchyard-translation/src/sse.rs +++ b/crates/switchyard-translation/src/sse.rs @@ -29,6 +29,16 @@ pub(crate) fn done_marker(_format: WireFormat) -> Option<&'static str> { Some("[DONE]") } +/// Value of a `data` field line; the space after the colon is optional framing. +fn data_field_value(line: &str) -> Option { + let value = match line.split_once(':') { + Some(("data", value)) => value, + None if line == "data" => "", + _ => return None, + }; + Some(value.strip_prefix(' ').unwrap_or(value).to_string()) +} + pub(crate) fn parse_json_sse_frame( frame: &str, done_marker: Option<&str>, @@ -36,7 +46,7 @@ pub(crate) fn parse_json_sse_frame( let data = frame .lines() .filter(|line| !line.is_empty() && !line.starts_with(':')) - .filter_map(|line| line.strip_prefix("data: ").map(|l| l.to_string())) + .filter_map(data_field_value) .fold(String::new(), |mut a, b| { a.reserve(b.len() + 1); a.push_str(&b); @@ -83,6 +93,35 @@ mod tests { Ok(()) } + #[test] + fn parses_a_data_line_without_a_space_after_the_colon() -> Result<(), BoxError> { + // The space after `data:` is optional framing, not part of the value. + let SseFrame::Data(value) = parse_json_sse_frame("data:{\"text\":\"hi\"}\n", DONE)? else { + return Err("expected a payload".into()); + }; + assert_eq!(value, json!({"text": "hi"})); + Ok(()) + } + + #[test] + fn done_marker_is_recognised_without_a_space() -> Result<(), BoxError> { + assert!(matches!( + parse_json_sse_frame("data:[DONE]\n", DONE)?, + SseFrame::Done + )); + Ok(()) + } + + #[test] + fn field_names_are_matched_exactly() -> Result<(), BoxError> { + // `database:` must not be read as a `data` field. + assert!(matches!( + parse_json_sse_frame("database: {\"n\":1}\n", DONE)?, + SseFrame::Empty + )); + Ok(()) + } + #[test] fn done_marker_yields_no_payload() -> Result<(), BoxError> { assert!(matches!(