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