Skip to main content

ruso_runtime/
util.rs

1//! Shared utility helpers with no domain dependencies.
2
3/// Truncate `input` to at most `max_len` bytes, snapping back to the nearest
4/// UTF-8 character boundary so multi-byte sequences are never split.
5///
6/// When the input is longer than `max_len`, an ellipsis `…` is appended.
7pub fn truncate_str(input: &str, max_len: usize) -> String {
8    if input.len() <= max_len {
9        return input.to_string();
10    }
11    let mut end = max_len;
12    while end > 0 && !input.is_char_boundary(end) {
13        end -= 1;
14    }
15    format!("{}…", &input[..end])
16}
17
18#[cfg(test)]
19mod tests {
20    use super::truncate_str;
21
22    #[test]
23    fn short_input_unchanged() {
24        assert_eq!(truncate_str("hi", 10), "hi");
25    }
26
27    #[test]
28    fn ascii_truncates_with_ellipsis() {
29        let out = truncate_str("abcdefghij", 5);
30        assert_eq!(out, "abcde…");
31    }
32
33    #[test]
34    fn does_not_panic_on_multibyte_boundary() {
35        // "日本語" is 9 UTF-8 bytes (3 chars × 3 bytes). max_len=4 falls
36        // mid-character; the unfixed implementation would panic.
37        let out = truncate_str("日本語テキスト", 4);
38        // We snap back to char boundary 3 (after first char), giving "日…".
39        assert_eq!(out, "日…");
40    }
41
42    #[test]
43    fn handles_emoji() {
44        // 🦀 is 4 bytes. max_len=2 falls mid-char.
45        let out = truncate_str("🦀🦀🦀", 2);
46        assert_eq!(out, "…");
47    }
48
49    #[test]
50    fn zero_max_len_returns_just_ellipsis() {
51        assert_eq!(truncate_str("hello", 0), "…");
52    }
53}