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