1use bc_envelope::prelude::*;
2
3use crate::Path;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum PathElementFormat {
8 Summary(Option<usize>),
10 EnvelopeUR,
11 DigestUR,
12}
13
14impl Default for PathElementFormat {
15 fn default() -> Self { PathElementFormat::Summary(None) }
16}
17
18#[derive(Debug, Clone)]
20pub struct FormatPathsOpts {
21 indent: bool,
24
25 element_format: PathElementFormat,
28
29 last_element_only: bool,
33}
34
35impl Default for FormatPathsOpts {
36 fn default() -> Self {
41 Self {
42 indent: true,
43 element_format: PathElementFormat::default(),
44 last_element_only: false,
45 }
46 }
47}
48
49impl FormatPathsOpts {
50 pub fn new() -> Self { Self::default() }
52
53 pub fn indent(mut self, indent: bool) -> Self {
56 self.indent = indent;
57 self
58 }
59
60 pub fn element_format(mut self, format: PathElementFormat) -> Self {
63 self.element_format = format;
64 self
65 }
66
67 pub fn last_element_only(mut self, last_element_only: bool) -> Self {
71 self.last_element_only = last_element_only;
72 self
73 }
74}
75
76impl AsRef<FormatPathsOpts> for FormatPathsOpts {
77 fn as_ref(&self) -> &FormatPathsOpts { self }
78}
79
80pub fn envelope_summary(env: &Envelope) -> String {
81 let id = env.short_id(DigestDisplayFormat::Short);
82 let summary = match env.case() {
83 EnvelopeCase::Node { .. } => {
84 format!("NODE {}", env.format_flat())
85 }
86 EnvelopeCase::Leaf { cbor, .. } => {
87 format!(
88 "LEAF {}",
89 cbor.envelope_summary(usize::MAX, &FormatContextOpt::default())
90 .unwrap_or_else(|_| "ERROR".to_string())
91 )
92 }
93 EnvelopeCase::Wrapped { .. } => {
94 format!("WRAPPED {}", env.format_flat())
95 }
96 EnvelopeCase::Assertion(_) => {
97 format!("ASSERTION {}", env.format_flat())
98 }
99 EnvelopeCase::Elided(_) => "ELIDED".to_string(),
100 EnvelopeCase::KnownValue { value, .. } => {
101 let content = with_format_context!(|ctx: &FormatContext| {
102 let known_value = KnownValuesStore::known_value_for_raw_value(
103 value.value(),
104 Some(ctx.known_values()),
105 );
106 format!("'{}'", known_value)
107 });
108 format!("KNOWN_VALUE {}", content)
109 }
110 EnvelopeCase::Encrypted(_) => "ENCRYPTED".to_string(),
111 EnvelopeCase::Compressed(_) => "COMPRESSED".to_string(),
112 };
113 format!("{} {}", id, summary)
114}
115
116fn truncate_with_ellipsis(s: &str, max_length: Option<usize>) -> String {
119 match max_length {
120 Some(max_len) if s.len() > max_len => {
121 if max_len > 1 {
122 format!("{}…", &s[0..(max_len - 1)])
123 } else {
124 "…".to_string()
125 }
126 }
127 _ => s.to_string(),
128 }
129}
130
131pub fn format_path_opt(
134 path: &Path,
135 opts: impl AsRef<FormatPathsOpts>,
136) -> String {
137 let opts = opts.as_ref();
138
139 if opts.last_element_only {
140 if let Some(element) = path.iter().last() {
142 match opts.element_format {
143 PathElementFormat::Summary(max_length) => {
144 let summary = envelope_summary(element);
145 truncate_with_ellipsis(&summary, max_length)
146 }
147 PathElementFormat::EnvelopeUR => element.ur_string(),
148 PathElementFormat::DigestUR => element.digest().ur_string(),
149 }
150 } else {
151 String::new()
152 }
153 } else {
154 match opts.element_format {
155 PathElementFormat::Summary(max_length) => {
156 let mut lines = Vec::new();
158 for (index, element) in path.iter().enumerate() {
159 let indent = if opts.indent {
160 " ".repeat(index * 4)
161 } else {
162 String::new()
163 };
164
165 let summary = envelope_summary(element);
166 let content = truncate_with_ellipsis(&summary, max_length);
167
168 lines.push(format!("{}{}", indent, content));
169 }
170 lines.join("\n")
171 }
172 PathElementFormat::EnvelopeUR => {
173 path.iter()
175 .map(|element| element.ur_string())
176 .collect::<Vec<_>>()
177 .join(" ")
178 }
179 PathElementFormat::DigestUR => {
180 path.iter()
182 .map(|element| element.digest().ur_string())
183 .collect::<Vec<_>>()
184 .join(" ")
185 }
186 }
187 }
188}
189
190pub fn format_path(path: &Path) -> String {
193 format_path_opt(path, FormatPathsOpts::default())
194}
195
196pub fn format_paths_with_captures(
197 paths: &[Path],
198 captures: &std::collections::HashMap<String, Vec<Path>>,
199) -> String {
200 format_paths_with_captures_opt(paths, captures, FormatPathsOpts::default())
201}
202
203pub fn format_paths_with_captures_opt(
207 paths: &[Path],
208 captures: &std::collections::HashMap<String, Vec<Path>>,
209 opts: impl AsRef<FormatPathsOpts>,
210) -> String {
211 let opts = opts.as_ref();
212 let mut result = Vec::new();
213
214 let mut capture_names: Vec<&String> = captures.keys().collect();
216 capture_names.sort();
217
218 for capture_name in capture_names {
219 if let Some(capture_paths) = captures.get(capture_name) {
220 result.push(format!("@{}", capture_name));
221 for path in capture_paths {
222 let formatted_path = format_path_opt(path, opts);
223 for line in formatted_path.split('\n') {
225 if !line.is_empty() {
226 result.push(format!(" {}", line));
227 }
228 }
229 }
230 }
231 }
232
233 match opts.element_format {
235 PathElementFormat::EnvelopeUR | PathElementFormat::DigestUR => {
236 if !paths.is_empty() {
238 let formatted_paths = paths
239 .iter()
240 .map(|path| format_path_opt(path, opts))
241 .collect::<Vec<_>>()
242 .join(" ");
243 if !formatted_paths.is_empty() {
244 result.push(formatted_paths);
245 }
246 }
247 }
248 PathElementFormat::Summary(_) => {
249 for path in paths {
251 let formatted_path = format_path_opt(path, opts);
252 for line in formatted_path.split('\n') {
253 if !line.is_empty() {
254 result.push(line.to_string());
255 }
256 }
257 }
258 }
259 }
260
261 result.join("\n")
262}
263
264pub fn format_paths_opt(
266 paths: &[Path],
267 opts: impl AsRef<FormatPathsOpts>,
268) -> String {
269 format_paths_with_captures_opt(
271 paths,
272 &std::collections::HashMap::new(),
273 opts,
274 )
275}
276
277pub fn format_paths(paths: &[Path]) -> String {
279 format_paths_opt(paths, FormatPathsOpts::default())
280}
281
282#[cfg(test)]
283mod tests {
284 use std::collections::HashMap;
285
286 use bc_envelope::prelude::*;
287 use indoc::indoc;
288
289 use super::*;
290
291 fn create_test_path() -> Path {
292 vec![
293 Envelope::new(42),
294 Envelope::new("test"),
295 Envelope::new(vec![1, 2, 3]),
296 ]
297 }
298
299 #[test]
300 fn test_format_path_default() {
301 let path = create_test_path();
302 let actual = format_path(&path);
303
304 #[rustfmt::skip]
305 let expected = indoc! {r#"
306 7f83f7bd LEAF 42
307 6fe3180f LEAF "test"
308 4abc3113 LEAF [1, 2, 3]
309 "#}.trim();
310
311 assert_eq!(actual, expected, "format_path with default options");
312 }
313
314 #[test]
315 fn test_format_path_last_element_only() {
316 let path = create_test_path();
317 let opts = FormatPathsOpts::new().last_element_only(true);
318 let actual = format_path_opt(&path, opts);
319
320 #[rustfmt::skip]
321 let expected = indoc! {r#"
322 4abc3113 LEAF [1, 2, 3]
323 "#}.trim();
324
325 assert_eq!(actual, expected, "format_path with last_element_only");
326 }
327
328 #[test]
329 fn test_format_paths_multiple() {
330 let path1 = vec![Envelope::new(1)];
331 let path2 = vec![Envelope::new(2)];
332 let paths = vec![path1, path2];
333
334 let actual = format_paths(&paths);
335
336 #[rustfmt::skip]
337 let expected = indoc! {r#"
338 4bf5122f LEAF 1
339 dbc1b4c9 LEAF 2
340 "#}.trim();
341
342 assert_eq!(actual, expected, "format_paths with multiple paths");
343 }
344
345 #[test]
346 fn test_format_paths_with_captures() {
347 let path1 = vec![Envelope::new(1)];
348 let path2 = vec![Envelope::new(2)];
349 let paths = vec![path1.clone(), path2.clone()];
350
351 let mut captures = HashMap::new();
352 captures.insert("capture1".to_string(), vec![path1]);
353 captures.insert("capture2".to_string(), vec![path2]);
354
355 let actual = format_paths_with_captures_opt(
356 &paths,
357 &captures,
358 FormatPathsOpts::default(),
359 );
360
361 #[rustfmt::skip]
362 let expected = indoc! {r#"
363 @capture1
364 4bf5122f LEAF 1
365 @capture2
366 dbc1b4c9 LEAF 2
367 4bf5122f LEAF 1
368 dbc1b4c9 LEAF 2
369 "#}.trim();
370
371 assert_eq!(
372 actual, expected,
373 "format_paths_with_captures with sorted captures"
374 );
375 }
376
377 #[test]
378 fn test_format_paths_with_empty_captures() {
379 let path1 = vec![Envelope::new(1)];
380 let path2 = vec![Envelope::new(2)];
381 let paths = vec![path1, path2];
382
383 let captures = HashMap::new();
384 let formatted = format_paths_with_captures_opt(
385 &paths,
386 &captures,
387 FormatPathsOpts::default(),
388 );
389
390 let expected = format_paths(&paths);
392 assert_eq!(formatted, expected);
393 }
394
395 #[test]
396 fn test_capture_names_sorted() {
397 let path1 = vec![Envelope::new(1)];
398 let path2 = vec![Envelope::new(2)];
399 let path3 = vec![Envelope::new(3)];
400 let paths = vec![];
401
402 let mut captures = HashMap::new();
403 captures.insert("zebra".to_string(), vec![path1]);
404 captures.insert("alpha".to_string(), vec![path2]);
405 captures.insert("beta".to_string(), vec![path3]);
406
407 let actual = format_paths_with_captures_opt(
408 &paths,
409 &captures,
410 FormatPathsOpts::default(),
411 );
412
413 #[rustfmt::skip]
414 let expected = indoc! {r#"
415 @alpha
416 dbc1b4c9 LEAF 2
417 @beta
418 084fed08 LEAF 3
419 @zebra
420 4bf5122f LEAF 1
421 "#}.trim();
422
423 assert_eq!(
424 actual, expected,
425 "capture names should be sorted lexicographically"
426 );
427 }
428
429 #[test]
430 fn test_format_paths_with_captures_envelope_ur() {
431 bc_components::register_tags();
432
433 let path1 = vec![Envelope::new(1)];
434 let path2 = vec![Envelope::new(2)];
435 let paths = vec![path1.clone(), path2.clone()];
436
437 let mut captures = HashMap::new();
438 captures.insert("capture1".to_string(), vec![path1]);
439
440 let opts = FormatPathsOpts::new()
441 .element_format(PathElementFormat::EnvelopeUR);
442
443 let actual = format_paths_with_captures_opt(&paths, &captures, opts);
444
445 assert!(actual.contains("@capture1"));
448 assert!(actual.contains("ur:envelope"));
449
450 let ur_count = actual.matches("ur:envelope").count();
453 assert_eq!(ur_count, 3, "Should have 3 envelope URs total");
454 }
455
456 #[test]
457 fn test_format_paths_with_captures_digest_ur() {
458 bc_components::register_tags();
459
460 let path1 = vec![Envelope::new(1)];
461 let path2 = vec![Envelope::new(2)];
462 let paths = vec![path1.clone(), path2.clone()];
463
464 let mut captures = HashMap::new();
465 captures.insert("capture1".to_string(), vec![path1]);
466
467 let opts =
468 FormatPathsOpts::new().element_format(PathElementFormat::DigestUR);
469
470 let actual = format_paths_with_captures_opt(&paths, &captures, opts);
471
472 assert!(actual.contains("@capture1"));
475 assert!(actual.contains("ur:digest"));
476
477 let ur_count = actual.matches("ur:digest").count();
480 assert_eq!(ur_count, 3, "Should have 3 digest URs total");
481 }
482
483 #[test]
484 fn test_format_paths_with_captures_no_indent() {
485 let path1 = vec![Envelope::new(1)];
486 let paths = vec![path1.clone()];
487
488 let mut captures = HashMap::new();
489 captures.insert("capture1".to_string(), vec![path1]);
490
491 let opts = FormatPathsOpts::new().indent(false);
492
493 let actual = format_paths_with_captures_opt(&paths, &captures, opts);
494
495 #[rustfmt::skip]
496 let expected = indoc! {r#"
497 @capture1
498 4bf5122f LEAF 1
499 4bf5122f LEAF 1
500 "#}.trim();
501
502 assert_eq!(
503 actual, expected,
504 "captures should still have fixed indentation even with indent=false"
505 );
506 }
507
508 #[test]
509 fn test_truncate_with_ellipsis() {
510 assert_eq!(truncate_with_ellipsis("hello", None), "hello");
511 assert_eq!(truncate_with_ellipsis("hello", Some(10)), "hello");
512 assert_eq!(truncate_with_ellipsis("hello world", Some(5)), "hell…");
513 assert_eq!(truncate_with_ellipsis("hello", Some(1)), "…");
514 }
515}