spikard_cli/codegen/common/
case_conversion.rs1#[must_use]
30pub fn to_snake_case(s: &str) -> String {
31 if s.is_empty() {
32 return String::new();
33 }
34
35 let mut result = String::new();
36 let chars: Vec<char> = s.chars().collect();
37
38 for (i, &ch) in chars.iter().enumerate() {
39 if ch.is_uppercase() {
40 let should_add_underscore = if i == 0 {
41 false
42 } else if result.ends_with('_') {
43 false
44 } else {
45 let prev_is_lower = chars[i - 1].is_lowercase();
46 let prev_is_digit = chars[i - 1].is_numeric();
47 let next_is_lower = (i + 1 < chars.len()) && chars[i + 1].is_lowercase();
48
49 prev_is_lower || prev_is_digit || (i > 0 && chars[i - 1].is_uppercase() && next_is_lower)
50 };
51
52 if should_add_underscore {
53 result.push('_');
54 }
55 result.push_str(&ch.to_lowercase().to_string());
56 } else {
57 result.push(ch);
58 }
59 }
60
61 result
62}
63
64#[must_use]
85pub fn to_camel_case(s: &str) -> String {
86 if s.is_empty() {
87 return String::new();
88 }
89
90 let parts: Vec<&str> = s.split('_').collect();
91 if parts.is_empty() {
92 return String::new();
93 }
94
95 let has_leading_underscore = s.starts_with('_');
96 let has_trailing_underscore = s.ends_with('_');
97
98 let mut result = if has_leading_underscore {
99 String::from("_")
100 } else {
101 String::new()
102 };
103
104 let non_empty_parts: Vec<&str> = parts.iter().filter(|p| !p.is_empty()).copied().collect();
105
106 if non_empty_parts.is_empty() {
107 if has_trailing_underscore {
108 result.push('_');
109 }
110 return result;
111 }
112
113 result.push_str(non_empty_parts[0]);
114
115 for part in &non_empty_parts[1..] {
116 if let Some(first_char) = part.chars().next() {
117 result.push_str(&first_char.to_uppercase().to_string());
118 result.push_str(&part[first_char.len_utf8()..]);
119 }
120 }
121
122 if has_trailing_underscore {
123 result.push('_');
124 }
125
126 result
127}
128
129#[must_use]
150pub fn to_pascal_case(s: &str) -> String {
151 if s.is_empty() {
152 return String::new();
153 }
154
155 let parts: Vec<&str> = s.split(|c: char| !c.is_alphanumeric()).collect();
156
157 parts
158 .into_iter()
159 .filter(|p| !p.is_empty())
160 .map(|part| {
161 let mut chars = part.chars();
162 match chars.next() {
163 None => String::new(),
164 Some(first) => {
165 let mut result = first.to_uppercase().collect::<String>();
166 result.push_str(chars.as_str());
167 result
168 }
169 }
170 })
171 .collect()
172}
173
174#[must_use]
195pub fn to_kebab_case(s: &str) -> String {
196 if s.is_empty() {
197 return String::new();
198 }
199
200 let mut result = String::new();
201 let chars: Vec<char> = s.chars().collect();
202
203 for (i, &ch) in chars.iter().enumerate() {
204 if ch.is_uppercase() {
205 let should_add_hyphen = if i == 0 || result.ends_with('-') {
206 false
207 } else {
208 let prev_is_lower = chars[i - 1].is_lowercase();
209 let prev_is_digit = chars[i - 1].is_numeric();
210 let next_is_lower = (i + 1 < chars.len()) && chars[i + 1].is_lowercase();
211
212 prev_is_lower || prev_is_digit || (i > 0 && chars[i - 1].is_uppercase() && next_is_lower)
213 };
214
215 if should_add_hyphen {
216 result.push('-');
217 }
218 result.push_str(&ch.to_lowercase().to_string());
219 } else if ch == '_' {
220 if !result.ends_with('-') {
221 result.push('-');
222 }
223 } else {
224 result.push(ch);
225 }
226 }
227
228 result.trim_matches('-').to_string()
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_to_snake_case_simple() {
237 assert_eq!(to_snake_case("user"), "user");
238 assert_eq!(to_snake_case("name"), "name");
239 assert_eq!(to_snake_case("id"), "id");
240 }
241
242 #[test]
243 fn test_to_snake_case_camel_case() {
244 assert_eq!(to_snake_case("getUser"), "get_user");
245 assert_eq!(to_snake_case("userName"), "user_name");
246 assert_eq!(to_snake_case("userId"), "user_id");
247 }
248
249 #[test]
250 fn test_to_snake_case_pascal_case() {
251 assert_eq!(to_snake_case("GetUser"), "get_user");
252 assert_eq!(to_snake_case("UserName"), "user_name");
253 assert_eq!(to_snake_case("CreateUserProfile"), "create_user_profile");
254 }
255
256 #[test]
257 fn test_to_snake_case_acronyms() {
258 assert_eq!(to_snake_case("HTTPServer"), "http_server");
259 assert_eq!(to_snake_case("GraphQLType"), "graph_ql_type");
260 assert_eq!(to_snake_case("XMLHttpRequest"), "xml_http_request");
261 assert_eq!(to_snake_case("IOError"), "io_error");
262 assert_eq!(to_snake_case("URLPath"), "url_path");
263 }
264
265 #[test]
266 fn test_to_snake_case_consecutive_caps() {
267 assert_eq!(to_snake_case("ID"), "id");
268 assert_eq!(to_snake_case("HTTPSConnection"), "https_connection");
269 assert_eq!(to_snake_case("JSONData"), "json_data");
270 }
271
272 #[test]
273 fn test_to_snake_case_leading_underscore() {
274 assert_eq!(to_snake_case("_id"), "_id");
275 assert_eq!(to_snake_case("_private"), "_private");
276 assert_eq!(to_snake_case("_getUser"), "_get_user");
277 }
278
279 #[test]
280 fn test_to_snake_case_trailing_underscore() {
281 assert_eq!(to_snake_case("id_"), "id_");
282 assert_eq!(to_snake_case("name_"), "name_");
283 assert_eq!(to_snake_case("getUserName_"), "get_user_name_");
284 }
285
286 #[test]
287 fn test_to_snake_case_already_snake_case() {
288 assert_eq!(to_snake_case("get_user"), "get_user");
289 assert_eq!(to_snake_case("create_user_profile"), "create_user_profile");
290 assert_eq!(to_snake_case("http_server"), "http_server");
291 }
292
293 #[test]
294 fn test_to_snake_case_mixed_separators() {
295 assert_eq!(to_snake_case("getUser_Name"), "get_user_name");
296 assert_eq!(to_snake_case("_private_field_"), "_private_field_");
297 }
298
299 #[test]
300 fn test_to_snake_case_numbers() {
301 assert_eq!(to_snake_case("user123"), "user123");
302 assert_eq!(to_snake_case("getUser123"), "get_user123");
303 assert_eq!(to_snake_case("User123Name"), "user123_name");
304 }
305
306 #[test]
307 fn test_to_snake_case_empty() {
308 assert_eq!(to_snake_case(""), "");
309 }
310
311 #[test]
312 fn test_to_snake_case_single_char() {
313 assert_eq!(to_snake_case("a"), "a");
314 assert_eq!(to_snake_case("A"), "a");
315 assert_eq!(to_snake_case("_"), "_");
316 }
317
318 #[test]
319 fn test_to_camel_case_simple() {
320 assert_eq!(to_camel_case("user"), "user");
321 assert_eq!(to_camel_case("name"), "name");
322 assert_eq!(to_camel_case("id"), "id");
323 }
324
325 #[test]
326 fn test_to_camel_case_snake_case() {
327 assert_eq!(to_camel_case("get_user"), "getUser");
328 assert_eq!(to_camel_case("user_name"), "userName");
329 assert_eq!(to_camel_case("user_id"), "userId");
330 }
331
332 #[test]
333 fn test_to_camel_case_multiple_words() {
334 assert_eq!(to_camel_case("create_user_profile"), "createUserProfile");
335 assert_eq!(to_camel_case("get_user_by_id"), "getUserById");
336 assert_eq!(to_camel_case("http_server_config"), "httpServerConfig");
337 }
338
339 #[test]
340 fn test_to_camel_case_pascal_case_input() {
341 assert_eq!(to_camel_case("GetUser"), "GetUser");
342 assert_eq!(to_camel_case("UserName"), "UserName");
343 }
344
345 #[test]
346 fn test_to_camel_case_leading_underscore() {
347 assert_eq!(to_camel_case("_id"), "_id");
348 assert_eq!(to_camel_case("_get_user"), "_getUser");
349 assert_eq!(to_camel_case("_private"), "_private");
350 }
351
352 #[test]
353 fn test_to_camel_case_trailing_underscore() {
354 assert_eq!(to_camel_case("id_"), "id_");
355 assert_eq!(to_camel_case("get_user_"), "getUser_");
356 }
357
358 #[test]
359 fn test_to_camel_case_consecutive_separators() {
360 assert_eq!(to_camel_case("get__user"), "getUser");
361 assert_eq!(to_camel_case("user___name"), "userName");
362 }
363
364 #[test]
365 fn test_to_camel_case_numbers() {
366 assert_eq!(to_camel_case("user_123"), "user123");
367 assert_eq!(to_camel_case("get_user_123"), "getUser123");
368 }
369
370 #[test]
371 fn test_to_camel_case_empty() {
372 assert_eq!(to_camel_case(""), "");
373 }
374
375 #[test]
376 fn test_to_camel_case_single_char() {
377 assert_eq!(to_camel_case("a"), "a");
378 assert_eq!(to_camel_case("_"), "__");
379 }
380
381 #[test]
382 fn test_to_pascal_case_simple() {
383 assert_eq!(to_pascal_case("user"), "User");
384 assert_eq!(to_pascal_case("name"), "Name");
385 assert_eq!(to_pascal_case("id"), "Id");
386 }
387
388 #[test]
389 fn test_to_pascal_case_snake_case() {
390 assert_eq!(to_pascal_case("get_user"), "GetUser");
391 assert_eq!(to_pascal_case("user_name"), "UserName");
392 assert_eq!(to_pascal_case("create_user_profile"), "CreateUserProfile");
393 }
394
395 #[test]
396 fn test_to_pascal_case_camel_case() {
397 assert_eq!(to_pascal_case("getUser"), "GetUser");
398 assert_eq!(to_pascal_case("userName"), "UserName");
399 assert_eq!(to_pascal_case("createUserProfile"), "CreateUserProfile");
400 }
401
402 #[test]
403 fn test_to_pascal_case_kebab_case() {
404 assert_eq!(to_pascal_case("get-user"), "GetUser");
405 assert_eq!(to_pascal_case("user-name"), "UserName");
406 assert_eq!(to_pascal_case("http-server"), "HttpServer");
407 }
408
409 #[test]
410 fn test_to_pascal_case_mixed_separators() {
411 assert_eq!(to_pascal_case("get_user-name"), "GetUserName");
412 assert_eq!(to_pascal_case("user-name_id"), "UserNameId");
413 }
414
415 #[test]
416 fn test_to_pascal_case_numbers() {
417 assert_eq!(to_pascal_case("user_123"), "User123");
418 assert_eq!(to_pascal_case("get_user_123"), "GetUser123");
419 assert_eq!(to_pascal_case("user123name"), "User123name");
420 }
421
422 #[test]
423 fn test_to_pascal_case_leading_trailing_separators() {
424 assert_eq!(to_pascal_case("_user"), "User");
425 assert_eq!(to_pascal_case("user_"), "User");
426 assert_eq!(to_pascal_case("_user_"), "User");
427 }
428
429 #[test]
430 fn test_to_pascal_case_empty() {
431 assert_eq!(to_pascal_case(""), "");
432 }
433
434 #[test]
435 fn test_to_pascal_case_single_char() {
436 assert_eq!(to_pascal_case("a"), "A");
437 assert_eq!(to_pascal_case("_"), "");
438 }
439
440 #[test]
441 fn test_to_pascal_case_already_pascal() {
442 assert_eq!(to_pascal_case("GetUser"), "GetUser");
443 assert_eq!(to_pascal_case("UserName"), "UserName");
444 assert_eq!(to_pascal_case("CreateUserProfile"), "CreateUserProfile");
445 }
446
447 #[test]
448 fn test_to_kebab_case_simple() {
449 assert_eq!(to_kebab_case("user"), "user");
450 assert_eq!(to_kebab_case("name"), "name");
451 assert_eq!(to_kebab_case("id"), "id");
452 }
453
454 #[test]
455 fn test_to_kebab_case_camel_case() {
456 assert_eq!(to_kebab_case("getUser"), "get-user");
457 assert_eq!(to_kebab_case("userName"), "user-name");
458 assert_eq!(to_kebab_case("createUserProfile"), "create-user-profile");
459 }
460
461 #[test]
462 fn test_to_kebab_case_pascal_case() {
463 assert_eq!(to_kebab_case("GetUser"), "get-user");
464 assert_eq!(to_kebab_case("UserName"), "user-name");
465 assert_eq!(to_kebab_case("CreateUserProfile"), "create-user-profile");
466 }
467
468 #[test]
469 fn test_to_kebab_case_snake_case() {
470 assert_eq!(to_kebab_case("get_user"), "get-user");
471 assert_eq!(to_kebab_case("user_name"), "user-name");
472 assert_eq!(to_kebab_case("http_server"), "http-server");
473 }
474
475 #[test]
476 fn test_to_kebab_case_acronyms() {
477 assert_eq!(to_kebab_case("HTTPServer"), "http-server");
478 assert_eq!(to_kebab_case("GraphQLType"), "graph-ql-type");
479 assert_eq!(to_kebab_case("XMLHttpRequest"), "xml-http-request");
480 }
481
482 #[test]
483 fn test_to_kebab_case_already_kebab_case() {
484 assert_eq!(to_kebab_case("get-user"), "get-user");
485 assert_eq!(to_kebab_case("user-name"), "user-name");
486 assert_eq!(to_kebab_case("http-server"), "http-server");
487 }
488
489 #[test]
490 fn test_to_kebab_case_numbers() {
491 assert_eq!(to_kebab_case("user123"), "user123");
492 assert_eq!(to_kebab_case("getUser123"), "get-user123");
493 assert_eq!(to_kebab_case("User123Name"), "user123-name");
494 }
495
496 #[test]
497 fn test_to_kebab_case_leading_trailing_hyphens() {
498 assert_eq!(to_kebab_case("getUser-"), "get-user");
499 assert_eq!(to_kebab_case("-getUser"), "get-user");
500 assert_eq!(to_kebab_case("-getUser-"), "get-user");
501 }
502
503 #[test]
504 fn test_to_kebab_case_empty() {
505 assert_eq!(to_kebab_case(""), "");
506 }
507
508 #[test]
509 fn test_to_kebab_case_single_char() {
510 assert_eq!(to_kebab_case("a"), "a");
511 assert_eq!(to_kebab_case("A"), "a");
512 }
513
514 #[test]
515 fn test_round_trip_snake_to_camel_to_snake() {
516 let original = "get_user_profile";
517 let camel = to_camel_case(original);
518 let back = to_snake_case(&camel);
519 assert_eq!(back, original);
520 }
521
522 #[test]
523 fn test_round_trip_snake_to_pascal_to_snake() {
524 let original = "get_user_profile";
525 let pascal = to_pascal_case(original);
526 let back = to_snake_case(&pascal);
527 assert_eq!(back, original);
528 }
529
530 #[test]
531 fn test_acronym_consistency() {
532 assert_eq!(to_snake_case("HTTPServer"), "http_server");
533 assert_eq!(to_camel_case("http_server"), "httpServer");
534 assert_eq!(to_pascal_case("http_server"), "HttpServer");
535 assert_eq!(to_kebab_case("HTTPServer"), "http-server");
536 }
537
538 #[test]
539 fn test_graphql_type_consistency() {
540 let graphql = "GraphQLType";
541 assert_eq!(to_snake_case(graphql), "graph_ql_type");
542 assert_eq!(to_camel_case("graph_ql_type"), "graphQlType");
543 assert_eq!(to_pascal_case("graph_ql_type"), "GraphQlType");
544 assert_eq!(to_kebab_case(graphql), "graph-ql-type");
545 }
546
547 #[test]
548 fn test_real_world_field_names() {
549 assert_eq!(to_snake_case("userId"), "user_id");
550 assert_eq!(to_snake_case("firstName"), "first_name");
551 assert_eq!(to_snake_case("lastName"), "last_name");
552 assert_eq!(to_snake_case("createdAt"), "created_at");
553 assert_eq!(to_snake_case("updatedAt"), "updated_at");
554
555 assert_eq!(to_pascal_case("user_id"), "UserId");
556 assert_eq!(to_pascal_case("first_name"), "FirstName");
557 assert_eq!(to_pascal_case("created_at"), "CreatedAt");
558 }
559
560 #[test]
561 fn test_edge_case_empty_parts() {
562 assert_eq!(to_camel_case("__"), "__");
563 assert_eq!(to_pascal_case("__"), "");
564 assert_eq!(to_snake_case("__"), "__");
565 assert_eq!(to_kebab_case("__"), "");
566 }
567}