ocpi_tariffs_cli/
print.rs

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