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 crate::Path;
45use dcbor::prelude::*;
46
47/// A builder that provides formatting options for each path element.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum PathElementFormat {
50    /// Diagnostic summary format, with optional maximum length for truncation.
51    DiagnosticSummary(Option<usize>),
52    /// Flat diagnostic format (single line), with optional maximum length for
53    /// truncation.
54    DiagnosticFlat(Option<usize>),
55}
56
57impl Default for PathElementFormat {
58    fn default() -> Self { PathElementFormat::DiagnosticSummary(None) }
59}
60
61/// Options for formatting paths.
62#[derive(Debug, Clone)]
63pub struct FormatPathsOpts {
64    /// Whether to indent each path element.
65    /// If true, each element will be indented by 4 spaces per level.
66    indent: bool,
67
68    /// Format for each path element.
69    /// Default is `PathElementFormat::Diagnostic(None)`.
70    element_format: PathElementFormat,
71
72    /// If true, only the last element of each path will be formatted.
73    /// This is useful for displaying only the final destination of a path.
74    /// If false, all elements will be formatted.
75    last_element_only: bool,
76}
77
78impl Default for FormatPathsOpts {
79    /// Returns the default formatting options:
80    /// - `indent`: true
81    /// - `element_format`: PathElementFormat::Diagnostic(None)
82    /// - `last_element_only`: false
83    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    /// Creates a new FormatPathsOpts with default values.
94    pub fn new() -> Self { Self::default() }
95
96    /// Sets whether to indent each path element.
97    /// If true, each element will be indented by 4 spaces per level.
98    pub fn indent(mut self, indent: bool) -> Self {
99        self.indent = indent;
100        self
101    }
102
103    /// Sets the format for each path element.
104    /// Default is `PathElementFormat::Diagnostic(None)`.
105    pub fn element_format(mut self, format: PathElementFormat) -> Self {
106        self.element_format = format;
107        self
108    }
109
110    /// Sets whether to format only the last element of each path.
111    /// If true, only the last element will be formatted.
112    /// If false, all elements will be formatted.
113    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
123/// Format a single CBOR element according to the specified format.
124fn 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
140/// Truncates a string to the specified maximum length, appending an ellipsis if
141/// truncated. If `max_length` is None, returns the original string.
142fn 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
155/// Format each path element on its own line, each line successively indented by
156/// 4 spaces. Options can be provided to customize the formatting.
157pub 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        // Only format the last element, no indentation.
165        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                // Multi-line output with indentation for diagnostic formats.
175                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
193/// Format each path element on its own line, each line successively indented by
194/// 4 spaces.
195pub fn format_path(path: &Path) -> String {
196    format_path_opt(path, FormatPathsOpts::default())
197}
198
199/// Format multiple paths with captures in a structured way.
200/// Captures come first, sorted lexicographically by name, with their name
201/// prefixed by '@'. Regular paths follow after all captures.
202pub 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    // First, format all captures, sorted lexicographically by name
211    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                // Add indentation to each line of the formatted path
220                for line in formatted_path.split('\n') {
221                    if !line.is_empty() {
222                        result.push(format!("    {}", line));
223                    }
224                }
225            }
226        }
227    }
228
229    // Then, format all regular paths
230    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
242/// Format multiple paths with custom formatting options.
243pub fn format_paths_opt(
244    paths: &[Path],
245    opts: impl AsRef<FormatPathsOpts>,
246) -> String {
247    // Call format_paths_with_captures with empty captures
248    format_paths_with_captures(paths, &std::collections::HashMap::new(), opts)
249}
250
251/// Format multiple paths with default options.
252pub 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        // Should have indentation and default diagnostic format
276        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        // Should format with flat diagnostic
289        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        // Should only contain the last element
301        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        // Should have captures first (sorted), then regular paths
348        assert!(lines[0] == "@capture1");
349        assert!(lines[1].contains("    1")); // Indented capture content
350        assert!(lines[2] == "@capture2");
351        assert!(lines[3].contains("    2")); // Indented capture content
352        assert!(lines[4].contains("1")); // Regular path 1
353        assert!(lines[5].contains("2")); // Regular path 2
354    }
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        // Should be same as format_paths when no captures
372        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        // Should be sorted lexicographically
398        assert!(lines[0] == "@alpha");
399        assert!(lines[2] == "@beta");
400        assert!(lines[4] == "@zebra");
401    }
402}