dcbor_pattern/
format.rs

1//! # Format Module for dcbor-pattern
2//!
3//! This module provides formatting utilities for displaying paths returned by
4//! pattern matching. Unlike `bc-envelope-pattern` which supports digest URs and
5//! envelope URs, this module focuses on CBOR diagnostic notation formatting.
6//!
7//! ## Features
8//!
9//! - **Diagnostic formatting**: Format CBOR elements using either standard or
10//!   flat diagnostic notation
11//! - **Path indentation**: Automatically indent nested path elements
12//! - **Truncation support**: Optionally truncate long representations with
13//!   ellipsis
14//! - **Flexible options**: Choose whether to show all elements or just the
15//!   final destination
16//!
17//! ## Usage
18//!
19//! ```rust
20//! use dcbor::prelude::*;
21//! use dcbor_pattern::{
22//!     FormatPathsOpts, PathElementFormat, format_paths, format_paths_opt,
23//! };
24//!
25//! // Create a path (normally this would come from pattern matching)
26//! let path = vec![
27//!     CBOR::from(42),
28//!     CBOR::from("hello"),
29//!     CBOR::from(vec![1, 2, 3]),
30//! ];
31//!
32//! // Default formatting (indented, full diagnostic)
33//! println!("{}", format_paths(&[path.clone()]));
34//!
35//! // Custom formatting options
36//! let opts = FormatPathsOpts::new()
37//!     .element_format(PathElementFormat::DiagnosticFlat(Some(20)))
38//!     .last_element_only(true);
39//! println!("{}", format_paths_opt(&[path], opts));
40//! ```
41
42#![allow(dead_code)]
43
44use dcbor::prelude::*;
45
46use crate::Path;
47
48/// A builder that provides formatting options for each path element.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PathElementFormat {
51    /// Diagnostic summary format, with optional maximum length for truncation.
52    DiagnosticSummary(Option<usize>),
53    /// Flat diagnostic format (single line), with optional maximum length for
54    /// truncation.
55    DiagnosticFlat(Option<usize>),
56}
57
58impl Default for PathElementFormat {
59    fn default() -> Self { PathElementFormat::DiagnosticSummary(None) }
60}
61
62/// Options for formatting paths.
63#[derive(Debug, Clone)]
64pub struct FormatPathsOpts {
65    /// Whether to indent each path element.
66    /// If true, each element will be indented by 4 spaces per level.
67    indent: bool,
68
69    /// Format for each path element.
70    /// Default is `PathElementFormat::Diagnostic(None)`.
71    element_format: PathElementFormat,
72
73    /// If true, only the last element of each path will be formatted.
74    /// This is useful for displaying only the final destination of a path.
75    /// If false, all elements will be formatted.
76    last_element_only: bool,
77}
78
79impl Default for FormatPathsOpts {
80    /// Returns the default formatting options:
81    /// - `indent`: true
82    /// - `element_format`: PathElementFormat::Diagnostic(None)
83    /// - `last_element_only`: false
84    fn default() -> Self {
85        Self {
86            indent: true,
87            element_format: PathElementFormat::default(),
88            last_element_only: false,
89        }
90    }
91}
92
93impl FormatPathsOpts {
94    /// Creates a new FormatPathsOpts with default values.
95    pub fn new() -> Self { Self::default() }
96
97    /// Sets whether to indent each path element.
98    /// If true, each element will be indented by 4 spaces per level.
99    pub fn indent(mut self, indent: bool) -> Self {
100        self.indent = indent;
101        self
102    }
103
104    /// Sets the format for each path element.
105    /// Default is `PathElementFormat::Diagnostic(None)`.
106    pub fn element_format(mut self, format: PathElementFormat) -> Self {
107        self.element_format = format;
108        self
109    }
110
111    /// Sets whether to format only the last element of each path.
112    /// If true, only the last element will be formatted.
113    /// If false, all elements will be formatted.
114    pub fn last_element_only(mut self, last_element_only: bool) -> Self {
115        self.last_element_only = last_element_only;
116        self
117    }
118}
119
120impl AsRef<FormatPathsOpts> for FormatPathsOpts {
121    fn as_ref(&self) -> &FormatPathsOpts { self }
122}
123
124/// Format a single CBOR element according to the specified format.
125fn format_cbor_element(cbor: &CBOR, format: PathElementFormat) -> String {
126    match format {
127        PathElementFormat::DiagnosticSummary(max_length) => {
128            let diagnostic = cbor.summary();
129            truncate_with_ellipsis(&diagnostic, max_length)
130        }
131        PathElementFormat::DiagnosticFlat(max_length) => {
132            let diagnostic = cbor.diagnostic_flat();
133            truncate_with_ellipsis(&diagnostic, max_length)
134        }
135    }
136}
137
138/// Truncates a string to the specified maximum length, appending an ellipsis if
139/// truncated. If `max_length` is None, returns the original string.
140fn truncate_with_ellipsis(s: &str, max_length: Option<usize>) -> String {
141    match max_length {
142        Some(max_len) if s.len() > max_len => {
143            if max_len > 1 {
144                format!("{}…", &s[0..(max_len - 1)])
145            } else {
146                "…".to_string()
147            }
148        }
149        _ => s.to_string(),
150    }
151}
152
153/// Format each path element on its own line, each line successively indented by
154/// 4 spaces. Options can be provided to customize the formatting.
155pub fn format_path_opt(
156    path: &Path,
157    opts: impl AsRef<FormatPathsOpts>,
158) -> String {
159    let opts = opts.as_ref();
160
161    if opts.last_element_only {
162        // Only format the last element, no indentation.
163        if let Some(element) = path.iter().last() {
164            format_cbor_element(element, opts.element_format)
165        } else {
166            String::new()
167        }
168    } else {
169        match opts.element_format {
170            PathElementFormat::DiagnosticSummary(_)
171            | PathElementFormat::DiagnosticFlat(_) => {
172                // Multi-line output with indentation for diagnostic formats.
173                let mut lines = Vec::new();
174                for (index, element) in path.iter().enumerate() {
175                    let indent = if opts.indent {
176                        " ".repeat(index * 4)
177                    } else {
178                        String::new()
179                    };
180
181                    let content =
182                        format_cbor_element(element, opts.element_format);
183                    lines.push(format!("{}{}", indent, content));
184                }
185                lines.join("\n")
186            }
187        }
188    }
189}
190
191/// Format each path element on its own line, each line successively indented by
192/// 4 spaces.
193pub fn format_path(path: &Path) -> String {
194    format_path_opt(path, FormatPathsOpts::default())
195}
196
197/// Format multiple paths with captures in a structured way.
198/// Captures come first, sorted lexicographically by name, with their name
199/// prefixed by '@'. Regular paths follow after all captures.
200pub fn format_paths_with_captures(
201    paths: &[Path],
202    captures: &std::collections::HashMap<String, Vec<Path>>,
203    opts: impl AsRef<FormatPathsOpts>,
204) -> String {
205    let opts = opts.as_ref();
206    let mut result = Vec::new();
207
208    // First, format all captures, sorted lexicographically by name
209    let mut capture_names: Vec<&String> = captures.keys().collect();
210    capture_names.sort();
211
212    for capture_name in capture_names {
213        if let Some(capture_paths) = captures.get(capture_name) {
214            result.push(format!("@{}", capture_name));
215            for path in capture_paths {
216                let formatted_path = format_path_opt(path, opts);
217                // Add indentation to each line of the formatted path
218                for line in formatted_path.split('\n') {
219                    if !line.is_empty() {
220                        result.push(format!("    {}", line));
221                    }
222                }
223            }
224        }
225    }
226
227    // Then, format all regular paths
228    for path in paths {
229        let formatted_path = format_path_opt(path, opts);
230        for line in formatted_path.split('\n') {
231            if !line.is_empty() {
232                result.push(line.to_string());
233            }
234        }
235    }
236
237    result.join("\n")
238}
239
240/// Format multiple paths with custom formatting options.
241pub fn format_paths_opt(
242    paths: &[Path],
243    opts: impl AsRef<FormatPathsOpts>,
244) -> String {
245    // Call format_paths_with_captures with empty captures
246    format_paths_with_captures(paths, &std::collections::HashMap::new(), opts)
247}
248
249/// Format multiple paths with default options.
250pub fn format_paths(paths: &[Path]) -> String {
251    format_paths_opt(paths, FormatPathsOpts::default())
252}
253
254#[cfg(test)]
255mod tests {
256    use dcbor::prelude::*;
257
258    use super::*;
259
260    fn create_test_path() -> Path {
261        vec![
262            CBOR::from(42),
263            CBOR::from("test"),
264            CBOR::from(vec![1, 2, 3]),
265        ]
266    }
267
268    #[test]
269    fn test_format_path_default() {
270        let path = create_test_path();
271        let formatted = format_path(&path);
272
273        // Should have indentation and default diagnostic format
274        assert!(formatted.contains("42"));
275        assert!(formatted.contains("\"test\""));
276        assert!(formatted.contains("[1, 2, 3]"));
277    }
278
279    #[test]
280    fn test_format_path_flat() {
281        let path = create_test_path();
282        let opts = FormatPathsOpts::new()
283            .element_format(PathElementFormat::DiagnosticFlat(None));
284        let formatted = format_path_opt(&path, opts);
285
286        // Should format with flat diagnostic
287        assert!(formatted.contains("42"));
288        assert!(formatted.contains("\"test\""));
289        assert!(formatted.contains("[1, 2, 3]"));
290    }
291
292    #[test]
293    fn test_format_path_last_element_only() {
294        let path = create_test_path();
295        let opts = FormatPathsOpts::new().last_element_only(true);
296        let formatted = format_path_opt(&path, opts);
297
298        // Should only contain the last element
299        assert!(!formatted.contains("42"));
300        assert!(!formatted.contains("\"test\""));
301        assert!(formatted.contains("[1, 2, 3]"));
302    }
303
304    #[test]
305    fn test_truncate_with_ellipsis() {
306        assert_eq!(truncate_with_ellipsis("hello", None), "hello");
307        assert_eq!(truncate_with_ellipsis("hello", Some(10)), "hello");
308        assert_eq!(truncate_with_ellipsis("hello world", Some(5)), "hell…");
309        assert_eq!(truncate_with_ellipsis("hello", Some(1)), "…");
310    }
311
312    #[test]
313    fn test_format_paths_multiple() {
314        let path1 = vec![CBOR::from(1)];
315        let path2 = vec![CBOR::from(2)];
316        let paths = vec![path1, path2];
317
318        let formatted = format_paths(&paths);
319        let lines: Vec<&str> = formatted.split('\n').collect();
320
321        assert_eq!(lines.len(), 2);
322        assert!(lines[0].contains("1"));
323        assert!(lines[1].contains("2"));
324    }
325
326    #[test]
327    fn test_format_paths_with_captures() {
328        use std::collections::HashMap;
329
330        let path1 = vec![CBOR::from(1)];
331        let path2 = vec![CBOR::from(2)];
332        let paths = vec![path1.clone(), path2.clone()];
333
334        let mut captures = HashMap::new();
335        captures.insert("capture1".to_string(), vec![path1]);
336        captures.insert("capture2".to_string(), vec![path2]);
337
338        let formatted = format_paths_with_captures(
339            &paths,
340            &captures,
341            FormatPathsOpts::default(),
342        );
343        let lines: Vec<&str> = formatted.split('\n').collect();
344
345        // Should have captures first (sorted), then regular paths
346        assert!(lines[0] == "@capture1");
347        assert!(lines[1].contains("    1")); // Indented capture content
348        assert!(lines[2] == "@capture2");
349        assert!(lines[3].contains("    2")); // Indented capture content
350        assert!(lines[4].contains("1")); // Regular path 1
351        assert!(lines[5].contains("2")); // Regular path 2
352    }
353
354    #[test]
355    fn test_format_paths_with_empty_captures() {
356        use std::collections::HashMap;
357
358        let path1 = vec![CBOR::from(1)];
359        let path2 = vec![CBOR::from(2)];
360        let paths = vec![path1, path2];
361
362        let captures = HashMap::new();
363        let formatted = format_paths_with_captures(
364            &paths,
365            &captures,
366            FormatPathsOpts::default(),
367        );
368
369        // Should be same as format_paths when no captures
370        let expected = format_paths(&paths);
371        assert_eq!(formatted, expected);
372    }
373
374    #[test]
375    fn test_capture_names_sorted() {
376        use std::collections::HashMap;
377
378        let path1 = vec![CBOR::from(1)];
379        let path2 = vec![CBOR::from(2)];
380        let path3 = vec![CBOR::from(3)];
381        let paths = vec![];
382
383        let mut captures = HashMap::new();
384        captures.insert("zebra".to_string(), vec![path1]);
385        captures.insert("alpha".to_string(), vec![path2]);
386        captures.insert("beta".to_string(), vec![path3]);
387
388        let formatted = format_paths_with_captures(
389            &paths,
390            &captures,
391            FormatPathsOpts::default(),
392        );
393        let lines: Vec<&str> = formatted.split('\n').collect();
394
395        // Should be sorted lexicographically
396        assert!(lines[0] == "@alpha");
397        assert!(lines[2] == "@beta");
398        assert!(lines[4] == "@zebra");
399    }
400}