#[derive(Debug, Clone)]
pub enum NdjsonResult {
ValidJson(String),
NoValidJson { tail_excerpt: String },
}
pub(crate) fn parse_ndjson(stdout: &str) -> NdjsonResult {
let mut last_valid_json: Option<String> = None;
for line in stdout.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
if let Ok(json_str) = serde_json::to_string(&value) {
last_valid_json = Some(json_str);
}
}
}
if let Some(json) = last_valid_json {
NdjsonResult::ValidJson(json)
} else {
let tail_excerpt = if stdout.len() <= 256 {
stdout.to_string()
} else {
let start = stdout.len() - 256;
stdout[start..].to_string()
};
NdjsonResult::NoValidJson { tail_excerpt }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ndjson_single_valid_json() {
let stdout = r#"{"status": "success", "result": "done"}"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["status"], "success");
assert_eq!(parsed["result"], "done");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_multiple_valid_json_returns_last() {
let stdout = r#"{"frame": 1}
{"frame": 2}
{"frame": 3}"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["frame"], 3);
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_interleaved_noise_and_json() {
let stdout = r#"Starting process...
{"frame": 1, "status": "initializing"}
Some debug output
Warning: something happened
{"frame": 2, "status": "processing"}
More noise here
{"frame": 3, "status": "complete"}
Done!"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["frame"], 3);
assert_eq!(parsed["status"], "complete");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_no_valid_json() {
let stdout = "This is just plain text\nNo JSON here\nJust noise";
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
NdjsonResult::NoValidJson { tail_excerpt } => {
assert_eq!(tail_excerpt, stdout);
}
}
}
#[test]
fn test_parse_ndjson_partial_json() {
let stdout = r#"{"frame": 1, "status": "ok"}
{"frame": 2, "incomplete": tru"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["frame"], 1);
assert_eq!(parsed["status"], "ok");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson from first frame"),
}
}
#[test]
fn test_parse_ndjson_only_partial_json() {
let stdout = r#"{"incomplete": tru"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
NdjsonResult::NoValidJson { tail_excerpt } => {
assert_eq!(tail_excerpt, stdout);
}
}
}
#[test]
fn test_parse_ndjson_empty_string() {
let stdout = "";
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
NdjsonResult::NoValidJson { tail_excerpt } => {
assert_eq!(tail_excerpt, "");
}
}
}
#[test]
fn test_parse_ndjson_only_whitespace() {
let stdout = " \n\n \t \n";
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
NdjsonResult::NoValidJson { tail_excerpt } => {
assert_eq!(tail_excerpt, stdout);
}
}
}
#[test]
fn test_parse_ndjson_tail_excerpt_truncation() {
let long_text = "x".repeat(300);
let result = parse_ndjson(&long_text);
match result {
NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
NdjsonResult::NoValidJson { tail_excerpt } => {
assert_eq!(tail_excerpt.len(), 256);
assert_eq!(tail_excerpt, "x".repeat(256));
}
}
}
#[test]
fn test_parse_ndjson_tail_excerpt_no_truncation() {
let short_text = "Short text";
let result = parse_ndjson(short_text);
match result {
NdjsonResult::ValidJson(_) => panic!("Expected NoValidJson"),
NdjsonResult::NoValidJson { tail_excerpt } => {
assert_eq!(tail_excerpt, short_text);
}
}
}
#[test]
fn test_parse_ndjson_malformed_json_lines() {
let stdout = r#"{"valid": "json"}
{malformed json}
{"another": "valid"}
[not an object]
{"final": "valid"}"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["final"], "valid");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_json_array_is_valid() {
let stdout = r#"[1, 2, 3]
{"object": "value"}
[4, 5, 6]"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.is_array());
assert_eq!(parsed[0], 4);
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_json_primitives() {
let stdout = r#""string value"
42
true
null
{"final": "object"}"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["final"], "object");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_unicode_content() {
let stdout = r#"{"message": "Hello 世界"}
{"emoji": "🎉🎊"}
{"final": "完成"}"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["final"], "完成");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
#[test]
fn test_parse_ndjson_escaped_characters() {
let stdout = r#"{"path": "C:\\Users\\test\\file.txt"}
{"quote": "He said \\"hello\\""}
{"final": "done"}"#;
let result = parse_ndjson(stdout);
match result {
NdjsonResult::ValidJson(json) => {
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["final"], "done");
}
NdjsonResult::NoValidJson { .. } => panic!("Expected ValidJson"),
}
}
}