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