1#![allow(dead_code)]
43
44use dcbor::prelude::*;
45
46use crate::Path;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum PathElementFormat {
51 DiagnosticSummary(Option<usize>),
53 DiagnosticFlat(Option<usize>),
56}
57
58impl Default for PathElementFormat {
59 fn default() -> Self { PathElementFormat::DiagnosticSummary(None) }
60}
61
62#[derive(Debug, Clone)]
64pub struct FormatPathsOpts {
65 indent: bool,
68
69 element_format: PathElementFormat,
72
73 last_element_only: bool,
77}
78
79impl Default for FormatPathsOpts {
80 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 pub fn new() -> Self { Self::default() }
96
97 pub fn indent(mut self, indent: bool) -> Self {
100 self.indent = indent;
101 self
102 }
103
104 pub fn element_format(mut self, format: PathElementFormat) -> Self {
107 self.element_format = format;
108 self
109 }
110
111 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
124fn 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
138fn 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
153pub 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 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 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
191pub fn format_path(path: &Path) -> String {
194 format_path_opt(path, FormatPathsOpts::default())
195}
196
197pub 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 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 for line in formatted_path.split('\n') {
219 if !line.is_empty() {
220 result.push(format!(" {}", line));
221 }
222 }
223 }
224 }
225 }
226
227 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
240pub fn format_paths_opt(
242 paths: &[Path],
243 opts: impl AsRef<FormatPathsOpts>,
244) -> String {
245 format_paths_with_captures(paths, &std::collections::HashMap::new(), opts)
247}
248
249pub 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 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 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 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 assert!(lines[0] == "@capture1");
347 assert!(lines[1].contains(" 1")); assert!(lines[2] == "@capture2");
349 assert!(lines[3].contains(" 2")); assert!(lines[4].contains("1")); assert!(lines[5].contains("2")); }
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 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 assert!(lines[0] == "@alpha");
397 assert!(lines[2] == "@beta");
398 assert!(lines[4] == "@zebra");
399 }
400}