magi-linear-axi 0.2.3

Agent-native Linear GraphQL CLI with TOON output
Documentation
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}}}"
    );
}

/// Maximum Unicode code points rendered for a string in default output.
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]
            })
        );
    }
}