1#![allow(dead_code)]
43
44use crate::Path;
45use dcbor::prelude::*;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum PathElementFormat {
50    DiagnosticSummary(Option<usize>),
52    DiagnosticFlat(Option<usize>),
55}
56
57impl Default for PathElementFormat {
58    fn default() -> Self { PathElementFormat::DiagnosticSummary(None) }
59}
60
61#[derive(Debug, Clone)]
63pub struct FormatPathsOpts {
64    indent: bool,
67
68    element_format: PathElementFormat,
71
72    last_element_only: bool,
76}
77
78impl Default for FormatPathsOpts {
79    fn default() -> Self {
84        Self {
85            indent: true,
86            element_format: PathElementFormat::default(),
87            last_element_only: false,
88        }
89    }
90}
91
92impl FormatPathsOpts {
93    pub fn new() -> Self { Self::default() }
95
96    pub fn indent(mut self, indent: bool) -> Self {
99        self.indent = indent;
100        self
101    }
102
103    pub fn element_format(mut self, format: PathElementFormat) -> Self {
106        self.element_format = format;
107        self
108    }
109
110    pub fn last_element_only(mut self, last_element_only: bool) -> Self {
114        self.last_element_only = last_element_only;
115        self
116    }
117}
118
119impl AsRef<FormatPathsOpts> for FormatPathsOpts {
120    fn as_ref(&self) -> &FormatPathsOpts { self }
121}
122
123fn format_cbor_element(
125    cbor: &CBOR,
126    format: PathElementFormat,
127) -> String {
128    match format {
129        PathElementFormat::DiagnosticSummary(max_length) => {
130            let diagnostic = cbor.summary();
131            truncate_with_ellipsis(&diagnostic, max_length)
132        }
133        PathElementFormat::DiagnosticFlat(max_length) => {
134            let diagnostic = cbor.diagnostic_flat();
135            truncate_with_ellipsis(&diagnostic, max_length)
136        }
137    }
138}
139
140fn truncate_with_ellipsis(s: &str, max_length: Option<usize>) -> String {
143    match max_length {
144        Some(max_len) if s.len() > max_len => {
145            if max_len > 1 {
146                format!("{}…", &s[0..(max_len - 1)])
147            } else {
148                "…".to_string()
149            }
150        }
151        _ => s.to_string(),
152    }
153}
154
155pub fn format_path_opt(
158    path: &Path,
159    opts: impl AsRef<FormatPathsOpts>,
160) -> String {
161    let opts = opts.as_ref();
162
163    if opts.last_element_only {
164        if let Some(element) = path.iter().last() {
166            format_cbor_element(element, opts.element_format)
167        } else {
168            String::new()
169        }
170    } else {
171        match opts.element_format {
172            PathElementFormat::DiagnosticSummary(_)
173            | PathElementFormat::DiagnosticFlat(_) => {
174                let mut lines = Vec::new();
176                for (index, element) in path.iter().enumerate() {
177                    let indent = if opts.indent {
178                        " ".repeat(index * 4)
179                    } else {
180                        String::new()
181                    };
182
183                    let content =
184                        format_cbor_element(element, opts.element_format);
185                    lines.push(format!("{}{}", indent, content));
186                }
187                lines.join("\n")
188            }
189        }
190    }
191}
192
193pub fn format_path(path: &Path) -> String {
196    format_path_opt(path, FormatPathsOpts::default())
197}
198
199pub fn format_paths_with_captures(
203    paths: &[Path],
204    captures: &std::collections::HashMap<String, Vec<Path>>,
205    opts: impl AsRef<FormatPathsOpts>,
206) -> String {
207    let opts = opts.as_ref();
208    let mut result = Vec::new();
209
210    let mut capture_names: Vec<&String> = captures.keys().collect();
212    capture_names.sort();
213
214    for capture_name in capture_names {
215        if let Some(capture_paths) = captures.get(capture_name) {
216            result.push(format!("@{}", capture_name));
217            for path in capture_paths {
218                let formatted_path = format_path_opt(path, opts);
219                for line in formatted_path.split('\n') {
221                    if !line.is_empty() {
222                        result.push(format!("    {}", line));
223                    }
224                }
225            }
226        }
227    }
228
229    for path in paths {
231        let formatted_path = format_path_opt(path, opts);
232        for line in formatted_path.split('\n') {
233            if !line.is_empty() {
234                result.push(line.to_string());
235            }
236        }
237    }
238
239    result.join("\n")
240}
241
242pub fn format_paths_opt(
244    paths: &[Path],
245    opts: impl AsRef<FormatPathsOpts>,
246) -> String {
247    format_paths_with_captures(paths, &std::collections::HashMap::new(), opts)
249}
250
251pub fn format_paths(paths: &[Path]) -> String {
253    format_paths_opt(paths, FormatPathsOpts::default())
254}
255
256#[cfg(test)]
257mod tests {
258    use dcbor::prelude::*;
259
260    use super::*;
261
262    fn create_test_path() -> Path {
263        vec![
264            CBOR::from(42),
265            CBOR::from("test"),
266            CBOR::from(vec![1, 2, 3]),
267        ]
268    }
269
270    #[test]
271    fn test_format_path_default() {
272        let path = create_test_path();
273        let formatted = format_path(&path);
274
275        assert!(formatted.contains("42"));
277        assert!(formatted.contains("\"test\""));
278        assert!(formatted.contains("[1, 2, 3]"));
279    }
280
281    #[test]
282    fn test_format_path_flat() {
283        let path = create_test_path();
284        let opts = FormatPathsOpts::new()
285            .element_format(PathElementFormat::DiagnosticFlat(None));
286        let formatted = format_path_opt(&path, opts);
287
288        assert!(formatted.contains("42"));
290        assert!(formatted.contains("\"test\""));
291        assert!(formatted.contains("[1, 2, 3]"));
292    }
293
294    #[test]
295    fn test_format_path_last_element_only() {
296        let path = create_test_path();
297        let opts = FormatPathsOpts::new().last_element_only(true);
298        let formatted = format_path_opt(&path, opts);
299
300        assert!(!formatted.contains("42"));
302        assert!(!formatted.contains("\"test\""));
303        assert!(formatted.contains("[1, 2, 3]"));
304    }
305
306    #[test]
307    fn test_truncate_with_ellipsis() {
308        assert_eq!(truncate_with_ellipsis("hello", None), "hello");
309        assert_eq!(truncate_with_ellipsis("hello", Some(10)), "hello");
310        assert_eq!(truncate_with_ellipsis("hello world", Some(5)), "hell…");
311        assert_eq!(truncate_with_ellipsis("hello", Some(1)), "…");
312    }
313
314    #[test]
315    fn test_format_paths_multiple() {
316        let path1 = vec![CBOR::from(1)];
317        let path2 = vec![CBOR::from(2)];
318        let paths = vec![path1, path2];
319
320        let formatted = format_paths(&paths);
321        let lines: Vec<&str> = formatted.split('\n').collect();
322
323        assert_eq!(lines.len(), 2);
324        assert!(lines[0].contains("1"));
325        assert!(lines[1].contains("2"));
326    }
327
328    #[test]
329    fn test_format_paths_with_captures() {
330        use std::collections::HashMap;
331
332        let path1 = vec![CBOR::from(1)];
333        let path2 = vec![CBOR::from(2)];
334        let paths = vec![path1.clone(), path2.clone()];
335
336        let mut captures = HashMap::new();
337        captures.insert("capture1".to_string(), vec![path1]);
338        captures.insert("capture2".to_string(), vec![path2]);
339
340        let formatted = format_paths_with_captures(
341            &paths,
342            &captures,
343            FormatPathsOpts::default(),
344        );
345        let lines: Vec<&str> = formatted.split('\n').collect();
346
347        assert!(lines[0] == "@capture1");
349        assert!(lines[1].contains("    1")); assert!(lines[2] == "@capture2");
351        assert!(lines[3].contains("    2")); assert!(lines[4].contains("1")); assert!(lines[5].contains("2")); }
355
356    #[test]
357    fn test_format_paths_with_empty_captures() {
358        use std::collections::HashMap;
359
360        let path1 = vec![CBOR::from(1)];
361        let path2 = vec![CBOR::from(2)];
362        let paths = vec![path1, path2];
363
364        let captures = HashMap::new();
365        let formatted = format_paths_with_captures(
366            &paths,
367            &captures,
368            FormatPathsOpts::default(),
369        );
370
371        let expected = format_paths(&paths);
373        assert_eq!(formatted, expected);
374    }
375
376    #[test]
377    fn test_capture_names_sorted() {
378        use std::collections::HashMap;
379
380        let path1 = vec![CBOR::from(1)];
381        let path2 = vec![CBOR::from(2)];
382        let path3 = vec![CBOR::from(3)];
383        let paths = vec![];
384
385        let mut captures = HashMap::new();
386        captures.insert("zebra".to_string(), vec![path1]);
387        captures.insert("alpha".to_string(), vec![path2]);
388        captures.insert("beta".to_string(), vec![path3]);
389
390        let formatted = format_paths_with_captures(
391            &paths,
392            &captures,
393            FormatPathsOpts::default(),
394        );
395        let lines: Vec<&str> = formatted.split('\n').collect();
396
397        assert!(lines[0] == "@alpha");
399        assert!(lines[2] == "@beta");
400        assert!(lines[4] == "@zebra");
401    }
402}