facet_macro_types/
renamerule.rs

1/// Represents different case conversion strategies for renaming.
2/// All strategies assume an initial input of `snake_case` (e.g., `foo_bar`).
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum RenameRule {
5    /// Rename to PascalCase: `foo_bar` -> `FooBar`
6    PascalCase,
7    /// Rename to camelCase: `foo_bar` -> `fooBar`
8    CamelCase,
9    /// Rename to snake_case: `foo_bar` -> `foo_bar`
10    SnakeCase,
11    /// Rename to SCREAMING_SNAKE_CASE: `foo_bar` -> `FOO_BAR`
12    ScreamingSnakeCase,
13    /// Rename to kebab-case: `foo_bar` -> `foo-bar`
14    KebabCase,
15    /// Rename to SCREAMING-KEBAB-CASE: `foo_bar` -> `FOO-BAR`
16    ScreamingKebabCase,
17    /// Rename to lowercase: `foo_bar` -> `foobar`
18    Lowercase,
19    /// Rename to UPPERCASE: `foo_bar` -> `FOOBAR`
20    Uppercase,
21}
22
23impl RenameRule {
24    /// Parse a string into a `RenameRule`
25    pub fn parse(rule: &str) -> Option<Self> {
26        match rule {
27            "PascalCase" => Some(RenameRule::PascalCase),
28            "camelCase" => Some(RenameRule::CamelCase),
29            "snake_case" => Some(RenameRule::SnakeCase),
30            "SCREAMING_SNAKE_CASE" => Some(RenameRule::ScreamingSnakeCase),
31            "kebab-case" => Some(RenameRule::KebabCase),
32            "SCREAMING-KEBAB-CASE" => Some(RenameRule::ScreamingKebabCase),
33            "lowercase" => Some(RenameRule::Lowercase),
34            "UPPERCASE" => Some(RenameRule::Uppercase),
35            _ => None,
36        }
37    }
38
39    /// Apply this renaming rule to a string
40    pub fn apply(self, input: &str) -> String {
41        match self {
42            RenameRule::PascalCase => to_pascal_case(input),
43            RenameRule::CamelCase => to_camel_case(input),
44            RenameRule::SnakeCase => to_snake_case(input),
45            RenameRule::ScreamingSnakeCase => to_screaming_snake_case(input),
46            RenameRule::KebabCase => to_kebab_case(input),
47            RenameRule::ScreamingKebabCase => to_screaming_kebab_case(input),
48            RenameRule::Lowercase => to_lowercase(input),
49            RenameRule::Uppercase => to_uppercase(input),
50        }
51    }
52}
53
54/// Converts a string to PascalCase: `foo_bar` -> `FooBar`
55fn to_pascal_case(input: &str) -> String {
56    split_into_words(input)
57        .iter()
58        .map(|word| {
59            let mut chars = word.chars();
60            match chars.next() {
61                None => String::new(),
62                Some(c) => {
63                    c.to_uppercase().collect::<String>() + &chars.collect::<String>().to_lowercase()
64                }
65            }
66        })
67        .collect()
68}
69
70/// Converts a string to camelCase: `foo_bar` -> `fooBar`
71fn to_camel_case(input: &str) -> String {
72    let pascal = to_pascal_case(input);
73    if pascal.is_empty() {
74        return String::new();
75    }
76
77    let mut result = String::new();
78    let mut chars = pascal.chars();
79    if let Some(first_char) = chars.next() {
80        result.push(first_char.to_lowercase().next().unwrap());
81    }
82    result.extend(chars);
83    result
84}
85
86/// Converts a string to snake_case: `FooBar` -> `foo_bar`
87fn to_snake_case(input: &str) -> String {
88    let words = split_into_words(input);
89    words
90        .iter()
91        .map(|word| word.to_lowercase())
92        .collect::<Vec<_>>()
93        .join("_")
94}
95
96/// Converts a string to SCREAMING_SNAKE_CASE: `FooBar` -> `FOO_BAR`
97fn to_screaming_snake_case(input: &str) -> String {
98    let words = split_into_words(input);
99    words
100        .iter()
101        .map(|word| word.to_uppercase())
102        .collect::<Vec<_>>()
103        .join("_")
104}
105
106/// Converts a string to kebab-case: `FooBar` -> `foo-bar`
107fn to_kebab_case(input: &str) -> String {
108    let words = split_into_words(input);
109    words
110        .iter()
111        .map(|word| word.to_lowercase())
112        .collect::<Vec<_>>()
113        .join("-")
114}
115
116/// Converts a string to SCREAMING-KEBAB-CASE: `FooBar` -> `FOO-BAR`
117fn to_screaming_kebab_case(input: &str) -> String {
118    let words = split_into_words(input);
119    words
120        .iter()
121        .map(|word| word.to_uppercase())
122        .collect::<Vec<_>>()
123        .join("-")
124}
125
126/// Converts a string to lowercase: `foo_bar` -> `foobar`
127fn to_lowercase(input: &str) -> String {
128    let words = split_into_words(input);
129    words
130        .iter()
131        .map(|word| word.to_lowercase())
132        .collect::<Vec<_>>()
133        .join("")
134}
135
136/// Converts a string to UPPERCASE: `foo_bar` -> `FOOBAR`
137fn to_uppercase(input: &str) -> String {
138    let words = split_into_words(input);
139    words
140        .iter()
141        .map(|word| word.to_uppercase())
142        .collect::<Vec<_>>()
143        .join("")
144}
145
146/// Splits a string into words based on case and separators
147///
148/// Logic:
149/// - Iterates through characters in the input string.
150/// - Splits at underscores, hyphens, or whitespace.
151/// - Starts a new word on case boundaries, e.g. between lowercase and uppercase (as in "fooBar").
152/// - Handles consecutive uppercase letters correctly (e.g. "HTTPServer").
153/// - Aggregates non-separator characters into words.
154/// - Returns a vector of non-empty words as Strings.
155fn split_into_words(input: &str) -> Vec<String> {
156    if input.is_empty() {
157        return vec![];
158    }
159
160    let mut words = Vec::new();
161    let mut current_word = String::new();
162    let mut chars = input.chars().peekable();
163
164    while let Some(c) = chars.next() {
165        // If separator, start new word
166        if c == '_' || c == '-' || c.is_whitespace() {
167            if !current_word.is_empty() {
168                words.push(std::mem::take(&mut current_word));
169            }
170            continue;
171        }
172
173        // Peek at next character for deciding about word boundaries
174        let next = chars.peek().copied();
175
176        if c.is_uppercase() {
177            if !current_word.is_empty() {
178                let prev = current_word.chars().last().unwrap();
179                // Both cases should take the same action, so fold them together.
180                // Case 1: previous is lowercase or digit, now uppercase (e.g. fooBar, foo1Bar)
181                // Case 2: end of consecutive uppercase group, e.g. "BARBaz"
182                // (prev is uppercase and next char is lowercase)
183                if prev.is_lowercase()
184                    || prev.is_ascii_digit()
185                    || (prev.is_uppercase() && next.map(|n| n.is_lowercase()).unwrap_or(false))
186                {
187                    words.push(std::mem::take(&mut current_word));
188                }
189            }
190            current_word.push(c);
191        } else {
192            // Lowercase or digit, just append
193            // If previous is uppercase and next is lowercase, need to split, but handled above
194            current_word.push(c);
195        }
196    }
197
198    if !current_word.is_empty() {
199        words.push(current_word);
200    }
201
202    words.into_iter().filter(|s| !s.is_empty()).collect()
203}
204
205#[cfg(test)]
206mod tests {
207    use super::split_into_words;
208
209    #[test]
210    fn test_split_into_words_simple_snake_case() {
211        assert_eq!(split_into_words("foo_bar_baz"), vec!["foo", "bar", "baz"]);
212    }
213
214    #[test]
215    fn test_split_into_words_single_word() {
216        assert_eq!(split_into_words("foo"), vec!["foo"]);
217        assert_eq!(split_into_words("Foo"), vec!["Foo"]);
218    }
219
220    #[test]
221    fn test_split_into_words_empty_string() {
222        assert_eq!(split_into_words(""), Vec::<String>::new());
223    }
224
225    #[test]
226    fn test_split_into_words_multiple_underscores() {
227        assert_eq!(split_into_words("foo__bar"), vec!["foo", "bar"]);
228        assert_eq!(split_into_words("_foo_bar_"), vec!["foo", "bar"]);
229    }
230
231    #[test]
232    fn test_split_into_words_kebab_case() {
233        assert_eq!(split_into_words("foo-bar-baz"), vec!["foo", "bar", "baz"]);
234    }
235
236    #[test]
237    fn test_split_into_words_mixed_separators_and_space() {
238        assert_eq!(split_into_words("foo_ bar-baz"), vec!["foo", "bar", "baz"]);
239        assert_eq!(split_into_words("a_b-c d"), vec!["a", "b", "c", "d"]);
240    }
241
242    #[test]
243    fn test_split_into_words_camel_case() {
244        assert_eq!(split_into_words("fooBarBaz"), vec!["foo", "Bar", "Baz"]);
245        assert_eq!(split_into_words("fooBar"), vec!["foo", "Bar"]);
246        assert_eq!(
247            split_into_words("fooBar_BazQuux"),
248            vec!["foo", "Bar", "Baz", "Quux"]
249        );
250    }
251
252    #[test]
253    fn test_split_into_words_pascal_case() {
254        assert_eq!(split_into_words("FooBarBaz"), vec!["Foo", "Bar", "Baz"]);
255        assert_eq!(split_into_words("FooBar"), vec!["Foo", "Bar"]);
256    }
257
258    #[test]
259    fn test_split_into_words_http_server() {
260        assert_eq!(split_into_words("HTTPServer"), vec!["HTTP", "Server"]);
261        assert_eq!(
262            split_into_words("theHTTPServer"),
263            vec!["the", "HTTP", "Server"]
264        );
265    }
266
267    #[test]
268    fn test_split_into_words_consecutive_uppercase_at_end() {
269        assert_eq!(split_into_words("FooBAR"), vec!["Foo", "BAR"]);
270        assert_eq!(split_into_words("FooBARBaz"), vec!["Foo", "BAR", "Baz"]);
271    }
272
273    #[test]
274    fn test_split_into_words_separators_and_case_boundaries() {
275        assert_eq!(split_into_words("foo_barBaz"), vec!["foo", "bar", "Baz"]);
276        assert_eq!(
277            split_into_words("fooBar_bazQux"),
278            vec!["foo", "Bar", "baz", "Qux"]
279        );
280    }
281
282    #[test]
283    fn test_rename_rule_snake_case() {
284        use super::RenameRule;
285        // Snake case input should remain unchanged
286        assert_eq!(RenameRule::SnakeCase.apply("foo_bar_baz"), "foo_bar_baz");
287        // CamelCase input becomes snake_case
288        assert_eq!(RenameRule::SnakeCase.apply("fooBarBaz"), "foo_bar_baz");
289        // PascalCase input becomes snake_case
290        assert_eq!(RenameRule::SnakeCase.apply("FooBarBaz"), "foo_bar_baz");
291        // SCREAMING_SNAKE_CASE input becomes snake_case
292        assert_eq!(RenameRule::SnakeCase.apply("FOO_BAR_BAZ"), "foo_bar_baz");
293        // kebab-case input becomes snake_case
294        assert_eq!(RenameRule::SnakeCase.apply("foo-bar-baz"), "foo_bar_baz");
295        assert_eq!(
296            RenameRule::SnakeCase.apply("Foo_Bar-Baz quux"),
297            "foo_bar_baz_quux"
298        );
299        // Mixed case and separator input
300        assert_eq!(
301            RenameRule::SnakeCase.apply("theHTTPServer"),
302            "the_http_server"
303        );
304        assert_eq!(RenameRule::SnakeCase.apply("FooBARBaz"), "foo_bar_baz");
305        // Empty input keeps empty
306        assert_eq!(RenameRule::SnakeCase.apply(""), "");
307    }
308
309    #[test]
310    fn test_rename_rule_lowercase() {
311        use super::RenameRule;
312        // Snake case input becomes lowercase without separators
313        assert_eq!(RenameRule::Lowercase.apply("foo_bar_baz"), "foobarbaz");
314        // CamelCase input becomes lowercase
315        assert_eq!(RenameRule::Lowercase.apply("fooBarBaz"), "foobarbaz");
316        // PascalCase input becomes lowercase
317        assert_eq!(RenameRule::Lowercase.apply("FooBarBaz"), "foobarbaz");
318        // SCREAMING_SNAKE_CASE input becomes lowercase
319        assert_eq!(RenameRule::Lowercase.apply("FOO_BAR_BAZ"), "foobarbaz");
320        // kebab-case input becomes lowercase
321        assert_eq!(RenameRule::Lowercase.apply("foo-bar-baz"), "foobarbaz");
322        // Mixed case and separator input
323        assert_eq!(
324            RenameRule::Lowercase.apply("theHTTPServer"),
325            "thehttpserver"
326        );
327        // Empty input keeps empty
328        assert_eq!(RenameRule::Lowercase.apply(""), "");
329    }
330
331    #[test]
332    fn test_rename_rule_uppercase() {
333        use super::RenameRule;
334        // Snake case input becomes UPPERCASE without separators
335        assert_eq!(RenameRule::Uppercase.apply("foo_bar_baz"), "FOOBARBAZ");
336        // CamelCase input becomes UPPERCASE
337        assert_eq!(RenameRule::Uppercase.apply("fooBarBaz"), "FOOBARBAZ");
338        // PascalCase input becomes UPPERCASE
339        assert_eq!(RenameRule::Uppercase.apply("FooBarBaz"), "FOOBARBAZ");
340        // SCREAMING_SNAKE_CASE input becomes UPPERCASE without separators
341        assert_eq!(RenameRule::Uppercase.apply("FOO_BAR_BAZ"), "FOOBARBAZ");
342        // kebab-case input becomes UPPERCASE
343        assert_eq!(RenameRule::Uppercase.apply("foo-bar-baz"), "FOOBARBAZ");
344        // Mixed case and separator input
345        assert_eq!(
346            RenameRule::Uppercase.apply("theHTTPServer"),
347            "THEHTTPSERVER"
348        );
349        // Typical use case: max_size -> MAXSIZE
350        assert_eq!(RenameRule::Uppercase.apply("max_size"), "MAXSIZE");
351        assert_eq!(RenameRule::Uppercase.apply("min_value"), "MINVALUE");
352        // Empty input keeps empty
353        assert_eq!(RenameRule::Uppercase.apply(""), "");
354    }
355}