Skip to main content

synwire_core/vfs/
output.rs

1//! Output serialization for VFS responses.
2//!
3//! VFS operations return Rust types that derive `Serialize`.  Before passing
4//! them to an LLM, they must be serialized to a text format the model can
5//! consume.  Two formats are supported:
6//!
7//! - **JSON** — standard `serde_json` serialization.
8//! - **TOON** — [Token-Oriented Object Notation](https://github.com/toon-format/spec),
9//!   a compact format that reduces token usage by 30–60% for tabular data.
10//!
11//! The format can be set as a default on the LLM provider and overridden
12//! per-call.
13
14use serde::Serialize;
15
16/// Serialization format for VFS output returned to LLMs.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum OutputFormat {
20    /// Standard JSON (`serde_json`).
21    #[default]
22    Json,
23    /// Compact JSON — no pretty-printing.
24    JsonCompact,
25    /// TOON — token-efficient format for LLM consumption.
26    ///
27    /// Requires the `toon` feature.  Falls back to `Json` if not enabled.
28    Toon,
29}
30
31/// Serialize any `Serialize` value to a string in the given format.
32///
33/// # Errors
34///
35/// Returns an error string if serialization fails.
36pub fn format_output<T: Serialize>(value: &T, format: OutputFormat) -> Result<String, String> {
37    match format {
38        OutputFormat::Json => serde_json::to_string_pretty(value).map_err(|e| e.to_string()),
39        OutputFormat::JsonCompact => serde_json::to_string(value).map_err(|e| e.to_string()),
40        OutputFormat::Toon => format_toon(value),
41    }
42}
43
44#[cfg(feature = "toon")]
45fn format_toon<T: Serialize>(value: &T) -> Result<String, String> {
46    let json = serde_json::to_value(value).map_err(|e| e.to_string())?;
47    Ok(toon::encode(&json, None))
48}
49
50#[cfg(not(feature = "toon"))]
51fn format_toon<T: Serialize>(value: &T) -> Result<String, String> {
52    // Fallback to pretty JSON when toon feature is not enabled.
53    serde_json::to_string_pretty(value).map_err(|e| e.to_string())
54}
55
56#[cfg(test)]
57#[allow(clippy::expect_used)]
58mod tests {
59    use super::*;
60    use crate::vfs::types::DirEntry;
61
62    #[test]
63    fn test_json_format() {
64        let entry = DirEntry {
65            name: "hello.txt".to_string(),
66            path: "/hello.txt".to_string(),
67            is_dir: false,
68            size: Some(42),
69            modified: None,
70            permissions: None,
71            is_symlink: false,
72        };
73        let out = format_output(&entry, OutputFormat::Json).expect("json");
74        assert!(out.contains("hello.txt"));
75        assert!(out.contains('\n')); // pretty-printed
76    }
77
78    #[test]
79    fn test_json_compact_format() {
80        let entry = DirEntry {
81            name: "hello.txt".to_string(),
82            path: "/hello.txt".to_string(),
83            is_dir: false,
84            size: Some(42),
85            modified: None,
86            permissions: None,
87            is_symlink: false,
88        };
89        let out = format_output(&entry, OutputFormat::JsonCompact).expect("json compact");
90        assert!(out.contains("hello.txt"));
91        assert!(!out.contains('\n')); // not pretty-printed
92    }
93
94    #[cfg(feature = "toon")]
95    #[test]
96    fn test_toon_format() {
97        let entries = vec![
98            DirEntry {
99                name: "a.rs".to_string(),
100                path: "/a.rs".to_string(),
101                is_dir: false,
102                size: Some(100),
103                modified: None,
104                permissions: None,
105                is_symlink: false,
106            },
107            DirEntry {
108                name: "b.rs".to_string(),
109                path: "/b.rs".to_string(),
110                is_dir: false,
111                size: Some(200),
112                modified: None,
113                permissions: None,
114                is_symlink: false,
115            },
116        ];
117        let out = format_output(&entries, OutputFormat::Toon).expect("toon");
118        // TOON should be more compact than JSON for uniform arrays.
119        let json = format_output(&entries, OutputFormat::Json).expect("json");
120        assert!(
121            out.len() <= json.len(),
122            "TOON should be <= JSON for tabular data"
123        );
124    }
125}