agent_commander/streaming/ndjson.rs
1//! NDJSON (Newline Delimited JSON) utilities
2//! https://github.com/ndjson/ndjson-spec
3
4use serde_json::Value;
5
6/// Parse a single NDJSON line
7///
8/// # Arguments
9/// * `line` - Line to parse
10///
11/// # Returns
12/// Parsed JSON value or None if invalid
13pub fn parse_ndjson_line(line: &str) -> Option<Value> {
14 let trimmed = line.trim();
15
16 // Empty lines are ignored in NDJSON
17 if trimmed.is_empty() {
18 return None;
19 }
20
21 // Must start with { or [ to be valid JSON
22 if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
23 return None;
24 }
25
26 serde_json::from_str(trimmed).ok()
27}
28
29/// Stringify an object to NDJSON line
30///
31/// # Arguments
32/// * `value` - Value to stringify
33/// * `compact` - Use compact JSON (no indentation)
34///
35/// # Returns
36/// NDJSON line with trailing newline
37pub fn stringify_ndjson_line(value: &Value, compact: bool) -> String {
38 if value.is_null() {
39 return String::new();
40 }
41
42 let json = if compact {
43 serde_json::to_string(value).unwrap_or_default()
44 } else {
45 serde_json::to_string_pretty(value).unwrap_or_default()
46 };
47
48 format!("{}\n", json)
49}
50
51/// Parse multiple NDJSON lines
52///
53/// # Arguments
54/// * `data` - Data containing multiple lines
55///
56/// # Returns
57/// Vector of parsed JSON values
58pub fn parse_ndjson(data: &str) -> Vec<Value> {
59 data.lines()
60 .filter_map(|line| parse_ndjson_line(line))
61 .collect()
62}
63
64/// Stringify multiple objects to NDJSON
65///
66/// # Arguments
67/// * `values` - Values to stringify
68/// * `compact` - Use compact JSON
69///
70/// # Returns
71/// NDJSON string
72pub fn stringify_ndjson(values: &[Value], compact: bool) -> String {
73 values
74 .iter()
75 .map(|v| stringify_ndjson_line(v, compact))
76 .collect()
77}
78
79// Tests are in rust/tests/streaming_tests.rs