aster_forge_utils/
text.rs

1//! Text length and UTF-8-safe truncation helpers.
2//!
3//! Aster services often need conservative limits for display text, file names, status messages, or
4//! external-system error snippets. This module keeps byte-based truncation UTF-8-safe and provides a
5//! small character-count helper for product validation rules that are expressed in Unicode scalar
6//! values instead of bytes.
7
8/// Returns the number of Unicode scalar values in a string.
9///
10/// This is intentionally not a grapheme-cluster count. Product validation rules that need
11/// user-perceived characters should use a dedicated Unicode segmentation policy at the product
12/// boundary.
13#[must_use]
14pub fn char_count(value: &str) -> usize {
15    value.chars().count()
16}
17
18/// Truncates a string to at most `max_bytes` bytes without splitting a UTF-8 code point.
19///
20/// If `value` is already within the limit it is returned unchanged as an owned string. A zero limit
21/// always returns an empty string.
22#[must_use]
23pub fn truncate_utf8_to_max_bytes(value: &str, max_bytes: usize) -> String {
24    if value.len() <= max_bytes {
25        return value.to_string();
26    }
27
28    let mut end = max_bytes;
29    while end > 0 && !value.is_char_boundary(end) {
30        end -= 1;
31    }
32    value[..end].to_string()
33}
34
35#[cfg(test)]
36mod tests {
37    use super::{char_count, truncate_utf8_to_max_bytes};
38
39    #[test]
40    fn char_count_counts_unicode_scalars() {
41        assert_eq!(char_count("Aster"), 5);
42        assert_eq!(char_count("你好世界"), 4);
43        assert_eq!(char_count("e\u{301}"), 2);
44    }
45
46    #[test]
47    fn truncate_utf8_to_max_bytes_keeps_short_ascii_unchanged() {
48        assert_eq!(
49            truncate_utf8_to_max_bytes("AsterYggdrasil", 32),
50            "AsterYggdrasil"
51        );
52    }
53
54    #[test]
55    fn truncate_utf8_to_max_bytes_truncates_ascii_by_bytes() {
56        assert_eq!(truncate_utf8_to_max_bytes("AsterYggdrasil", 5), "Aster");
57    }
58
59    #[test]
60    fn truncate_utf8_to_max_bytes_preserves_char_boundaries() {
61        assert_eq!(truncate_utf8_to_max_bytes("你好世界", 7), "你好");
62        assert_eq!(truncate_utf8_to_max_bytes("éclair", 1), "");
63        assert_eq!(truncate_utf8_to_max_bytes("éclair", 2), "é");
64    }
65
66    #[test]
67    fn truncate_utf8_to_max_bytes_handles_zero_limit() {
68        assert_eq!(truncate_utf8_to_max_bytes("AsterYggdrasil", 0), "");
69        assert_eq!(truncate_utf8_to_max_bytes("你好", 0), "");
70    }
71}