ocpi_tariffs_cli/
print.rs

1use std::fmt::{self, Write as _};
2
3use console::{measure_text_width, style};
4use ocpi_tariffs::UnknownFields;
5use tracing::error;
6
7/// A general purpose horizontal break
8const LINE: &str = "----------------------------------------------------------------";
9/// The initial amount of memory allocated for writing out the table.
10const TABLE_BUF_LEN: usize = 4096;
11
12/// Print the unknown fields of a CDR to stderr.
13pub fn unknown_cdr_fields(unknown_fields: UnknownFields) {
14    if unknown_fields.is_empty() {
15        return;
16    }
17
18    eprintln!(
19        "{}: {} Unknown fields found in the CDR",
20        style("WARN").yellow(),
21        unknown_fields.len(),
22    );
23
24    for field_name in unknown_fields {
25        eprintln!("  - {}", style(field_name).yellow());
26    }
27
28    let line = style(LINE).yellow();
29    eprintln!("{line}");
30}
31
32/// Print the unknown fields of a list of tariffs to stderr.
33pub fn unknown_tariff_fields(unknown_fields: Vec<(String, UnknownFields)>) {
34    if unknown_fields
35        .iter()
36        .all(|(_, unknown_fields)| unknown_fields.is_empty())
37    {
38        return;
39    }
40
41    let line = style(LINE).yellow();
42
43    eprintln!(
44        "{}: Unknown fields found in the tariffs",
45        style("WARN").yellow(),
46    );
47
48    for (tariff_id, unknown_fields) in unknown_fields {
49        if unknown_fields.is_empty() {
50            continue;
51        }
52
53        eprintln!(
54            "{}: {} Unknown fields found in the tariff with id: {}",
55            style("WARN").yellow(),
56            unknown_fields.len(),
57            style(tariff_id).yellow(),
58        );
59
60        for field_name in unknown_fields {
61            eprintln!("  - {}", style(field_name).yellow());
62        }
63
64        eprintln!("{line}");
65    }
66
67    eprintln!("{line}");
68}
69
70/// A helper for printing tables with fixed width cols.
71pub struct Table {
72    /// The widths given in the `header` fn.
73    widths: Vec<usize>,
74    /// The table is written into this buffer.
75    buf: String,
76}
77
78/// The config date for setting up a table column.
79pub struct Col<'a> {
80    pub label: &'a dyn fmt::Display,
81    pub width: usize,
82}
83
84impl Col<'_> {
85    pub fn empty(width: usize) -> Self {
86        Self { label: &"", width }
87    }
88}
89
90impl Table {
91    /// Print the table header and use the column widths for all the following rows.
92    pub fn header(header: &[Col<'_>]) -> Self {
93        let widths = header
94            .iter()
95            .map(|Col { label: _, width }| *width)
96            .collect::<Vec<_>>();
97        let mut buf = String::with_capacity(TABLE_BUF_LEN);
98        let labels = header
99            .iter()
100            .map(|Col { label, width: _ }| *label)
101            .collect::<Vec<_>>();
102
103        print_table_line(&mut buf, &widths);
104        print_table_row(&mut buf, &widths, &labels);
105        print_table_line(&mut buf, &widths);
106
107        Self { widths, buf }
108    }
109
110    /// Print a separating line.
111    pub fn print_line(&mut self) {
112        print_table_line(&mut self.buf, &self.widths);
113    }
114
115    /// Print a single row of values.
116    pub fn print_row(&mut self, values: &[&dyn fmt::Display]) {
117        print_table_row(&mut self.buf, &self.widths, values);
118    }
119
120    /// Print a single row with a label stylized based of the validity.
121    ///
122    /// If the row represents a valid value, the label is colored green.
123    /// Otherwise the label is colored red.
124    pub fn print_valid_row(
125        &mut self,
126        is_valid: bool,
127        label: &'static str,
128        values: &[&dyn fmt::Display],
129    ) {
130        let label = if is_valid {
131            style(label).green()
132        } else {
133            style(label).red()
134        };
135        print_table_row_with_label(&mut self.buf, &self.widths, &label, values);
136    }
137
138    /// Print the bottom line of the table and return the buffer for printing.
139    pub fn finish(self) -> String {
140        let Self { widths, mut buf } = self;
141        print_table_line(&mut buf, &widths);
142        buf
143    }
144}
145
146/// Just like the std lib `write!` macro except that it suppresses in `fmt::Result`.
147///
148/// This should only be used if you are in control of the buffer you're writing to
149/// and the only way it can fail is if the OS allocator fails.
150///
151/// See: <https://doc.rust-lang.org/std/io/trait.Write.html#method.write_fmt>
152#[macro_export]
153macro_rules! write_or {
154    ($dst:expr, $($arg:tt)*) => {{
155        let _ignore_result = $dst.write_fmt(std::format_args!($($arg)*));
156    }};
157}
158
159/// Print a separation line for a table.
160fn print_table_line(buf: &mut String, widths: &[usize]) {
161    write_or!(buf, "+");
162
163    for width in widths {
164        write_or!(buf, "{0:->1$}+", "", width + 2);
165    }
166
167    write_or!(buf, "\n");
168}
169
170/// Print a single row to the buffer with a label.
171fn print_table_row(buf: &mut String, widths: &[usize], values: &[&dyn fmt::Display]) {
172    assert_eq!(
173        widths.len(),
174        values.len(),
175        "The widths and values amounts should be the same"
176    );
177    print_table_row_(buf, widths, values, None);
178}
179
180/// Print a single row to the buffer with a distinct label.
181///
182/// This fn is used to create a row with a stylized label.
183fn print_table_row_with_label(
184    buf: &mut String,
185    widths: &[usize],
186    label: &dyn fmt::Display,
187    values: &[&dyn fmt::Display],
188) {
189    print_table_row_(buf, widths, values, Some(label));
190}
191
192/// Print a single row to the buffer.
193fn print_table_row_(
194    buf: &mut String,
195    widths: &[usize],
196    values: &[&dyn fmt::Display],
197    label: Option<&dyn fmt::Display>,
198) {
199    write_or!(buf, "|");
200
201    if let Some(label) = label {
202        let mut widths = widths.iter();
203        let Some(width) = widths.next() else {
204            return;
205        };
206        print_col(buf, label, *width);
207
208        for (value, width) in values.iter().zip(widths) {
209            print_col(buf, *value, *width);
210        }
211    } else {
212        for (value, width) in values.iter().zip(widths) {
213            print_col(buf, *value, *width);
214        }
215    }
216
217    write_or!(buf, "\n");
218}
219
220/// Print a single column to the buffer
221fn print_col(buf: &mut String, value: &dyn fmt::Display, width: usize) {
222    write_or!(buf, " ");
223
224    // The value could contain ANSI escape codes and the `Display` impl of the type
225    // may not implement fill and alignment logic. So we need to implement left-aligned text ourselves.
226    let len_before = buf.len();
227    write_or!(buf, "{value}");
228    let len_after = buf.len();
229
230    // Use the len before and after to capture the str just written.
231    // And compute it's visible len in the terminal.
232    let Some(s) = &buf.get(len_before..len_after) else {
233        error!("Non UTF8 values were written as a column value");
234        return;
235    };
236
237    let len = measure_text_width(s);
238    // calculate the padding we need to apply at the end of the str.
239    let padding = width.saturating_sub(len);
240
241    // and apply the padding
242    for _ in 0..padding {
243        write_or!(buf, " ");
244    }
245
246    write_or!(buf, " |");
247}