use crate::error::AppError;
use serde_json::Value;
use std::{fs::OpenOptions, io::Write, time::Instant};
fn timing_event(duration_ms: u128) {
let Some(path) = std::env::var_os("MAGI_LINEAR_TIMING_FILE") else {
return;
};
let Ok(duration_ms) = u64::try_from(duration_ms) else {
return;
};
let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) else {
return;
};
let _ = writeln!(
file,
"{{\"component\":\"render\",\"durationMs\":{duration_ms}}}"
);
}
pub const AXI_OUTPUT_MAX_UNICODE_CODE_POINTS: usize = 240;
pub struct Output {
format: String,
full: bool,
}
impl Output {
pub fn new(format: String, full: bool) -> Self {
Self { format, full }
}
pub fn render(&self, v: &Value) -> Result<(), AppError> {
let started = Instant::now();
let result = if self.full {
self.render_value(v)
} else {
let mut value = v.clone();
truncate_value(&mut value);
self.render_value(&value)
};
timing_event(started.elapsed().as_millis());
result
}
pub fn render_owned(&self, mut value: Value) -> Result<(), AppError> {
let started = Instant::now();
if !self.full {
truncate_value(&mut value);
}
let result = self.render_value(&value);
timing_event(started.elapsed().as_millis());
result
}
fn render_value(&self, value: &Value) -> Result<(), AppError> {
let text = if self.format == "json" {
serde_json::to_string(value).map_err(|e| AppError::Output(e.to_string()))?
} else {
toon_format::encode(value, &toon_format::EncodeOptions::default())
.map_err(|e| AppError::Output(e.to_string()))?
};
println!("{text}");
Ok(())
}
}
fn truncate_value(v: &mut Value) {
match v {
Value::String(s) if s.chars().count() > AXI_OUTPUT_MAX_UNICODE_CODE_POINTS => {
let count = s.chars().count();
let prefix: String = s.chars().take(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS).collect();
*s = format!("{prefix}… [truncated, {count} chars]");
}
Value::Array(items) => items.iter_mut().for_each(truncate_value),
Value::Object(items) => items.values_mut().for_each(truncate_value),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::{AXI_OUTPUT_MAX_UNICODE_CODE_POINTS, truncate_value};
#[test]
fn valid_toon() {
let encoded = toon_format::encode(
&serde_json::json!({"x":1}),
&toon_format::EncodeOptions::default(),
);
assert!(encoded.is_ok());
assert!(encoded.unwrap_or_default().contains("x"));
}
#[test]
fn default_output_limit_counts_unicode_code_points() {
let exact = "🙂".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS);
let mut exact_value = serde_json::json!(exact);
truncate_value(&mut exact_value);
assert_eq!(exact_value, serde_json::json!(exact));
let oversized = "🙂".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS + 1);
let mut oversized_value = serde_json::json!(oversized);
truncate_value(&mut oversized_value);
assert_eq!(
oversized_value,
serde_json::json!(format!(
"{}… [truncated, {} chars]",
"🙂".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS),
AXI_OUTPUT_MAX_UNICODE_CODE_POINTS + 1,
))
);
}
#[test]
fn default_output_truncates_nested_strings_in_place() {
let exact = "é".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS);
let oversized = "🙂".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS + 1);
let mut value = serde_json::json!({
"nested": [exact, {"text": oversized}],
"unchanged": [42, true, null]
});
truncate_value(&mut value);
assert_eq!(
value,
serde_json::json!({
"nested": [
"é".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS),
{"text": format!(
"{}… [truncated, {} chars]",
"🙂".repeat(AXI_OUTPUT_MAX_UNICODE_CODE_POINTS),
AXI_OUTPUT_MAX_UNICODE_CODE_POINTS + 1,
)}
],
"unchanged": [42, true, null]
})
);
}
}