Skip to main content

fraiseql_core/utils/
casing.rs

1//! Field name case conversion (camelCase → `snake_case`).
2//!
3//! This module handles converting GraphQL field names (typically camelCase)
4//! to PostgreSQL column names (typically `snake_case`).
5
6/// The canonical field-name → `snake_case` JSONB-key rule.
7///
8/// Re-exported from `fraiseql-db` so the SQL projection generators and this
9/// crate's Rust entity projector share **one** definition — eliminating the
10/// historical drift where two copies disagreed on acronym field names
11/// (`userID` → `user_id`, never `user_i_d`). See
12/// [`fraiseql_db::utils::to_snake_case`].
13pub use fraiseql_db::utils::{set_runtime_acronyms, to_snake_case};
14
15/// Convert `snake_case` to camelCase.
16///
17/// This is the inverse of [`to_snake_case`]: a digit segment collapses onto the
18/// previous word (`phone_1` → `phone1`), and `to_snake_case` reinserts the
19/// boundary (`phone1` → `phone_1`), so the round trip is bijective. See
20/// [`to_snake_case`] for the digit caveat (`oauth2`/`ipv4`/`s3`).
21///
22/// # Examples
23///
24/// ```
25/// use fraiseql_core::utils::casing::to_camel_case;
26///
27/// assert_eq!(to_camel_case("user_id"), "userId");
28/// assert_eq!(to_camel_case("created_at"), "createdAt");
29/// assert_eq!(to_camel_case("http_response"), "httpResponse");
30/// assert_eq!(to_camel_case("phone_1"), "phone1");
31/// assert_eq!(to_camel_case("dns_1_id"), "dns1Id");
32/// assert_eq!(to_camel_case("alreadyCamel"), "alreadyCamel");
33/// ```
34#[must_use]
35pub fn to_camel_case(s: &str) -> String {
36    // If no underscores, assume already camelCase
37    if !s.contains('_') {
38        return s.to_string();
39    }
40
41    let mut result = String::with_capacity(s.len());
42    let mut capitalize_next = false;
43
44    for c in s.chars() {
45        if c == '_' {
46            capitalize_next = true;
47        } else if capitalize_next {
48            result.push(c.to_ascii_uppercase());
49            capitalize_next = false;
50        } else {
51            result.push(c);
52        }
53    }
54
55    result
56}
57
58/// Normalize a field path for database access.
59///
60/// This handles dotted paths like "user.profile.name" and converts each segment.
61///
62/// # Examples
63///
64/// ```
65/// use fraiseql_core::utils::casing::normalize_field_path;
66///
67/// assert_eq!(normalize_field_path("userId"), "user_id");
68/// assert_eq!(normalize_field_path("user.createdAt"), "user.created_at");
69/// assert_eq!(normalize_field_path("device.sensor.currentValue"), "device.sensor.current_value");
70/// ```
71#[must_use]
72pub fn normalize_field_path(path: &str) -> String {
73    if !path.contains('.') {
74        return to_snake_case(path);
75    }
76
77    path.split('.').map(to_snake_case).collect::<Vec<_>>().join(".")
78}