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