prism_mcp_rs/utils/
string_utils.rs1use super::{UtilError, UtilResult};
4use std::collections::HashMap;
5
6pub fn sanitize_identifier(input: &str) -> String {
8 input
9 .chars()
10 .map(|c| {
11 if c.is_alphanumeric() || c == '_' || c == '-' {
12 c
13 } else {
14 '_'
15 }
16 })
17 .collect::<String>()
18 .trim_matches('_')
19 .to_string()
20}
21
22pub fn to_snake_case(input: &str) -> String {
24 let mut result = String::new();
25 let mut prev_was_uppercase = false;
26
27 for (i, c) in input.chars().enumerate() {
28 if c.is_uppercase() {
29 if i > 0 && !prev_was_uppercase {
30 result.push('_');
31 }
32 result.push(c.to_lowercase().next().unwrap());
33 prev_was_uppercase = true;
34 } else {
35 result.push(c);
36 prev_was_uppercase = false;
37 }
38 }
39
40 result
41}
42
43pub fn to_camel_case(input: &str) -> String {
45 let mut result = String::new();
46 let mut capitalize_next = false;
47
48 for c in input.chars() {
49 if c == '_' || c == '-' || c.is_whitespace() {
50 capitalize_next = true;
51 } else if capitalize_next {
52 result.push(c.to_uppercase().next().unwrap());
53 capitalize_next = false;
54 } else {
55 result.push(c.to_lowercase().next().unwrap());
56 }
57 }
58
59 result
60}
61
62pub fn to_pascal_case(input: &str) -> String {
64 let camel = to_camel_case(input);
65 if let Some(first) = camel.chars().next() {
66 first.to_uppercase().collect::<String>() + &camel[1..]
67 } else {
68 camel
69 }
70}
71
72pub fn truncate_with_ellipsis(input: &str, max_len: usize) -> String {
74 if input.len() <= max_len {
75 input.to_string()
76 } else if max_len <= 3 {
77 "...".to_string()
78 } else {
79 format!("{}...", &input[..max_len - 3])
80 }
81}
82
83pub fn extract_template_variables(template: &str) -> Vec<String> {
85 let mut variables = Vec::new();
86 let mut chars = template.chars().peekable();
87
88 while let Some(c) = chars.next() {
89 if c == '{' {
90 let mut var_name = String::new();
91 while let Some(&next_c) = chars.peek() {
92 if next_c == '}' {
93 chars.next(); if !var_name.is_empty() {
95 variables.push(var_name);
96 }
97 break;
98 }
99 var_name.push(chars.next().unwrap());
100 }
101 }
102 }
103
104 variables
105}
106
107pub fn replace_template_variables(
109 template: &str,
110 variables: &HashMap<String, String>,
111) -> UtilResult<String> {
112 let mut result = template.to_string();
113
114 for (key, value) in variables {
115 let placeholder = format!("{{{}}}", key);
116 result = result.replace(&placeholder, value);
117 }
118
119 let remaining_vars = extract_template_variables(&result);
121 if !remaining_vars.is_empty() {
122 return Err(UtilError::ValidationFailed(format!(
123 "Unreplaced template variables: {:?}",
124 remaining_vars
125 )));
126 }
127
128 Ok(result)
129}
130
131pub fn random_string(length: usize, charset: &str) -> String {
133 use rand::prelude::*;
134 use rand::rng;
135
136 let chars: Vec<char> = charset.chars().collect();
137 let mut rng = rng();
138
139 (0..length)
140 .map(|_| *chars.choose(&mut rng).unwrap_or(&'a'))
141 .collect()
142}
143
144pub fn random_alphanumeric(length: usize) -> String {
146 random_string(
147 length,
148 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
149 )
150}
151
152pub fn escape_json_string(input: &str) -> String {
154 serde_json::to_string(input).unwrap_or_else(|_| format!("{:?}", input))
155}
156
157pub fn is_valid_semver(version: &str) -> bool {
159 let parts: Vec<&str> = version.split('.').collect();
160 if parts.len() != 3 {
161 return false;
162 }
163
164 parts
165 .iter()
166 .all(|part| part.parse::<u32>().is_ok() && !part.starts_with('0') || *part == "0")
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_sanitize_identifier() {
175 assert_eq!(sanitize_identifier("hello world!"), "hello_world");
176 assert_eq!(sanitize_identifier("test@email.com"), "test_email_com");
177 }
178
179 #[test]
180 fn test_case_conversions() {
181 assert_eq!(to_snake_case("HelloWorld"), "hello_world");
182 assert_eq!(to_camel_case("hello_world"), "helloWorld");
183 assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
184 }
185
186 #[test]
187 fn test_template_variables() {
188 let template = "Hello {name}, you have {count} messages";
189 let vars = extract_template_variables(template);
190 assert_eq!(vars, vec!["name", "count"]);
191
192 let mut replacements = HashMap::new();
193 replacements.insert("name".to_string(), "Alice".to_string());
194 replacements.insert("count".to_string(), "5".to_string());
195
196 let result = replace_template_variables(template, &replacements).unwrap();
197 assert_eq!(result, "Hello Alice, you have 5 messages");
198 }
199
200 #[test]
201 fn test_truncate() {
202 assert_eq!(truncate_with_ellipsis("hello", 10), "hello");
203 assert_eq!(truncate_with_ellipsis("hello world", 8), "hello...");
204 }
205
206 #[test]
207 fn test_semver_validation() {
208 assert!(is_valid_semver("1.0.0"));
209 assert!(is_valid_semver("0.1.0"));
210 assert!(!is_valid_semver("1.0"));
211 assert!(!is_valid_semver("01.0.0"));
212 }
213}