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