1pub trait StrExt {
7 fn is_blank(&self) -> bool;
9 fn to_camel(&self) -> String;
11 fn to_snake(&self) -> String;
13 fn truncate(&self, max: usize) -> String;
15 fn random(len: usize) -> String;
17 fn has_text(&self) -> bool { !self.is_blank() }
19}
20
21impl StrExt for str {
22 fn is_blank(&self) -> bool { self.trim().is_empty() }
23
24 fn to_camel(&self) -> String {
25 self.split('_')
26 .enumerate()
27 .map(|(i, w)| {
28 if i == 0 { w.to_lowercase() }
29 else { let mut c = w.chars(); c.next().map(|x| x.to_uppercase().chain(c).collect()).unwrap_or_default() }
30 })
31 .collect()
32 }
33
34 fn to_snake(&self) -> String {
35 let mut result = String::with_capacity(self.len() + 4);
36 for (i, ch) in self.chars().enumerate() {
37 if ch.is_uppercase() && i > 0 {
38 result.push('_');
39 }
40 result.push(ch.to_ascii_lowercase());
41 }
42 result
43 }
44
45 fn truncate(&self, max: usize) -> String {
46 if self.len() <= max { self.to_string() }
47 else { format!("{}...", &self[..max]) }
48 }
49
50 fn random(len: usize) -> String {
51 use rand::Rng;
52 const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
53 let mut rng = rand::thread_rng();
54 (0..len).map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char).collect()
55 }
56}
57
58pub fn sanitize_filename(filename: &str) -> String {
60 use regex::Regex;
61 let re = Regex::new(r"[^a-zA-Z0-9.\-_]").unwrap();
62 re.replace_all(filename, "_").to_string()
63}
64
65pub fn parse_json_value(value: &str) -> Result<serde_json::Value, serde_json::Error> {
67 serde_json::from_str(value)
68}
69
70pub fn format_file_size(bytes: u64) -> String {
80 const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
81
82 if bytes == 0 {
83 return "0 B".to_string();
84 }
85
86 let i = (bytes as f64).log(1024.0).floor() as i32;
87 let size = bytes as f64 / 1024_f64.powi(i);
88 let unit = UNITS.get(i as usize).unwrap_or(&"B");
89
90 format!("{:.2} {}", size, unit)
91}
92
93pub fn clean_string_param(s: &str) -> String {
95 s.trim().to_string()
96}
97
98pub fn clean_email(email: &str) -> String {
100 email.trim().to_lowercase()
101}
102
103pub fn clean_password(password: &str) -> String {
105 password.trim().to_string()
106}
107
108pub struct InputCleaner;
110
111impl InputCleaner {
112 pub fn clean_register_input(
116 email: &str,
117 password: &str,
118 nickname: &str,
119 ) -> (String, String, String) {
120 let email = clean_email(email);
121 let password = clean_password(password);
122 let nickname = clean_string_param(nickname);
123 (email, password, nickname)
124 }
125
126 pub fn clean_login_input(email: &str, password: &str) -> (String, String) {
130 let email = clean_email(email);
131 let password = clean_password(password);
132 (email, password)
133 }
134}
135
136pub fn generate_invite_code() -> String {
138 use rand::{Rng, distributions::Alphanumeric};
139 rand::thread_rng()
140 .sample_iter(&Alphanumeric)
141 .take(12)
142 .map(char::from)
143 .collect()
144}
145
146pub fn generate_random_digits(n: usize) -> String {
160 if n == 0 {
161 return String::new();
162 }
163 use rand::Rng;
164 let mut rng = rand::thread_rng();
165 (0..n)
166 .map(|_| (rng.gen_range(1..=9) + b'0') as char)
167 .collect()
168}
169
170pub fn generate_random_alphanum(length: usize) -> String {
184 const CHARSET: &[u8] = b"ABCDEFGHJKMNPQRSTUVWXYZ\
185 abcdefghjkmnpqrstuvwxyz\
186 123456789";
187 use rand::Rng;
188 let mut rng = rand::thread_rng();
189 let mut result = String::with_capacity(length);
190 for _ in 0..length {
191 let idx = rng.gen_range(0..CHARSET.len());
192 result.push(CHARSET[idx] as char);
193 }
194 result
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_camel() { assert_eq!("user_name".to_camel(), "userName"); }
203 #[test]
204 fn test_snake() { assert_eq!("UserName".to_snake(), "user_name"); }
205 #[test]
206 fn test_blank() { assert!(" ".is_blank()); assert!(!"abc".is_blank()); }
207}