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