string_cases/
lib.rs

1#![doc = include_str!("./README.md")]
2
3/// Extension trait for adding methods
4pub trait StringCasesExt {
5    /// '_' defines boundaries
6    fn to_snake_case(&self) -> String;
7
8    /// '-' defines boundaries
9    fn to_kebab_case(&self) -> String;
10
11    /// Differences in cases define boundaries
12    fn to_camel_case(&self) -> String;
13
14    /// Differences in cases define boundaries. First character is always uppercase
15    fn to_pascal_case(&self) -> String;
16}
17
18impl<T> StringCasesExt for T
19where
20    T: std::ops::Deref<Target = str>,
21{
22    fn to_snake_case(&self) -> String {
23        apply_divider_transform(self, '_')
24    }
25
26    fn to_kebab_case(&self) -> String {
27        apply_divider_transform(self, '-')
28    }
29
30    fn to_camel_case(&self) -> String {
31        apply_case_transform(self, false)
32    }
33
34    fn to_pascal_case(&self) -> String {
35        apply_case_transform(self, true)
36    }
37}
38
39fn apply_divider_transform(s: &str, divider: char) -> String {
40    let mut peekable = s.chars().peekable();
41    let mut string = String::with_capacity(s.len());
42    let mut previous_was_uppercase = false;
43
44    while let Some(character) = peekable.next() {
45        if let '_' | '-' = character {
46            string.push(divider);
47            continue;
48        }
49
50        let upcoming = peekable.peek().copied();
51
52        let uppercase_sequence_to_lower = previous_was_uppercase
53            && character.is_uppercase()
54            && upcoming.map(char::is_lowercase).unwrap_or(false);
55
56        if uppercase_sequence_to_lower {
57            string.push(divider);
58            string.extend(character.to_lowercase());
59            continue;
60        }
61
62        let lowercase_to_other = character.is_lowercase()
63            && upcoming
64                .map(|c| c.is_uppercase() || c.is_numeric())
65                .unwrap_or(false);
66
67        string.extend(character.to_lowercase());
68
69        if lowercase_to_other {
70            string.push(divider);
71        }
72
73        previous_was_uppercase = character.is_uppercase();
74    }
75    string
76}
77
78fn apply_case_transform(s: &str, uppercase_first: bool) -> String {
79    let mut last_was_divider = uppercase_first;
80    let mut string = String::new();
81    for chr in s.chars() {
82        if chr == '_' {
83            last_was_divider = true;
84        } else if last_was_divider {
85            last_was_divider = false;
86            string.extend(chr.to_uppercase());
87        } else {
88            string.push(chr);
89        }
90    }
91    string
92}
93
94#[cfg(test)]
95mod tests {
96    use super::StringCasesExt;
97
98    #[test]
99    fn works_on_string() {
100        let x: String = "test".into();
101        let _ = x.to_snake_case();
102    }
103
104    #[test]
105    fn snake_case() {
106        assert_eq!("SomeStruct".to_snake_case(), "some_struct");
107        assert_eq!("SomeTLA".to_snake_case(), "some_tla");
108        assert_eq!(
109            "Member_With_underscore".to_snake_case(),
110            "member_with_underscore"
111        );
112        assert_eq!("JSXElement".to_snake_case(), "jsx_element");
113        assert_eq!("Field6".to_snake_case(), "field_6");
114    }
115
116    #[test]
117    fn kebab_case() {
118        assert_eq!("SomeStruct".to_kebab_case(), "some-struct");
119        assert_eq!("SomeTLA".to_kebab_case(), "some-tla");
120        assert_eq!(
121            "Member_With_underscore".to_kebab_case(),
122            "member-with-underscore"
123        );
124        assert_eq!("Field6".to_kebab_case(), "field-6");
125    }
126
127    #[test]
128    fn camel_case() {
129        assert_eq!("Some_Struct".to_camel_case(), "SomeStruct");
130        assert_eq!("some_thing".to_camel_case(), "someThing");
131        assert_eq!(
132            "Member_With_underscore".to_camel_case(),
133            "MemberWithUnderscore"
134        );
135        assert_eq!("field6".to_camel_case(), "field6");
136    }
137
138    #[test]
139    fn pascal_case() {
140        assert_eq!("Some_Struct".to_pascal_case(), "SomeStruct");
141        assert_eq!("some_thing".to_pascal_case(), "SomeThing");
142        assert_eq!(
143            "Member_With_underscore".to_pascal_case(),
144            "MemberWithUnderscore"
145        );
146        assert_eq!("field6".to_pascal_case(), "Field6");
147    }
148}