1use std::fmt;
10
11#[derive(Clone, Debug, Default, Eq, PartialEq)]
18pub struct Fields<'a> {
19 pub rows: Vec<(&'a str, String)>,
21}
22
23impl<'a> Fields<'a> {
24 pub fn push(&mut self, label: &'a str, value: impl fmt::Display) {
26 self.rows.push((label, value.to_string()));
27 }
28}
29
30impl fmt::Display for Fields<'_> {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 let width = self.rows.iter().map(|(label, _)| label.chars().count());
33 let width = width.max().unwrap_or_default();
34 let indent = width + 3;
35
36 for (label, value) in &self.rows {
37 let mut lines = value.lines();
38
39 match lines.next().filter(|line| !line.is_empty()) {
40 Some(first) => {
41 let pad = width - label.chars().count() + 2;
42 writeln!(f, "{label}:{:pad$}{first}", "")?;
43 }
44 None => writeln!(f, "{label}:")?,
45 }
46
47 for line in lines {
48 match line {
49 "" => writeln!(f)?,
50 line => writeln!(f, "{:indent$}{line}", "")?,
51 }
52 }
53 }
54
55 Ok(())
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use crate::fields::*;
62
63 #[test]
64 fn values_align_past_the_longest_label() {
65 let mut fields = Fields::default();
66 fields.push("Type", "Bearer");
67 fields.push("Refresh token", "yes");
68 fields.push("Scopes", "mail\ncontacts");
69 fields.push("Note", "");
70
71 let expected = "\
72Type: Bearer
73Refresh token: yes
74Scopes: mail
75 contacts
76Note:
77";
78 assert_eq!(fields.to_string(), expected);
79 }
80
81 #[test]
82 fn an_empty_record_prints_nothing() {
83 assert_eq!(Fields::default().to_string(), "");
84 }
85}