Skip to main content

facet_value/
format.rs

1//! Pretty formatting for Values with span tracking.
2//!
3//! This module provides functionality to format a `Value` as JSON-like text,
4//! tracking byte spans for each path through the value for use in diagnostics.
5
6use alloc::collections::BTreeMap;
7use alloc::string::String;
8use alloc::vec::Vec;
9use core::fmt::Write;
10
11use crate::{Value, ValueType};
12
13/// A segment in a path through a Value
14#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub enum PathSegment {
16    /// A key in an object
17    Key(String),
18    /// An index in an array
19    Index(usize),
20}
21
22/// A path to a location within a Value
23pub type Path = Vec<PathSegment>;
24
25/// A byte span in formatted output (start, end)
26pub type Span = (usize, usize);
27
28/// Result of formatting a value with span tracking
29#[derive(Debug)]
30pub struct FormattedValue {
31    /// The formatted text (plain text)
32    pub text: String,
33    /// Map from paths to their byte spans in `text`
34    pub spans: BTreeMap<Path, Span>,
35}
36
37/// Format a Value as JSON-like text with span tracking
38pub fn format_value_with_spans(value: &Value) -> FormattedValue {
39    let mut ctx = FormatContext::new();
40    format_value_into(&mut ctx, value, &[]);
41    FormattedValue {
42        text: ctx.output,
43        spans: ctx.spans,
44    }
45}
46
47/// Format a Value as JSON-like text (no span tracking, just plain output)
48pub fn format_value(value: &Value) -> String {
49    let mut ctx = FormatContext::new();
50    format_value_into(&mut ctx, value, &[]);
51    ctx.output
52}
53
54struct FormatContext {
55    output: String,
56    spans: BTreeMap<Path, Span>,
57    indent: usize,
58}
59
60impl FormatContext {
61    const fn new() -> Self {
62        Self {
63            output: String::new(),
64            spans: BTreeMap::new(),
65            indent: 0,
66        }
67    }
68
69    const fn len(&self) -> usize {
70        self.output.len()
71    }
72
73    fn write_indent(&mut self) {
74        for _ in 0..self.indent {
75            self.output.push_str("  ");
76        }
77    }
78
79    fn record_span(&mut self, path: &[PathSegment], start: usize, end: usize) {
80        self.spans.insert(path.to_vec(), (start, end));
81    }
82}
83
84fn format_value_into(ctx: &mut FormatContext, value: &Value, current_path: &[PathSegment]) {
85    let start = ctx.len();
86
87    match value.value_type() {
88        ValueType::Null => {
89            ctx.output.push_str("null");
90        }
91        ValueType::Bool => {
92            if value.is_true() {
93                ctx.output.push_str("true");
94            } else {
95                ctx.output.push_str("false");
96            }
97        }
98        ValueType::Number => {
99            let num = value.as_number().unwrap();
100            // Use the numeric representation directly
101            if let Some(i) = num.to_i64() {
102                let _ = write!(ctx.output, "{i}");
103            } else if let Some(u) = num.to_u64() {
104                let _ = write!(ctx.output, "{u}");
105            } else if let Some(f) = num.to_f64() {
106                let _ = write!(ctx.output, "{f}");
107            }
108        }
109        ValueType::String => {
110            let s = value.as_string().unwrap();
111            // Write as JSON string with escaping
112            ctx.output.push('"');
113            for c in s.as_str().chars() {
114                match c {
115                    '"' => ctx.output.push_str("\\\""),
116                    '\\' => ctx.output.push_str("\\\\"),
117                    '\n' => ctx.output.push_str("\\n"),
118                    '\r' => ctx.output.push_str("\\r"),
119                    '\t' => ctx.output.push_str("\\t"),
120                    c if c.is_control() => {
121                        let _ = write!(ctx.output, "\\u{:04x}", c as u32);
122                    }
123                    c => ctx.output.push(c),
124                }
125            }
126            ctx.output.push('"');
127        }
128        ValueType::Bytes => {
129            let bytes = value.as_bytes().unwrap();
130            ctx.output.push_str("<bytes:");
131            let _ = write!(ctx.output, "{}", bytes.len());
132            ctx.output.push('>');
133        }
134        ValueType::Array => {
135            let arr = value.as_array().unwrap();
136            if arr.is_empty() {
137                ctx.output.push_str("[]");
138            } else {
139                ctx.output.push_str("[\n");
140                ctx.indent += 1;
141                for (i, item) in arr.iter().enumerate() {
142                    ctx.write_indent();
143                    let mut item_path = current_path.to_vec();
144                    item_path.push(PathSegment::Index(i));
145                    format_value_into(ctx, item, &item_path);
146                    if i < arr.len() - 1 {
147                        ctx.output.push(',');
148                    }
149                    ctx.output.push('\n');
150                }
151                ctx.indent -= 1;
152                ctx.write_indent();
153                ctx.output.push(']');
154            }
155        }
156        ValueType::Object => {
157            let obj = value.as_object().unwrap();
158            if obj.is_empty() {
159                ctx.output.push_str("{}");
160            } else {
161                ctx.output.push_str("{\n");
162                ctx.indent += 1;
163                let entries: Vec<_> = obj.iter().collect();
164                for (i, (key, val)) in entries.iter().enumerate() {
165                    ctx.write_indent();
166                    // Write key
167                    ctx.output.push('"');
168                    ctx.output.push_str(key.as_str());
169                    ctx.output.push_str("\": ");
170                    // Format value with path
171                    let mut item_path = current_path.to_vec();
172                    item_path.push(PathSegment::Key(key.as_str().into()));
173                    format_value_into(ctx, val, &item_path);
174                    if i < entries.len() - 1 {
175                        ctx.output.push(',');
176                    }
177                    ctx.output.push('\n');
178                }
179                ctx.indent -= 1;
180                ctx.write_indent();
181                ctx.output.push('}');
182            }
183        }
184        ValueType::DateTime => {
185            let dt = value.as_datetime().unwrap();
186            // Format using Debug which produces ISO 8601 format
187            let _ = write!(ctx.output, "{dt:?}");
188        }
189        ValueType::QName => {
190            let qname = value.as_qname().unwrap();
191            // Format using Debug which produces {namespace}local_name format
192            let _ = write!(ctx.output, "{qname:?}");
193        }
194        ValueType::Uuid => {
195            let uuid = value.as_uuid().unwrap();
196            // Format using Debug which produces standard UUID format
197            let _ = write!(ctx.output, "{uuid:?}");
198        }
199        ValueType::Char => {
200            let c = value.as_char().unwrap();
201            // Write as a JSON string with escaping, mirroring the String arm.
202            ctx.output.push('"');
203            match c {
204                '"' => ctx.output.push_str("\\\""),
205                '\\' => ctx.output.push_str("\\\\"),
206                '\n' => ctx.output.push_str("\\n"),
207                '\r' => ctx.output.push_str("\\r"),
208                '\t' => ctx.output.push_str("\\t"),
209                c if c.is_control() => {
210                    let _ = write!(ctx.output, "\\u{:04x}", c as u32);
211                }
212                c => ctx.output.push(c),
213            }
214            ctx.output.push('"');
215        }
216    }
217
218    let end = ctx.len();
219    ctx.record_span(current_path, start, end);
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::{VArray, VObject, VString};
226
227    #[test]
228    fn test_format_primitives() {
229        assert_eq!(format_value(&Value::NULL), "null");
230        assert_eq!(format_value(&Value::TRUE), "true");
231        assert_eq!(format_value(&Value::FALSE), "false");
232        assert_eq!(format_value(&Value::from(42i64)), "42");
233        assert_eq!(
234            format_value(&Value::from(VString::new("hello"))),
235            "\"hello\""
236        );
237    }
238
239    #[test]
240    fn test_format_array() {
241        let mut arr = VArray::new();
242        arr.push(Value::from(1i64));
243        arr.push(Value::from(2i64));
244        arr.push(Value::from(3i64));
245        let value: Value = arr.into();
246
247        let result = format_value_with_spans(&value);
248        assert!(result.text.contains("1"));
249        assert!(result.text.contains("2"));
250        assert!(result.text.contains("3"));
251
252        // Check that array elements have spans
253        let path_0 = vec![PathSegment::Index(0)];
254        assert!(result.spans.contains_key(&path_0));
255    }
256
257    #[test]
258    fn test_format_object() {
259        let mut obj = VObject::new();
260        obj.insert("name", Value::from(VString::new("Alice")));
261        obj.insert("age", Value::from(30i64));
262        let value: Value = obj.into();
263
264        let result = format_value_with_spans(&value);
265        assert!(result.text.contains("\"name\""));
266        assert!(result.text.contains("\"Alice\""));
267        assert!(result.text.contains("\"age\""));
268        assert!(result.text.contains("30"));
269
270        // Check that object fields have spans
271        let name_path = vec![PathSegment::Key("name".into())];
272        let age_path = vec![PathSegment::Key("age".into())];
273        assert!(
274            result.spans.contains_key(&name_path),
275            "Missing span for 'name'"
276        );
277        assert!(
278            result.spans.contains_key(&age_path),
279            "Missing span for 'age'"
280        );
281
282        // Verify the span content
283        let age_span = result.spans.get(&age_path).unwrap();
284        let age_text = &result.text[age_span.0..age_span.1];
285        assert_eq!(age_text, "30");
286    }
287
288    #[test]
289    fn test_format_nested() {
290        let mut inner = VObject::new();
291        inner.insert("x", Value::from(10i64));
292
293        let mut outer = VObject::new();
294        outer.insert("point", Value::from(inner));
295        let value: Value = outer.into();
296
297        let result = format_value_with_spans(&value);
298
299        // Check nested path
300        let nested_path = vec![
301            PathSegment::Key("point".into()),
302            PathSegment::Key("x".into()),
303        ];
304        assert!(
305            result.spans.contains_key(&nested_path),
306            "Missing span for nested path. Spans: {:?}",
307            result.spans
308        );
309
310        let span = result.spans.get(&nested_path).unwrap();
311        let text = &result.text[span.0..span.1];
312        assert_eq!(text, "10");
313    }
314}