openapi_trait_shared/codegen/
idents.rs1use heck::{ToPascalCase, ToSnakeCase};
13use proc_macro2::{Ident, Span};
14
15const RAW_SAFE_KEYWORDS: &[&str] = &[
21 "as", "async", "await", "break", "const", "continue", "dyn", "else", "enum", "extern", "false",
23 "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
24 "return", "static", "struct", "trait", "true", "type", "unsafe", "use", "where", "while",
25 "abstract", "become", "box", "do", "final", "macro", "override", "priv", "try", "typeof",
27 "unsized", "virtual", "yield",
28];
29
30const NON_RAW_KEYWORDS: &[&str] = &["crate", "self", "super", "Self"];
33
34fn is_plain_ident(s: &str) -> bool {
40 if s.is_empty() || s == "_" {
41 return false;
42 }
43 let mut chars = s.chars();
44 let first = chars.next().expect("non-empty checked above");
45 if !(first.is_ascii_alphabetic() || first == '_') {
46 return false;
47 }
48 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
49}
50
51pub fn safe_ident(converted: &str, original: &str, role: &str) -> Result<Ident, String> {
62 if converted.is_empty() {
63 return Err(format!(
64 "{role} `{original}` produces an empty Rust identifier; rename it"
65 ));
66 }
67 if NON_RAW_KEYWORDS.contains(&converted) {
68 return Err(format!(
69 "{role} `{original}` maps to the reserved Rust keyword `{converted}`, which cannot be used as an identifier (not even as a raw identifier); rename it"
70 ));
71 }
72 if RAW_SAFE_KEYWORDS.contains(&converted) {
73 return Ok(Ident::new_raw(converted, Span::call_site()));
74 }
75 if !is_plain_ident(converted) {
76 return Err(format!(
77 "{role} `{original}` produces the invalid Rust identifier `{converted}`; identifiers must start with a letter or underscore and contain only letters, digits, and underscores"
78 ));
79 }
80 Ok(Ident::new(converted, Span::call_site()))
81}
82
83pub fn method_ident(operation_id: &str) -> Result<Ident, String> {
89 safe_ident(&operation_id.to_snake_case(), operation_id, "operationId")
90}
91
92pub fn field_ident(param_name: &str) -> Result<Ident, String> {
98 safe_ident(¶m_name.to_snake_case(), param_name, "parameter")
99}
100
101pub fn validate_type_base(operation_id: &str) -> Result<(), String> {
112 let pascal = operation_id.to_pascal_case();
113 if !is_plain_ident(&pascal) {
114 return Err(format!(
115 "operationId `{operation_id}` produces the invalid Rust type-name base `{pascal}`; it must start with a letter or underscore and contain only letters, digits, and underscores"
116 ));
117 }
118 Ok(())
119}
120
121pub fn type_ident(name: &str, source: &str) -> Result<Ident, String> {
128 if !is_plain_ident(name) {
129 return Err(format!(
130 "synthesized type name `{name}` (from `{source}`) is not a valid Rust identifier"
131 ));
132 }
133 Ok(Ident::new(name, Span::call_site()))
134}
135
136#[must_use]
143pub fn keyword_safe_ident(name: &str) -> Ident {
144 if NON_RAW_KEYWORDS.contains(&name) {
145 return Ident::new(&format!("{name}_"), Span::call_site());
146 }
147 if RAW_SAFE_KEYWORDS.contains(&name) {
148 return Ident::new_raw(name, Span::call_site());
149 }
150 Ident::new(name, Span::call_site())
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn keyword_method_becomes_raw_ident() {
159 let id = method_ident("type").expect("keyword is raw-safe");
160 assert_eq!(id.to_string(), "r#type");
161 assert_eq!(method_ident("async").unwrap().to_string(), "r#async");
162 }
163
164 #[test]
165 fn hyphenated_and_clean_names_pass_through() {
166 assert_eq!(method_ident("list-pets").unwrap().to_string(), "list_pets");
167 assert_eq!(
168 method_ident("getPetById").unwrap().to_string(),
169 "get_pet_by_id"
170 );
171 }
172
173 #[test]
174 fn leading_digit_is_an_error() {
175 let err = method_ident("1pet").unwrap_err();
176 assert!(err.contains("operationId `1pet`"), "{err}");
177 }
178
179 #[test]
180 fn empty_after_conversion_is_an_error() {
181 assert!(method_ident("___").is_err());
182 }
183
184 #[test]
185 fn non_raw_keyword_is_an_error() {
186 let err = method_ident("self").unwrap_err();
187 assert!(err.contains("reserved Rust keyword"), "{err}");
188 }
189
190 #[test]
191 fn keyword_param_field_becomes_raw_ident() {
192 assert_eq!(field_ident("type").unwrap().to_string(), "r#type");
193 }
194
195 #[test]
196 fn type_base_keyword_is_fine_but_digit_is_not() {
197 assert!(validate_type_base("type").is_ok());
199 assert!(validate_type_base("1pet").is_err());
200 }
201
202 #[test]
203 fn infallible_helper_handles_non_raw_keyword() {
204 assert_eq!(keyword_safe_ident("self").to_string(), "self_");
205 assert_eq!(keyword_safe_ident("type").to_string(), "r#type");
206 assert_eq!(keyword_safe_ident("name").to_string(), "name");
207 }
208}