Skip to main content

prism_mcp_rs/utils/
string_utils.rs

1//! String manipulation utilities
2
3use super::{UtilError, UtilResult};
4use std::collections::HashMap;
5
6/// Sanitize a string for safe use in identifiers
7pub 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
22/// Convert a string to snake_case
23pub 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
43/// Convert a string to camelCase
44pub 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
62/// Convert a string to PascalCase
63pub 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
72/// Truncate a string to a maximum length with ellipsis
73pub 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
83/// Extract variables from a template string (e.g., "Hello {name}")
84pub 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(); // consume '}'
94                    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
107/// Replace variables in a template string
108pub 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    // Check for unreplaced variables
120    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
131/// Generate a random string with specified length and character set
132pub 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
144/// Generate a random alphanumeric string
145pub fn random_alphanumeric(length: usize) -> String {
146    random_string(
147        length,
148        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
149    )
150}
151
152/// Escape a string for JSON
153pub fn escape_json_string(input: &str) -> String {
154    serde_json::to_string(input).unwrap_or_else(|_| format!("{:?}", input))
155}
156
157/// Check if a string is a valid semantic version
158pub 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}