ocpi_tariffs_cli/
print.rs

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