Skip to main content

dotenv_space/utils/
string.rs

1//! String utility functions for formatting and display.
2//!
3//! These helpers are used throughout the CLI for consistent presentation
4//! of values in terminal output.
5
6/// Truncate a string to `max_len` characters.
7///
8/// If the string is longer than `max_len`, it is cut and `...` is appended.
9/// `max_len` must be at least 4 — otherwise the ellipsis itself would not fit.
10///
11/// # Examples
12///
13/// ```
14/// use dotenv_space::utils::string::truncate;
15/// assert_eq!(truncate("hello world", 8), "hello...");
16/// assert_eq!(truncate("hi", 10), "hi");
17/// ```
18pub fn truncate(s: &str, max_len: usize) -> String {
19    if s.len() <= max_len {
20        s.to_string()
21    } else {
22        format!("{}...", &s[..max_len.saturating_sub(3)])
23    }
24}
25
26/// Redact a sensitive value for safe display in terminal output.
27///
28/// - Short values (≤ 8 chars): replaced entirely with `*`.
29/// - Longer values: first 4 chars shown, then `...****`.
30///
31/// The original value is never fully exposed in output; this is purely
32/// for debugging context (e.g. showing that a key *has* a value).
33///
34/// # Examples
35///
36/// ```
37/// use dotenv_space::utils::string::redact;
38/// assert_eq!(redact("secret"), "******");
39/// assert_eq!(redact("secretkey123"), "secr...****");
40/// ```
41pub fn redact(s: &str) -> String {
42    if s.len() <= 8 {
43        "*".repeat(s.len())
44    } else {
45        format!("{}...{}", &s[..4], "*".repeat(4))
46    }
47}
48
49/// Return a count-aware string using the correct singular or plural form.
50///
51/// # Examples
52///
53/// ```
54/// use dotenv_space::utils::string::pluralize;
55/// assert_eq!(pluralize(1, "file", "files"), "1 file");
56/// assert_eq!(pluralize(3, "file", "files"), "3 files");
57/// ```
58pub fn pluralize(count: usize, singular: &str, plural: &str) -> String {
59    if count == 1 {
60        format!("{} {}", count, singular)
61    } else {
62        format!("{} {}", count, plural)
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_truncate_short() {
72        assert_eq!(truncate("hello", 10), "hello");
73    }
74
75    #[test]
76    fn test_truncate_long() {
77        assert_eq!(truncate("hello world", 8), "hello...");
78    }
79
80    #[test]
81    fn test_truncate_exact_boundary() {
82        assert_eq!(truncate("hello", 5), "hello");
83    }
84
85    #[test]
86    fn test_redact_short() {
87        assert_eq!(redact("secret"), "******");
88    }
89
90    #[test]
91    fn test_redact_long() {
92        assert_eq!(redact("secretkey123"), "secr...****");
93    }
94
95    #[test]
96    fn test_redact_exactly_eight() {
97        // 8 chars → all stars
98        assert_eq!(redact("12345678"), "********");
99    }
100
101    #[test]
102    fn test_pluralize_singular() {
103        assert_eq!(pluralize(1, "file", "files"), "1 file");
104    }
105
106    #[test]
107    fn test_pluralize_plural() {
108        assert_eq!(pluralize(2, "file", "files"), "2 files");
109    }
110
111    #[test]
112    fn test_pluralize_zero() {
113        assert_eq!(pluralize(0, "file", "files"), "0 files");
114    }
115}