Skip to main content

engram_core/
output.rs

1use serde_json::Value;
2
3#[derive(Clone, clap::ValueEnum)]
4pub enum OutputFormat {
5    Json,
6    Text,
7    Jsonl,
8}
9
10pub fn format_output(value: &Value, format: &OutputFormat) -> String {
11    match format {
12        OutputFormat::Json => format_as_json(value),
13        OutputFormat::Text => format_as_text(value),
14        OutputFormat::Jsonl => format_as_jsonl(value),
15    }
16}
17
18fn format_as_json(value: &Value) -> String {
19    serde_json::to_string_pretty(value).unwrap_or_default()
20}
21
22fn format_as_jsonl(value: &Value) -> String {
23    if let Some((results, degraded)) = search_envelope(value) {
24        let mut lines: Vec<String> = results
25            .iter()
26            .map(|item| serde_json::to_string(item).unwrap_or_default())
27            .collect();
28        lines.push(serde_json::to_string(&json_degraded(degraded)).unwrap_or_default());
29        return lines.join("\n");
30    }
31    match value {
32        Value::Array(items) => items
33            .iter()
34            .map(|item| serde_json::to_string(item).unwrap_or_default())
35            .collect::<Vec<_>>()
36            .join("\n"),
37        _ => serde_json::to_string(value).unwrap_or_default(),
38    }
39}
40
41fn format_as_text(value: &Value) -> String {
42    if let Some((results, degraded)) = search_envelope(value) {
43        let degraded_line = format!("degraded: {degraded}");
44        if results.is_empty() {
45            return degraded_line;
46        }
47        return format!("{}\n{degraded_line}", format_array_as_text(results));
48    }
49    match value {
50        Value::Object(map) => format_object_as_text(map),
51        Value::Array(items) => format_array_as_text(items),
52        Value::String(string) => string.clone(),
53        Value::Null => String::new(),
54        other => other.to_string(),
55    }
56}
57
58/// A `memory_search` response is an object carrying a `results` array alongside
59/// a `degraded` flag. The jsonl/text formatters fan out over the inner array so
60/// each hit renders as its own line/block; other object responses (status,
61/// judge, config) lack the `results` array and fall through to the generic
62/// object rendering unchanged.
63fn search_envelope(value: &Value) -> Option<(&[Value], bool)> {
64    let map = value.as_object()?;
65    let results = map.get("results")?.as_array()?;
66    let degraded = map.get("degraded")?.as_bool()?;
67    Some((results, degraded))
68}
69
70fn json_degraded(degraded: bool) -> Value {
71    serde_json::json!({ "degraded": degraded })
72}
73
74fn format_object_as_text(map: &serde_json::Map<String, Value>) -> String {
75    map.iter()
76        .map(|(key, value)| format!("{key}: {}", scalar_to_text(value)))
77        .collect::<Vec<_>>()
78        .join("\n")
79}
80
81fn format_array_as_text(items: &[Value]) -> String {
82    items
83        .iter()
84        .map(format_as_text)
85        .collect::<Vec<_>>()
86        .join("\n---\n")
87}
88
89fn scalar_to_text(value: &Value) -> String {
90    match value {
91        Value::String(string) => string.clone(),
92        Value::Null => String::new(),
93        Value::Array(_) | Value::Object(_) => serde_json::to_string(value).unwrap_or_default(),
94        other => other.to_string(),
95    }
96}