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