Skip to main content

veilid_tools/
formatting.rs

1use super::*;
2
3use indent::{indent_all_by, indent_by};
4
5//////////////////////////////////////////////////////////////////////////////////////////////////////////////
6// Indentation
7
8/// Spaces prepended by [`indent_string`] and [`indent_all_string`]
9pub const DEFAULT_INDENT_SPACES: usize = 4;
10
11/// Indent all but the first line of `s`
12#[must_use]
13pub fn indent_string<S: AsRef<str>>(s: S) -> String {
14    indent_by(DEFAULT_INDENT_SPACES, s.as_ref().to_owned())
15}
16
17/// Indent every line of `s`
18#[must_use]
19pub fn indent_all_string<S: AsRef<str>>(s: S) -> String {
20    indent_all_by(DEFAULT_INDENT_SPACES, s.as_ref().to_owned())
21}
22
23//////////////////////////////////////////////////////////////////////////////////////////////////////////////
24// Newline stripping
25
26/// Strip a trailing `\n` or `\r\n` from a string-like value.
27pub trait StripTrailingNewline {
28    /// Return the string with a single trailing `\n` or `\r\n` removed.
29    fn strip_trailing_newline(&self) -> &str;
30}
31
32impl<T: AsRef<str>> StripTrailingNewline for T {
33    fn strip_trailing_newline(&self) -> &str {
34        self.as_ref()
35            .strip_suffix("\r\n")
36            .or_else(|| self.as_ref().strip_suffix('\n'))
37            .unwrap_or(self.as_ref())
38    }
39}
40
41//////////////////////////////////////////////////////////////////////////////////////////////////////////////
42// Human display formatting
43
44/// Format a byte count as a human-readable string with binary (1024-based) GB/MB/KB/B units.
45#[must_use]
46pub fn human_byte_count<T: Into<u64>>(count: T) -> String {
47    let count = count.into();
48    if count >= 1024u64 * 1024u64 * 1024u64 {
49        format!("{:.3}GB", (count / (1024u64 * 1024u64)) as f64 / 1024.0)
50    } else if count >= 1024u64 * 1024u64 {
51        format!("{:.3}MB", (count / 1024u64) as f64 / 1024.0)
52    } else if count >= 1024u64 {
53        format!("{:.3}KB", count as f64 / 1024.0)
54    } else {
55        format!("{}B", count)
56    }
57}
58
59/// Format a byte slice for display: ASCII-printable data is shown as text, otherwise as shell-quoted
60/// text or hex (whichever is shorter). Truncated to `truncate_len` bytes with a trailing `...` when set.
61#[must_use]
62pub fn human_byte_data<T: AsRef<[u8]>>(data: T, truncate_len: Option<usize>) -> String {
63    let data = data.as_ref();
64
65    // check if message body is ascii printable
66    let mut printable = true;
67    for c in data {
68        if *c < 32 || *c > 126 {
69            printable = false;
70            break;
71        }
72    }
73
74    let (data, truncated) = if let Some(truncate_len) = truncate_len {
75        if data.len() > truncate_len {
76            (&data[0..truncate_len], true)
77        } else {
78            (data, false)
79        }
80    } else {
81        (data, false)
82    };
83
84    let strdata = if printable {
85        String::from_utf8_lossy(data).to_string()
86    } else {
87        let sw = shell_words::quote(String::from_utf8_lossy(data).as_ref()).to_string();
88        let always_hex = sw
89            .chars()
90            .any(|c| !c.is_ascii() || c.is_ascii_control() || c.is_ascii_graphic());
91        let h = hex::encode(data);
92        if always_hex || h.len() < sw.len() {
93            h
94        } else {
95            sw
96        }
97    };
98    if truncated {
99        format!("{}...", strdata)
100    } else {
101        strdata
102    }
103}
104
105/// Format [`fmt::Display`] values to [`String`], respecting a formatter-style alternate flag.
106///
107/// Implemented for [`fmt::Formatter`] (uses [`fmt::Formatter::alternate`]), [`DefaultDisplayFormat`]
108/// (`{}` only), and [`AlternateDisplayFormat`] (`{:#}` only). List helpers match the behavior of the
109/// former `ToMultilineString` / `ToTableString` traits from `veilid-core`.
110pub trait FormatterToString {
111    /// Format values to [`String`], respecting a formatter-style alternate flag.
112    fn to_string<T: fmt::Display>(&self, value: T) -> String;
113
114    /// Format `Option<>` values to [`String`], using [`FormatterToString::to_string`] for the inner value.
115    fn to_string_opt<T: fmt::Display>(&self, value: Option<T>) -> String {
116        if let Some(ts) = value {
117            self.to_string(ts)
118        } else {
119            "None".to_string()
120        }
121    }
122
123    /// One line per item, each formatted via [`FormatterToString::to_string`].
124    fn to_multiline_string<'a, T, I>(&self, items: I) -> String
125    where
126        T: fmt::Display + 'a,
127        I: IntoIterator<Item = T>,
128    {
129        let mut out = Vec::new();
130        for x in items {
131            out.push(self.to_string(x));
132        }
133        out.join("\n")
134    }
135
136    /// Like [`FormatterToString::to_multiline_string`], but each item is prefixed with its index and
137    /// indented (four spaces per line of the item's string form).
138    fn to_multiline_indexed_string<'a, T, I>(&self, items: I) -> String
139    where
140        T: fmt::Display + 'a,
141        I: IntoIterator<Item = T>,
142    {
143        let mut out = Vec::new();
144        for (n, x) in items.into_iter().enumerate() {
145            let block = self.to_string(x);
146            if block.contains('\n') {
147                out.push(format!("{n}:"));
148                out.push(indent_all_by(DEFAULT_INDENT_SPACES, block));
149            } else {
150                out.push(format!("{n}: {block}"));
151            }
152        }
153        out.join("\n")
154    }
155
156    /// Format items as a bracketed table with default layout (32 columns, 4-space indent).
157    fn to_table_string<T: fmt::Display>(&self, items: &[T]) -> String {
158        self.to_table_string_custom(items, 32, 4, true, true)
159    }
160
161    /// Format items as a comma-separated table. `columns` items per row (0 disables wrapping),
162    /// each row indented by `indent` spaces, optionally wrapped in `[]` and split across lines.
163    fn to_table_string_custom<T: fmt::Display, I: IntoIterator<Item = T>>(
164        &self,
165        items: I,
166        columns: usize,
167        indent: usize,
168        brackets: bool,
169        newlines: bool,
170    ) -> String {
171        let strings: Vec<String> = items.into_iter().map(|s| self.to_string(s)).collect();
172        table_string_from_strings(&strings, columns, indent, brackets, newlines)
173    }
174}
175
176fn table_string_from_strings(
177    strings: &[String],
178    columns: usize,
179    indent: usize,
180    brackets: bool,
181    newlines: bool,
182) -> String {
183    let len = strings.len();
184    let mut col = 0usize;
185    let mut out = String::new();
186    let mut left = len;
187
188    let use_columns = columns > 0 && columns < len;
189
190    if brackets {
191        out.push('[');
192    }
193    if use_columns && newlines {
194        out.push('\n');
195    }
196    let indent_str = " ".repeat(indent);
197
198    for sc in strings {
199        if use_columns && col == 0 {
200            out.push_str(&indent_str);
201        }
202        out.push_str(sc);
203        col += 1;
204        left -= 1;
205        if left != 0 {
206            out.push(',');
207            if use_columns && col == columns {
208                col = 0;
209                out.push('\n');
210            }
211        }
212    }
213    if use_columns && newlines {
214        out.push('\n');
215    }
216    if brackets {
217        out.push(']');
218    }
219    out
220}
221
222impl FormatterToString for fmt::Formatter<'_> {
223    fn to_string<T: fmt::Display>(&self, value: T) -> String {
224        if self.alternate() {
225            format!("{:#}", value)
226        } else {
227            format!("{}", value)
228        }
229    }
230}
231
232/// Formats values with [`fmt::Display`] using `{}` only (ignores alternate form).
233#[derive(Debug, Clone, Copy, Default)]
234pub struct DefaultDisplayFormat;
235
236impl FormatterToString for DefaultDisplayFormat {
237    fn to_string<T: fmt::Display>(&self, value: T) -> String {
238        value.to_string()
239    }
240}
241
242/// Formats values with [`fmt::Display`] using `{:#}` (alternate form always on).
243#[derive(Debug, Clone, Copy, Default)]
244pub struct AlternateDisplayFormat;
245
246impl FormatterToString for AlternateDisplayFormat {
247    fn to_string<T: fmt::Display>(&self, value: T) -> String {
248        format!("{:#}", value)
249    }
250}
251
252/// Generate multiline human-readable output
253pub trait ToMultilineString {
254    /// One item per line.
255    fn to_multiline_string(&self) -> String;
256    /// One item per line, each prefixed with its index.
257    fn to_multiline_indexed_string(&self) -> String;
258}
259
260impl<T> ToMultilineString for Vec<T>
261where
262    T: fmt::Display,
263{
264    fn to_multiline_string(&self) -> String {
265        AlternateDisplayFormat.to_multiline_string(self.iter())
266    }
267
268    fn to_multiline_indexed_string(&self) -> String {
269        AlternateDisplayFormat.to_multiline_indexed_string(self.iter())
270    }
271}
272
273/// Trait for printing values in a human-readable table format
274pub trait ToTableString {
275    /// Format with default layout (32 columns, 4-space indent, brackets, newlines).
276    fn to_table_string(&self) -> String {
277        self.to_table_string_custom(32, 4, true, true)
278    }
279    /// Format with explicit column count, indent, bracketing, and line-wrapping.
280    fn to_table_string_custom(
281        &self,
282        columns: usize,
283        indent: usize,
284        brackets: bool,
285        newlines: bool,
286    ) -> String;
287}
288
289impl<T> ToTableString for Vec<T>
290where
291    T: fmt::Display,
292{
293    fn to_table_string_custom(
294        &self,
295        columns: usize,
296        indent: usize,
297        brackets: bool,
298        newlines: bool,
299    ) -> String {
300        AlternateDisplayFormat.to_table_string_custom(self, columns, indent, brackets, newlines)
301    }
302}
303
304/// Substitute a placeholder for an empty string.
305pub trait StringIfEmpty<X: ToString> {
306    /// Return `empty_string` if this is empty, otherwise the value itself.
307    fn string_if_empty(&self, empty_string: X) -> String;
308}
309
310impl<S: AsRef<str>, X: ToString> StringIfEmpty<X> for S {
311    fn string_if_empty(&self, empty_string: X) -> String {
312        let s = self.as_ref();
313        if s.is_empty() {
314            empty_string.to_string()
315        } else {
316            s.to_string()
317        }
318    }
319}
320
321//////////////////////////////////////////////////////////////////////////////////////////////////////////////
322// Misc string tools
323
324/// Split a `host:port` string into host and optional port at the last `:`.
325///
326/// Errors with a `String` if a `:` is present but the trailing portion does not parse as a `u16`.
327pub fn split_port(name: &str) -> Result<(String, Option<u16>), String> {
328    if let Some(split) = name.rfind(':') {
329        let hoststr = &name[0..split];
330        let portstr = &name[split + 1..];
331        let port: u16 = portstr
332            .parse::<u16>()
333            .map_err(|e| format!("invalid port: {}", e))?;
334
335        Ok((hoststr.to_string(), Some(port)))
336    } else {
337        Ok((name.to_string(), None))
338    }
339}
340
341/// Prepend a `/` unless the string already starts with one.
342#[must_use]
343pub fn prepend_slash(s: String) -> String {
344    if s.starts_with('/') {
345        return s;
346    }
347    let mut out = "/".to_owned();
348    out.push_str(s.as_str());
349    out
350}
351
352/// Call [`ToString::to_string`], for use as a `.map()` argument.
353pub fn map_to_string<X: ToString>(arg: X) -> String {
354    arg.to_string()
355}
356
357#[cfg(test)]
358mod formatter_to_string_tests {
359    use super::*;
360
361    #[derive(Clone, Copy)]
362    struct AltEcho(u32);
363
364    impl fmt::Display for AltEcho {
365        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366            if f.alternate() {
367                write!(f, "[{}]", self.0)
368            } else {
369                write!(f, "{}", self.0)
370            }
371        }
372    }
373
374    struct Wrap<'a, T: fmt::Display>(&'a T);
375
376    impl<T: fmt::Display> fmt::Display for Wrap<'_, T> {
377        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378            f.write_str(&f.to_string(self.0))
379        }
380    }
381
382    #[test]
383    fn nested_format_inherits_alternate() {
384        assert_eq!(format!("{}", Wrap(&AltEcho(3))), "3");
385        assert_eq!(format!("{:#}", Wrap(&AltEcho(3))), "[3]");
386    }
387
388    #[test]
389    fn default_table_string_matches_single_line_when_short() {
390        let v = [1u32, 2, 3];
391        assert_eq!(AlternateDisplayFormat.to_table_string(&v), "[1,2,3]");
392    }
393
394    #[test]
395    fn alternate_indexed_multiline_uses_hash_flag() {
396        let v = [AltEcho(7)];
397        let s = AlternateDisplayFormat.to_multiline_indexed_string(v.iter());
398        assert!(s.contains("[7]"), "got {:?}", s);
399    }
400}