Skip to main content

sqlc_gen_sqlx/
ident.rs

1use convert_case::{Case, Casing as _};
2
3const KEYWORDS: &[&str] = &[
4    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
5    "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
6    "ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "union",
7    "unsafe", "use", "where", "while", "abstract", "become", "box", "do", "final", "macro",
8    "override", "priv", "try", "typeof", "unsized", "virtual", "yield",
9];
10
11pub fn normalize_ident(s: &str) -> String {
12    s.chars()
13        .map(|c| {
14            if c.is_alphanumeric() || c == '_' {
15                c
16            } else {
17                '_'
18            }
19        })
20        .collect()
21}
22
23pub fn to_snake_case(s: &str) -> String {
24    normalize_ident(s).to_case(Case::Snake)
25}
26
27pub fn to_pascal_case(s: &str) -> String {
28    normalize_ident(s).to_case(Case::Pascal)
29}
30
31pub fn field_ident(name: &str) -> proc_macro2::Ident {
32    let snake = to_snake_case(name);
33    if KEYWORDS.contains(&snake.as_str()) {
34        quote::format_ident!("r#{}", snake)
35    } else {
36        quote::format_ident!("{}", snake)
37    }
38}
39
40pub fn type_ident(name: &str) -> proc_macro2::Ident {
41    quote::format_ident!("{}", to_pascal_case(name))
42}
43
44/// For enum variants: PascalCase with keyword escaping.
45/// Converts `val` to PascalCase, then checks whether the result would
46/// collide with a Rust keyword by checking both the PascalCase form and
47/// its snake_case equivalent against the keyword list.
48/// Example: "type" → PascalCase "Type" → snake "type" → keyword → emits `r#Type`.
49pub fn variant_ident(name: &str) -> proc_macro2::Ident {
50    let pascal = to_pascal_case(name);
51    let snake = to_snake_case(&pascal);
52    if KEYWORDS.contains(&pascal.as_str()) || KEYWORDS.contains(&snake.as_str()) {
53        // Only use raw identifier if the pascal form itself can be raw-escaped.
54        // Some keywords like `Self` cannot appear as raw identifiers, so we
55        // append an underscore as a fallback.
56        if !["Self", "true", "false"].contains(&pascal.as_str()) {
57            quote::format_ident!("r#{}", pascal)
58        } else {
59            quote::format_ident!("{}_", pascal)
60        }
61    } else {
62        quote::format_ident!("{}", pascal)
63    }
64}
65
66pub fn query_params_name(query_name: &str) -> String {
67    format!("{}Params", to_pascal_case(query_name))
68}
69
70pub fn query_row_name(query_name: &str) -> String {
71    format!("{}Row", to_pascal_case(query_name))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn snake_from_pascal() {
80        assert_eq!(to_snake_case("GetAuthor"), "get_author");
81    }
82    #[test]
83    fn snake_idempotent() {
84        assert_eq!(to_snake_case("get_author"), "get_author");
85    }
86    #[test]
87    fn pascal_from_snake() {
88        assert_eq!(to_pascal_case("get_author"), "GetAuthor");
89    }
90    #[test]
91    fn pascal_idempotent() {
92        assert_eq!(to_pascal_case("GetAuthor"), "GetAuthor");
93    }
94
95    #[test]
96    fn keyword_type_escaped() {
97        let id = field_ident("type");
98        assert_eq!(id.to_string(), "r#type");
99    }
100    #[test]
101    fn keyword_for_escaped() {
102        let id = field_ident("for");
103        assert_eq!(id.to_string(), "r#for");
104    }
105    #[test]
106    fn non_keyword_unchanged() {
107        let id = field_ident("name");
108        assert_eq!(id.to_string(), "name");
109    }
110
111    #[test]
112    fn normalize_hyphens() {
113        assert_eq!(normalize_ident("some-name"), "some_name");
114    }
115    #[test]
116    fn normalize_colons() {
117        assert_eq!(normalize_ident("some:name"), "some_name");
118    }
119
120    #[test]
121    fn variant_ident_self_is_escaped() {
122        let id = variant_ident("self");
123        // "self" → PascalCase "Self" → keyword, but r#Self is invalid → Self_
124        assert_eq!(id.to_string(), "Self_");
125    }
126
127    #[test]
128    fn variant_ident_normal_value() {
129        let id = variant_ident("active");
130        assert_eq!(id.to_string(), "Active");
131    }
132
133    #[test]
134    fn params_name_works() {
135        assert_eq!(query_params_name("GetAuthor"), "GetAuthorParams");
136    }
137    #[test]
138    fn row_name_works() {
139        assert_eq!(query_row_name("GetAuthor"), "GetAuthorRow");
140    }
141}