Skip to main content

reformat_core/
case.rs

1//! Case format definitions and conversion logic
2
3/// Supported case formats for identifier conversion
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum CaseFormat {
6    /// camelCase: firstName, lastName
7    CamelCase,
8    /// PascalCase: FirstName, LastName
9    PascalCase,
10    /// snake_case: first_name, last_name
11    SnakeCase,
12    /// SCREAMING_SNAKE_CASE: FIRST_NAME, LAST_NAME
13    ScreamingSnakeCase,
14    /// kebab-case: first-name, last-name
15    KebabCase,
16    /// SCREAMING-KEBAB-CASE: FIRST-NAME, LAST-NAME
17    ScreamingKebabCase,
18}
19
20impl CaseFormat {
21    /// Returns the regex pattern for identifying this case format
22    pub fn pattern(&self) -> &str {
23        match self {
24            CaseFormat::CamelCase => r"\b[a-z]+(?:[A-Z][a-z0-9]*)+\b",
25            CaseFormat::PascalCase => r"\b[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]*)+\b",
26            CaseFormat::SnakeCase => r"\b[a-z]+(?:_[a-z0-9]+)+\b",
27            CaseFormat::ScreamingSnakeCase => r"\b[A-Z]+(?:_[A-Z0-9]+)+\b",
28            CaseFormat::KebabCase => r"\b[a-z]+(?:-[a-z0-9]+)+\b",
29            CaseFormat::ScreamingKebabCase => r"\b[A-Z]+(?:-[A-Z0-9]+)+\b",
30        }
31    }
32
33    /// Splits a string into words based on this case format
34    pub fn split_words(&self, text: &str) -> Vec<String> {
35        match self {
36            CaseFormat::CamelCase | CaseFormat::PascalCase => {
37                // Split on uppercase letters manually since regex doesn't support lookahead
38                let mut words = Vec::new();
39                let mut current_word = String::new();
40
41                for ch in text.chars() {
42                    if ch.is_uppercase() && !current_word.is_empty() {
43                        words.push(current_word.to_lowercase());
44                        current_word = String::new();
45                    }
46                    current_word.push(ch);
47                }
48
49                if !current_word.is_empty() {
50                    words.push(current_word.to_lowercase());
51                }
52
53                words
54            }
55            CaseFormat::SnakeCase | CaseFormat::ScreamingSnakeCase => {
56                // Split on underscores
57                text.split('_')
58                    .filter(|s| !s.is_empty())
59                    .map(|s| s.to_lowercase())
60                    .collect()
61            }
62            CaseFormat::KebabCase | CaseFormat::ScreamingKebabCase => {
63                // Split on hyphens
64                text.split('-')
65                    .filter(|s| !s.is_empty())
66                    .map(|s| s.to_lowercase())
67                    .collect()
68            }
69        }
70    }
71
72    /// Joins words into this case format with optional prefix and suffix
73    pub fn join_words(&self, words: &[String], prefix: &str, suffix: &str) -> String {
74        if words.is_empty() {
75            return String::new();
76        }
77
78        let result = match self {
79            CaseFormat::CamelCase => {
80                let first = words[0].to_lowercase();
81                let rest: String = words[1..]
82                    .iter()
83                    .map(|w| {
84                        let mut chars = w.chars();
85                        match chars.next() {
86                            None => String::new(),
87                            Some(first) => first.to_uppercase().chain(chars).collect(),
88                        }
89                    })
90                    .collect();
91                format!("{}{}", first, rest)
92            }
93            CaseFormat::PascalCase => words
94                .iter()
95                .map(|w| {
96                    let mut chars = w.chars();
97                    match chars.next() {
98                        None => String::new(),
99                        Some(first) => first.to_uppercase().chain(chars).collect(),
100                    }
101                })
102                .collect::<String>(),
103            CaseFormat::SnakeCase => words
104                .iter()
105                .map(|w| w.to_lowercase())
106                .collect::<Vec<_>>()
107                .join("_"),
108            CaseFormat::ScreamingSnakeCase => words
109                .iter()
110                .map(|w| w.to_uppercase())
111                .collect::<Vec<_>>()
112                .join("_"),
113            CaseFormat::KebabCase => words
114                .iter()
115                .map(|w| w.to_lowercase())
116                .collect::<Vec<_>>()
117                .join("-"),
118            CaseFormat::ScreamingKebabCase => words
119                .iter()
120                .map(|w| w.to_uppercase())
121                .collect::<Vec<_>>()
122                .join("-"),
123        };
124
125        format!("{}{}{}", prefix, result, suffix)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_camel_split() {
135        let words = CaseFormat::CamelCase.split_words("firstName");
136        assert_eq!(words, vec!["first", "name"]);
137    }
138
139    #[test]
140    fn test_snake_split() {
141        let words = CaseFormat::SnakeCase.split_words("first_name");
142        assert_eq!(words, vec!["first", "name"]);
143    }
144
145    #[test]
146    fn test_camel_join() {
147        let words = vec!["first".to_string(), "name".to_string()];
148        assert_eq!(
149            CaseFormat::CamelCase.join_words(&words, "", ""),
150            "firstName"
151        );
152    }
153
154    #[test]
155    fn test_snake_join() {
156        let words = vec!["first".to_string(), "name".to_string()];
157        assert_eq!(
158            CaseFormat::SnakeCase.join_words(&words, "", ""),
159            "first_name"
160        );
161    }
162
163    #[test]
164    fn test_with_prefix_suffix() {
165        let words = vec!["first".to_string(), "name".to_string()];
166        assert_eq!(
167            CaseFormat::SnakeCase.join_words(&words, "old_", "_v1"),
168            "old_first_name_v1"
169        );
170    }
171}