Skip to main content

serializer/
formatter.rs

1//! Human-readable formatter for DX data
2//!
3//! Transforms machine-optimized DX into beautiful, aligned, easy-to-read format.
4//! This is what you see in the IDE extension (DX View).
5
6// Writing to a String via std::fmt::Write never fails (infallible),
7// so unwrap() on write!/writeln! to String is safe and idiomatic.
8#![allow(clippy::unwrap_used)]
9
10use crate::error::Result;
11use crate::types::{DxArray, DxObject, DxTable, DxValue};
12use std::fmt::Write as FmtWrite;
13
14/// Formatter configuration
15#[derive(Debug, Clone)]
16pub struct FormatterConfig {
17    /// Column padding for alignment
18    pub column_padding: usize,
19    /// Use Unicode symbols (✓/✗ instead of +/-)
20    pub use_unicode: bool,
21    /// Add section dividers
22    pub add_dividers: bool,
23    /// Color output (ANSI codes)
24    pub use_colors: bool,
25}
26
27impl Default for FormatterConfig {
28    fn default() -> Self {
29        Self {
30            column_padding: 4,
31            use_unicode: true,
32            add_dividers: true,
33            use_colors: false,
34        }
35    }
36}
37
38/// Human-readable formatter
39pub struct HumanFormatter {
40    config: FormatterConfig,
41    output: String,
42    indent: usize,
43}
44
45impl HumanFormatter {
46    /// Create a formatter with an explicit configuration.
47    pub fn new(config: FormatterConfig) -> Self {
48        Self {
49            config,
50            output: String::new(),
51            indent: 0,
52        }
53    }
54
55    /// Create a formatter with default human-readable settings.
56    pub fn with_defaults() -> Self {
57        Self::new(FormatterConfig::default())
58    }
59
60    /// Format a value to human-readable string
61    pub fn format(&mut self, value: &DxValue) -> Result<String> {
62        self.output.clear();
63        self.indent = 0;
64
65        if self.config.add_dividers {
66            self.write_header();
67        }
68
69        self.format_value(value)?;
70
71        Ok(self.output.clone())
72    }
73
74    /// Write header
75    fn write_header(&mut self) {
76        let line = "─".repeat(70);
77        writeln!(self.output, "┌{}┐", line).unwrap();
78        writeln!(
79            self.output,
80            "│  DX HUMAN VIEW  •  Enhanced Readability Mode{}│",
81            " ".repeat(31)
82        )
83        .unwrap();
84        writeln!(self.output, "└{}┘", line).unwrap();
85        writeln!(self.output).unwrap();
86    }
87
88    /// Format a value
89    fn format_value(&mut self, value: &DxValue) -> Result<()> {
90        match value {
91            DxValue::Object(obj) => self.format_object(obj),
92            DxValue::Table(table) => self.format_table(table),
93            DxValue::Array(arr) => self.format_array(arr),
94            _ => {
95                self.write_indent();
96                self.write_simple_value(value)?;
97                writeln!(self.output).unwrap();
98                Ok(())
99            }
100        }
101    }
102
103    /// Format an object
104    fn format_object(&mut self, obj: &DxObject) -> Result<()> {
105        // Calculate max key length for alignment
106        let max_key_len = obj.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
107
108        for (key, value) in obj.iter() {
109            self.write_indent();
110
111            // Write key with padding
112            write!(self.output, "{}", key).unwrap();
113            let padding = max_key_len - key.len() + self.config.column_padding;
114            write!(self.output, "{}", " ".repeat(padding)).unwrap();
115
116            match value {
117                DxValue::Table(table) => {
118                    writeln!(self.output).unwrap();
119                    if self.config.add_dividers {
120                        self.write_indent();
121                        writeln!(self.output, "┌─ {} TABLE ─┐", key.to_uppercase()).unwrap();
122                    }
123                    self.indent += 1;
124                    self.format_table(table)?;
125                    self.indent -= 1;
126                    if self.config.add_dividers {
127                        self.write_indent();
128                        writeln!(self.output, "└{}┘", "─".repeat(15)).unwrap();
129                    }
130                }
131                DxValue::Array(arr) if arr.is_stream => {
132                    write!(self.output, "> ").unwrap();
133                    for (i, val) in arr.values.iter().enumerate() {
134                        if i > 0 {
135                            write!(self.output, " | ").unwrap();
136                        }
137                        self.write_simple_value(val)?;
138                    }
139                    writeln!(self.output).unwrap();
140                }
141                DxValue::Object(nested) => {
142                    writeln!(self.output).unwrap();
143                    self.indent += 1;
144                    self.format_object(nested)?;
145                    self.indent -= 1;
146                }
147                _ => {
148                    write!(self.output, ": ").unwrap();
149                    self.write_simple_value(value)?;
150                    writeln!(self.output).unwrap();
151                }
152            }
153        }
154
155        Ok(())
156    }
157
158    /// Format a table with aligned columns
159    fn format_table(&mut self, table: &DxTable) -> Result<()> {
160        if table.rows.is_empty() {
161            self.write_indent();
162            writeln!(self.output, "(empty table)").unwrap();
163            return Ok(());
164        }
165
166        // Calculate column widths
167        let mut col_widths: Vec<usize> =
168            table.schema.columns.iter().map(|c| c.name.len()).collect();
169
170        for row in &table.rows {
171            for (i, value) in row.iter().enumerate() {
172                let val_str = self.value_to_string(value);
173                col_widths[i] = col_widths[i].max(val_str.len());
174            }
175        }
176
177        // Write column headers
178        self.write_indent();
179        for (i, col) in table.schema.columns.iter().enumerate() {
180            if i > 0 {
181                write!(self.output, "  ").unwrap();
182            }
183            write!(self.output, "{:width$}", col.name, width = col_widths[i]).unwrap();
184        }
185        writeln!(self.output).unwrap();
186
187        // Write separator
188        self.write_indent();
189        for (i, width) in col_widths.iter().enumerate() {
190            if i > 0 {
191                write!(self.output, "  ").unwrap();
192            }
193            write!(self.output, "{}", "─".repeat(*width)).unwrap();
194        }
195        writeln!(self.output).unwrap();
196
197        // Write rows
198        for row in &table.rows {
199            self.write_indent();
200            for (i, value) in row.iter().enumerate() {
201                if i > 0 {
202                    write!(self.output, "  ").unwrap();
203                }
204                let val_str = self.value_to_string(value);
205                write!(self.output, "{:width$}", val_str, width = col_widths[i]).unwrap();
206            }
207            writeln!(self.output).unwrap();
208        }
209
210        Ok(())
211    }
212
213    /// Format an array
214    fn format_array(&mut self, arr: &DxArray) -> Result<()> {
215        if arr.is_stream {
216            write!(self.output, "> ").unwrap();
217            for (i, val) in arr.values.iter().enumerate() {
218                if i > 0 {
219                    write!(self.output, " | ").unwrap();
220                }
221                self.write_simple_value(val)?;
222            }
223            writeln!(self.output).unwrap();
224        } else {
225            for (i, val) in arr.values.iter().enumerate() {
226                self.write_indent();
227                write!(self.output, "{}. ", i + 1).unwrap();
228                self.write_simple_value(val)?;
229                writeln!(self.output).unwrap();
230            }
231        }
232        Ok(())
233    }
234
235    /// Write a simple value
236    fn write_simple_value(&mut self, value: &DxValue) -> Result<()> {
237        let s = match value {
238            DxValue::Null => "null".to_string(),
239            DxValue::Bool(true) => {
240                if self.config.use_unicode {
241                    "✓".to_string()
242                } else {
243                    "+".to_string()
244                }
245            }
246            DxValue::Bool(false) => {
247                if self.config.use_unicode {
248                    "✗".to_string()
249                } else {
250                    "-".to_string()
251                }
252            }
253            DxValue::Int(i) => i.to_string(),
254            DxValue::Float(f) => format!("{:.2}", f),
255            DxValue::String(s) => s.clone(),
256            DxValue::Ref(id) => format!("@{}", id),
257            _ => format!("{:?}", value),
258        };
259
260        write!(self.output, "{}", s).unwrap();
261        Ok(())
262    }
263
264    /// Convert value to string for width calculation
265    fn value_to_string(&self, value: &DxValue) -> String {
266        match value {
267            DxValue::Null => "null".to_string(),
268            DxValue::Bool(true) => {
269                if self.config.use_unicode {
270                    "✓".to_string()
271                } else {
272                    "+".to_string()
273                }
274            }
275            DxValue::Bool(false) => {
276                if self.config.use_unicode {
277                    "✗".to_string()
278                } else {
279                    "-".to_string()
280                }
281            }
282            DxValue::Int(i) => i.to_string(),
283            DxValue::Float(f) => format!("{:.2}", f),
284            DxValue::String(s) => s.clone(),
285            DxValue::Ref(id) => format!("@{}", id),
286            _ => format!("{:?}", value),
287        }
288    }
289
290    /// Write indentation
291    fn write_indent(&mut self) {
292        write!(self.output, "{}", "  ".repeat(self.indent)).unwrap();
293    }
294}
295
296/// Format a value with default config
297#[must_use = "formatting result should be used"]
298pub fn format_human(value: &DxValue) -> Result<String> {
299    let mut formatter = HumanFormatter::with_defaults();
300    formatter.format(value)
301}
302
303/// Format with custom config
304#[must_use = "formatting result should be used"]
305pub fn format_human_with_config(value: &DxValue, config: FormatterConfig) -> Result<String> {
306    let mut formatter = HumanFormatter::new(config);
307    formatter.format(value)
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::parser::parse;
314
315    #[test]
316    fn test_format_simple() {
317        let input = b"name:Alice
318age:30
319active:+";
320
321        let value = parse(input).unwrap();
322        let formatted = format_human(&value).unwrap();
323
324        assert!(formatted.contains("Alice"));
325        assert!(formatted.contains("30"));
326        assert!(formatted.contains("✓"));
327    }
328
329    #[test]
330    fn test_format_table() {
331        let input = b"users=id%i name%s active%b
3321 Alice +
3332 Bob -";
334
335        let value = parse(input).unwrap();
336        let formatted = format_human(&value).unwrap();
337
338        assert!(formatted.contains("USERS TABLE"));
339        assert!(formatted.contains("Alice"));
340        assert!(formatted.contains("Bob"));
341        assert!(formatted.contains("✓"));
342        assert!(formatted.contains("✗"));
343    }
344}