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
44pub 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 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 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}