Skip to main content

ocpi_tariffs_cli/
print.rs

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