Skip to main content

ocpi_tariffs_cli/
print.rs

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