fraiseql_core/utils/
casing.rs1#[must_use]
21pub fn to_snake_case(s: &str) -> String {
22 if !s.chars().any(char::is_uppercase) {
24 return s.to_string();
25 }
26
27 let mut result = String::with_capacity(s.len() + 5);
28 let mut prev_was_upper = false;
29 let mut prev_was_lower = false;
30
31 for (i, c) in s.chars().enumerate() {
32 if c.is_uppercase() {
33 if i > 0 {
38 let next_is_lower = s.chars().nth(i + 1).is_some_and(char::is_lowercase);
39 if prev_was_lower || (prev_was_upper && next_is_lower) {
40 result.push('_');
41 }
42 }
43 result.push(c.to_ascii_lowercase());
44 prev_was_upper = true;
45 prev_was_lower = false;
46 } else {
47 result.push(c);
48 prev_was_upper = false;
49 prev_was_lower = c.is_lowercase();
50 }
51 }
52
53 result
54}
55
56#[must_use]
71pub fn to_camel_case(s: &str) -> String {
72 if !s.contains('_') {
74 return s.to_string();
75 }
76
77 let mut result = String::with_capacity(s.len());
78 let mut capitalize_next = false;
79
80 for c in s.chars() {
81 if c == '_' {
82 capitalize_next = true;
83 } else if capitalize_next {
84 result.push(c.to_ascii_uppercase());
85 capitalize_next = false;
86 } else {
87 result.push(c);
88 }
89 }
90
91 result
92}
93
94pub fn normalize_field_path(path: &str) -> String {
108 if !path.contains('.') {
109 return to_snake_case(path);
110 }
111
112 path.split('.').map(to_snake_case).collect::<Vec<_>>().join(".")
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_simple_camel_to_snake() {
121 assert_eq!(to_snake_case("userId"), "user_id");
122 assert_eq!(to_snake_case("userName"), "user_name");
123 assert_eq!(to_snake_case("firstName"), "first_name");
124 }
125
126 #[test]
127 fn test_pascal_to_snake() {
128 assert_eq!(to_snake_case("UserId"), "user_id");
129 assert_eq!(to_snake_case("FirstName"), "first_name");
130 }
131
132 #[test]
133 fn test_consecutive_capitals() {
134 assert_eq!(to_snake_case("HTTPResponse"), "http_response");
135 assert_eq!(to_snake_case("XMLParser"), "xml_parser");
136 assert_eq!(to_snake_case("IOError"), "io_error");
137 }
138
139 #[test]
140 fn test_already_snake_case() {
141 assert_eq!(to_snake_case("user_id"), "user_id");
142 assert_eq!(to_snake_case("first_name"), "first_name");
143 assert_eq!(to_snake_case("http_response"), "http_response");
144 }
145
146 #[test]
147 fn test_mixed_formats() {
148 assert_eq!(to_snake_case("user_Id"), "user_id"); assert_eq!(to_snake_case("HTTPStatus_Code"), "http_status_code");
150 }
151
152 #[test]
153 fn test_single_char() {
154 assert_eq!(to_snake_case("a"), "a");
155 assert_eq!(to_snake_case("A"), "a");
156 }
157
158 #[test]
159 fn test_empty_string() {
160 assert_eq!(to_snake_case(""), "");
161 }
162
163 #[test]
164 fn test_numbers() {
165 assert_eq!(to_snake_case("user2FA"), "user2fa"); assert_eq!(to_snake_case("level99Boss"), "level99boss");
167 }
168
169 #[test]
170 fn test_simple_snake_to_camel() {
171 assert_eq!(to_camel_case("user_id"), "userId");
172 assert_eq!(to_camel_case("first_name"), "firstName");
173 assert_eq!(to_camel_case("http_response"), "httpResponse");
174 }
175
176 #[test]
177 fn test_already_camel_case() {
178 assert_eq!(to_camel_case("userId"), "userId");
179 assert_eq!(to_camel_case("firstName"), "firstName");
180 }
181
182 #[test]
183 fn test_multiple_underscores() {
184 assert_eq!(to_camel_case("user__id"), "userId");
185 assert_eq!(to_camel_case("http___response"), "httpResponse");
186 }
187
188 #[test]
189 fn test_trailing_underscore() {
190 assert_eq!(to_camel_case("user_id_"), "userId");
191 assert_eq!(to_camel_case("first_name_"), "firstName");
192 }
193
194 #[test]
195 fn test_normalize_field_path_simple() {
196 assert_eq!(normalize_field_path("userId"), "user_id");
197 assert_eq!(normalize_field_path("createdAt"), "created_at");
198 }
199
200 #[test]
201 fn test_normalize_field_path_nested() {
202 assert_eq!(normalize_field_path("user.createdAt"), "user.created_at");
203 assert_eq!(
204 normalize_field_path("device.sensorData.currentValue"),
205 "device.sensor_data.current_value"
206 );
207 }
208
209 #[test]
210 fn test_normalize_field_path_already_snake() {
211 assert_eq!(normalize_field_path("user_id"), "user_id");
212 assert_eq!(normalize_field_path("user.created_at"), "user.created_at");
213 }
214
215 #[test]
216 fn test_roundtrip_conversion() {
217 let original = "userId";
218 let snake = to_snake_case(original);
219 let back = to_camel_case(&snake);
220 assert_eq!(back, original);
221
222 let original2 = "HTTPResponse";
223 let snake2 = to_snake_case(original2);
224 assert_eq!(snake2, "http_response");
225 let back2 = to_camel_case(&snake2);
226 assert_eq!(back2, "httpResponse"); }
228
229 #[test]
230 fn test_real_world_examples() {
231 assert_eq!(to_snake_case("createdAt"), "created_at");
233 assert_eq!(to_snake_case("updatedAt"), "updated_at");
234 assert_eq!(to_snake_case("deletedAt"), "deleted_at");
235 assert_eq!(to_snake_case("isActive"), "is_active");
236 assert_eq!(to_snake_case("isDeleted"), "is_deleted");
237 assert_eq!(to_snake_case("machineId"), "machine_id");
238 assert_eq!(to_snake_case("deviceType"), "device_type");
239 }
240}