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, suitable for miette)
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    fn new() -> Self {
62        Self {
63            output: String::new(),
64            spans: BTreeMap::new(),
65            indent: 0,
66        }
67    }
68
69    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    }
200
201    let end = ctx.len();
202    ctx.record_span(current_path, start, end);
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{VArray, VObject, VString};
209
210    #[test]
211    fn test_format_primitives() {
212        assert_eq!(format_value(&Value::NULL), "null");
213        assert_eq!(format_value(&Value::TRUE), "true");
214        assert_eq!(format_value(&Value::FALSE), "false");
215        assert_eq!(format_value(&Value::from(42i64)), "42");
216        assert_eq!(
217            format_value(&Value::from(VString::new("hello"))),
218            "\"hello\""
219        );
220    }
221
222    #[test]
223    fn test_format_array() {
224        let mut arr = VArray::new();
225        arr.push(Value::from(1i64));
226        arr.push(Value::from(2i64));
227        arr.push(Value::from(3i64));
228        let value: Value = arr.into();
229
230        let result = format_value_with_spans(&value);
231        assert!(result.text.contains("1"));
232        assert!(result.text.contains("2"));
233        assert!(result.text.contains("3"));
234
235        // Check that array elements have spans
236        let path_0 = vec![PathSegment::Index(0)];
237        assert!(result.spans.contains_key(&path_0));
238    }
239
240    #[test]
241    fn test_format_object() {
242        let mut obj = VObject::new();
243        obj.insert("name", Value::from(VString::new("Alice")));
244        obj.insert("age", Value::from(30i64));
245        let value: Value = obj.into();
246
247        let result = format_value_with_spans(&value);
248        assert!(result.text.contains("\"name\""));
249        assert!(result.text.contains("\"Alice\""));
250        assert!(result.text.contains("\"age\""));
251        assert!(result.text.contains("30"));
252
253        // Check that object fields have spans
254        let name_path = vec![PathSegment::Key("name".into())];
255        let age_path = vec![PathSegment::Key("age".into())];
256        assert!(
257            result.spans.contains_key(&name_path),
258            "Missing span for 'name'"
259        );
260        assert!(
261            result.spans.contains_key(&age_path),
262            "Missing span for 'age'"
263        );
264
265        // Verify the span content
266        let age_span = result.spans.get(&age_path).unwrap();
267        let age_text = &result.text[age_span.0..age_span.1];
268        assert_eq!(age_text, "30");
269    }
270
271    #[test]
272    fn test_format_nested() {
273        let mut inner = VObject::new();
274        inner.insert("x", Value::from(10i64));
275
276        let mut outer = VObject::new();
277        outer.insert("point", Value::from(inner));
278        let value: Value = outer.into();
279
280        let result = format_value_with_spans(&value);
281
282        // Check nested path
283        let nested_path = vec![
284            PathSegment::Key("point".into()),
285            PathSegment::Key("x".into()),
286        ];
287        assert!(
288            result.spans.contains_key(&nested_path),
289            "Missing span for nested path. Spans: {:?}",
290            result.spans
291        );
292
293        let span = result.spans.get(&nested_path).unwrap();
294        let text = &result.text[span.0..span.1];
295        assert_eq!(text, "10");
296    }
297}