ocpi_tariffs_cli/
print.rs

1use std::fmt::{self, Write as _};
2
3use console::{measure_text_width, style};
4use ocpi_tariffs::{
5    cdr, json,
6    price::{self, TariffReport},
7    timezone, warning,
8};
9use tracing::error;
10
11use crate::ObjectKind;
12
13/// A general purpose horizontal break
14const LINE: &str = "----------------------------------------------------------------";
15/// The initial amount of memory allocated for writing out the table.
16const TABLE_BUF_LEN: usize = 4096;
17
18/// A helper for printing `Option<T>` without creating a `String`.
19pub struct Optional<T>(pub Option<T>)
20where
21    T: fmt::Display;
22
23impl<T> fmt::Display for Optional<T>
24where
25    T: fmt::Display,
26{
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match &self.0 {
29            Some(v) => fmt::Display::fmt(v, f),
30            None => f.write_str("-"),
31        }
32    }
33}
34
35/// Print the warnings produced by a call to `ocpi-tariffs::timezone::find_or_infer`.
36pub fn timezone_warnings(cdr: &cdr::Versioned<'_>, warnings: &warning::Set<timezone::WarningKind>) {
37    if warnings.is_empty() {
38        return;
39    }
40
41    eprintln!(
42        "{}: {} warnings from the timezone search",
43        style("WARN").yellow(),
44        warnings.len(),
45    );
46
47    for warning::Group { element, warnings } in warnings.group_by_elem(cdr.as_element()) {
48        for warning in warnings {
49            eprintln!(
50                "  - path: {}: {}",
51                style(element.path()).green(),
52                style(warning).yellow()
53            );
54        }
55    }
56
57    let line = style(LINE).yellow();
58    eprintln!("{line}");
59}
60
61/// Print the unknown fields of the object to stderr.
62pub fn unexpected_fields(object: ObjectKind, unexpected_fields: &json::UnexpectedFields<'_>) {
63    if unexpected_fields.is_empty() {
64        return;
65    }
66
67    eprintln!(
68        "{}: {} Unknown fields found in the {}",
69        style("WARN").yellow(),
70        unexpected_fields.len(),
71        style(object).green()
72    );
73
74    for field_path in unexpected_fields {
75        eprintln!("  - {}", style(field_path).yellow());
76    }
77
78    let line = style(LINE).yellow();
79    eprintln!("{line}");
80}
81
82/// Print the warnings from pricing a CDR to stderr.
83pub fn cdr_warnings(warnings: price::WarningMap) {
84    if warnings.is_empty() {
85        return;
86    }
87
88    eprintln!(
89        "{}: {} warnings for the CDR",
90        style("WARN").yellow(),
91        warnings.len(),
92    );
93
94    for (elem_path, warnings) in warnings {
95        eprintln!("  {}", style(elem_path).green());
96
97        for warning in warnings {
98            eprintln!("    - {}", style(warning).yellow());
99        }
100    }
101
102    let line = style(LINE).yellow();
103    eprintln!("{line}");
104}
105
106/// Print the unexpected fields of a list of tariffs to stderr.
107pub fn tariff_reports(reports: Vec<TariffReport>) {
108    if reports.iter().all(|report| report.warnings.is_empty()) {
109        return;
110    }
111
112    let line = style(LINE).yellow();
113
114    eprintln!("{}: warnings found in tariffs", style("WARN").yellow(),);
115
116    for report in reports {
117        let TariffReport { origin, warnings } = report;
118
119        if warnings.is_empty() {
120            continue;
121        }
122
123        eprintln!(
124            "{}: {} warnings from tariff with id: {}",
125            style("WARN").yellow(),
126            warnings.len(),
127            style(origin.id).yellow(),
128        );
129
130        for (elem_path, warnings) in warnings {
131            eprintln!("  {}", style(elem_path).green());
132
133            for warning in warnings {
134                eprintln!("  - {}", style(warning).yellow());
135            }
136        }
137
138        eprintln!("{line}");
139    }
140
141    eprintln!("{line}");
142}
143
144/// A helper for printing tables with fixed width cols.
145pub struct Table {
146    /// The widths given in the `header` fn.
147    widths: Vec<usize>,
148    /// The table is written into this buffer.
149    buf: String,
150}
151
152/// The config date for setting up a table column.
153pub struct Col<'a> {
154    pub label: &'a dyn fmt::Display,
155    pub width: usize,
156}
157
158impl Col<'_> {
159    pub fn empty(width: usize) -> Self {
160        Self { label: &"", width }
161    }
162}
163
164impl Table {
165    /// Print the table header and use the column widths for all the following rows.
166    pub fn header(header: &[Col<'_>]) -> Self {
167        let widths = header
168            .iter()
169            .map(|Col { label: _, width }| *width)
170            .collect::<Vec<_>>();
171        let mut buf = String::with_capacity(TABLE_BUF_LEN);
172        let labels = header
173            .iter()
174            .map(|Col { label, width: _ }| *label)
175            .collect::<Vec<_>>();
176
177        print_table_line(&mut buf, &widths);
178        print_table_row(&mut buf, &widths, &labels);
179        print_table_line(&mut buf, &widths);
180
181        Self { widths, buf }
182    }
183
184    /// Print a separating line.
185    pub fn print_line(&mut self) {
186        print_table_line(&mut self.buf, &self.widths);
187    }
188
189    /// Print a single row of values.
190    pub fn print_row(&mut self, values: &[&dyn fmt::Display]) {
191        print_table_row(&mut self.buf, &self.widths, values);
192    }
193
194    /// Print a single row with a label stylized based of the validity.
195    ///
196    /// If the row represents a valid value, the label is colored green.
197    /// Otherwise the label is colored red.
198    pub fn print_valid_row(
199        &mut self,
200        is_valid: bool,
201        label: &'static str,
202        values: &[&dyn fmt::Display],
203    ) {
204        let label = if is_valid {
205            style(label).green()
206        } else {
207            style(label).red()
208        };
209        print_table_row_with_label(&mut self.buf, &self.widths, &label, values);
210    }
211
212    /// Print the bottom line of the table and return the buffer for printing.
213    pub fn finish(self) -> String {
214        let Self { widths, mut buf } = self;
215        print_table_line(&mut buf, &widths);
216        buf
217    }
218}
219
220/// Just like the std lib `write!` macro except that it suppresses in `fmt::Result`.
221///
222/// This should only be used if you are in control of the buffer you're writing to
223/// and the only way it can fail is if the OS allocator fails.
224///
225/// * See: <https://doc.rust-lang.org/std/io/trait.Write.html#method.write_fmt>
226#[macro_export]
227macro_rules! write_or {
228    ($dst:expr, $($arg:tt)*) => {{
229        let _ignore_result = $dst.write_fmt(std::format_args!($($arg)*));
230    }};
231}
232
233/// Print a separation line for a table.
234fn print_table_line(buf: &mut String, widths: &[usize]) {
235    write_or!(buf, "+");
236
237    for width in widths {
238        write_or!(buf, "{0:->1$}+", "", width + 2);
239    }
240
241    write_or!(buf, "\n");
242}
243
244/// Print a single row to the buffer with a label.
245fn print_table_row(buf: &mut String, widths: &[usize], values: &[&dyn fmt::Display]) {
246    assert_eq!(
247        widths.len(),
248        values.len(),
249        "The widths and values amounts should be the same"
250    );
251    print_table_row_(buf, widths, values, None);
252}
253
254/// Print a single row to the buffer with a distinct label.
255///
256/// This fn is used to create a row with a stylized label.
257fn print_table_row_with_label(
258    buf: &mut String,
259    widths: &[usize],
260    label: &dyn fmt::Display,
261    values: &[&dyn fmt::Display],
262) {
263    print_table_row_(buf, widths, values, Some(label));
264}
265
266/// Print a single row to the buffer.
267fn print_table_row_(
268    buf: &mut String,
269    widths: &[usize],
270    values: &[&dyn fmt::Display],
271    label: Option<&dyn fmt::Display>,
272) {
273    write_or!(buf, "|");
274
275    if let Some(label) = label {
276        let mut widths = widths.iter();
277        let Some(width) = widths.next() else {
278            return;
279        };
280        print_col(buf, label, *width);
281
282        for (value, width) in values.iter().zip(widths) {
283            print_col(buf, *value, *width);
284        }
285    } else {
286        for (value, width) in values.iter().zip(widths) {
287            print_col(buf, *value, *width);
288        }
289    }
290
291    write_or!(buf, "\n");
292}
293
294/// Print a single column to the buffer
295fn print_col(buf: &mut String, value: &dyn fmt::Display, width: usize) {
296    write_or!(buf, " ");
297
298    // The value could contain ANSI escape codes and the `Display` impl of the type
299    // may not implement fill and alignment logic. So we need to implement left-aligned text ourselves.
300    let len_before = buf.len();
301    write_or!(buf, "{value}");
302    let len_after = buf.len();
303
304    // Use the len before and after to capture the str just written.
305    // And compute it's visible len in the terminal.
306    let Some(s) = &buf.get(len_before..len_after) else {
307        error!("Non UTF8 values were written as a column value");
308        return;
309    };
310
311    let len = measure_text_width(s);
312    // calculate the padding we need to apply at the end of the str.
313    let padding = width.saturating_sub(len);
314
315    // and apply the padding
316    for _ in 0..padding {
317        write_or!(buf, " ");
318    }
319
320    write_or!(buf, " |");
321}