use std::io::{self, Write};
use anyhow::{Context, Result};
use serde_json::Value;
use crate::snapshot::{Snapshot, sanitize_json_floats};
fn snapshots_to_sanitized_value(snapshots: &[Snapshot]) -> Result<Value> {
let mut value = if snapshots.len() == 1 {
serde_json::to_value(&snapshots[0]).context("failed to serialize snapshot to JSON value")?
} else {
serde_json::to_value(snapshots).context("failed to serialize snapshots to JSON value")?
};
sanitize_json_floats(&mut value);
Ok(value)
}
pub fn write_frame_json<W: Write>(w: &mut W, snapshot: &Snapshot) -> io::Result<()> {
let mut value = serde_json::to_value(snapshot)
.map_err(|e| io::Error::other(format!("failed to serialize snapshot frame: {e}")))?;
sanitize_json_floats(&mut value);
let line = serde_json::to_string(&value)
.map_err(|e| io::Error::other(format!("failed to serialize snapshot frame: {e}")))?;
w.write_all(line.as_bytes())?;
w.write_all(b"\n")?;
Ok(())
}
pub fn render(snapshots: &[Snapshot], pretty: bool) -> Result<String> {
let value = snapshots_to_sanitized_value(snapshots)?;
let mut out = if pretty {
serde_json::to_string_pretty(&value).context("failed to serialize snapshot to JSON")?
} else {
serde_json::to_string(&value).context("failed to serialize snapshot to JSON")?
};
out.push('\n');
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::Snapshot;
use serde_json::Value;
fn minimal_snapshot() -> Snapshot {
Snapshot {
schema: 1,
timestamp: "2026-04-20T00:00:00Z".to_string(),
hostname: "testhost".to_string(),
gpus: None,
cpus: Some(Vec::new()),
memory: Some(Vec::new()),
chassis: None,
processes: None,
storage: None,
errors: Vec::new(),
}
}
#[test]
fn single_sample_is_object() {
let snap = minimal_snapshot();
let rendered = render(std::slice::from_ref(&snap), false).unwrap();
let parsed: Value = serde_json::from_str(rendered.trim()).unwrap();
assert!(parsed.is_object());
assert_eq!(parsed["schema"], Value::from(1));
assert_eq!(parsed["timestamp"], "2026-04-20T00:00:00Z");
assert!(parsed.get("gpus").is_none());
assert!(parsed.get("chassis").is_none());
assert!(parsed.get("cpus").unwrap().is_array());
}
#[test]
fn multi_sample_is_array() {
let snaps = vec![minimal_snapshot(), minimal_snapshot()];
let rendered = render(&snaps, false).unwrap();
let parsed: Value = serde_json::from_str(rendered.trim()).unwrap();
assert!(parsed.is_array());
assert_eq!(parsed.as_array().unwrap().len(), 2);
}
#[test]
fn pretty_output_has_newlines() {
let snap = minimal_snapshot();
let rendered = render(std::slice::from_ref(&snap), true).unwrap();
assert!(rendered.contains('\n'));
assert!(rendered.ends_with('\n'));
}
#[test]
fn compact_output_ends_with_newline() {
let snap = minimal_snapshot();
let rendered = render(std::slice::from_ref(&snap), false).unwrap();
assert!(rendered.ends_with('\n'));
}
}