ocpi_tariffs_cli/
print.rs

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