1#![doc = include_str!("./README.md")]
2
3pub trait StringCasesExt {
5 fn to_snake_case(&self) -> String;
7
8 fn to_kebab_case(&self) -> String;
10
11 fn to_camel_case(&self) -> String;
13
14 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}