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}