1use proc_macro2::Ident;
5use proc_macro2::Span;
6
7pub(crate) const X_RUST_NAME: &str = "x-rust-name";
13
14mod casing;
16
17pub mod operations;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct RustIdent {
24 text: String,
26 raw: bool,
28}
29
30impl RustIdent {
31 pub fn logical(&self) -> &str {
33 return &self.text;
34 }
35
36 pub fn to_token(&self) -> Ident {
38 let ident = if self.raw {
39 Ident::new_raw(&self.text, Span::call_site())
40 } else {
41 Ident::new(&self.text, Span::call_site())
42 };
43 return ident;
44 }
45}
46
47#[derive(Debug, Clone, Copy)]
49pub enum Case {
50 Pascal,
52 Snake,
54 ScreamingSnake,
56}
57
58pub fn to_ident(name: &str, case: Case) -> RustIdent {
61 let cased = match case {
62 Case::Pascal => casing::to_upper_camel_case(name),
63 Case::Snake => casing::to_snake_case(name),
64 Case::ScreamingSnake => casing::to_screaming_snake_case(name),
65 };
66
67 let cased = if cased.is_empty() { "Unnamed".to_owned() } else { cased };
68
69 let starts_with_digit = cased.chars().next().map(char::is_numeric).unwrap_or(false);
71 let cased = if starts_with_digit { format!("_{cased}") } else { cased };
72
73 match classify_ident(&cased) {
74 IdentForm::Plain => {
75 return RustIdent {
76 text: cased,
77 raw: false,
78 };
79 }
80 IdentForm::Raw => {
81 return RustIdent { text: cased, raw: true };
82 }
83 IdentForm::Suffix => {
84 return RustIdent {
85 text: format!("{cased}_"),
86 raw: false,
87 };
88 }
89 }
90}
91
92pub fn rename_for(wire: &str, ident: &RustIdent) -> Option<String> {
95 if ident.logical() == wire {
96 return None;
97 }
98 return Some(wire.to_owned());
99}
100
101pub fn deconflict_ident(ident: RustIdent, seen: &mut std::collections::HashSet<String>) -> RustIdent {
114 if seen.insert(ident.logical().to_owned()) {
115 return ident;
116 }
117 let mut suffix: u32 = 2;
118 loop {
119 let candidate = to_ident(&format!("{} {suffix}", ident.logical()), Case::Pascal);
120 if seen.insert(candidate.logical().to_owned()) {
121 return candidate;
122 }
123 suffix += 1;
124 }
125}
126
127enum IdentForm {
129 Plain,
131 Raw,
133 Suffix,
135}
136
137fn classify_ident(s: &str) -> IdentForm {
145 if syn::parse_str::<syn::Ident>(s).is_ok() && !is_edition_2024_keyword(s) {
146 return IdentForm::Plain;
147 }
148 if syn::parse_str::<syn::Ident>(&format!("r#{s}")).is_ok() {
149 return IdentForm::Raw;
150 }
151 return IdentForm::Suffix;
152}
153
154fn is_edition_2024_keyword(s: &str) -> bool {
158 return matches!(s, "gen");
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn pascal_case_types() {
167 let cases = [
168 ("ErrorResponse", "ErrorResponse", false),
169 ("payment_form", "PaymentForm", false),
170 ("da", "Da", false),
171 ("PET_SHOP_SIGNUP_REQUEST", "PetShopSignupRequest", false),
172 ];
173 for (input, expected, raw) in cases {
174 let ident = to_ident(input, Case::Pascal);
175 assert_eq!(ident.logical(), expected, "input {input}");
176 assert_eq!(ident.raw, raw, "input {input}");
177 }
178 }
179
180 #[test]
181 fn snake_case_fields_and_keywords() {
182 let ty = to_ident("type", Case::Snake);
183 assert_eq!(ty.logical(), "type");
184 assert!(ty.raw, "`type` should be a raw identifier");
185 assert_eq!(rename_for("type", &ty), None);
186
187 let email = to_ident("customer_email", Case::Snake);
188 assert_eq!(email.logical(), "customer_email");
189 assert!(!email.raw);
190 assert_eq!(rename_for("customer_email", &email), None);
191 }
192
193 #[test]
194 fn rename_when_casing_differs() {
195 let ident = to_ident("da", Case::Pascal);
196 assert_eq!(rename_for("da", &ident), Some("da".to_owned()));
197 }
198
199 #[test]
200 fn keyword_that_cannot_be_raw_is_suffixed() {
201 let ident = to_ident("self", Case::Snake);
202 assert_eq!(ident.logical(), "self_");
203 assert!(!ident.raw);
204 }
205
206 #[test]
207 fn edition_2024_keyword_is_raw() {
208 let ident = to_ident("gen", Case::Snake);
211 assert_eq!(ident.logical(), "gen");
212 assert!(ident.raw, "`gen` should be a raw identifier under edition 2024");
213 }
214
215 #[test]
216 fn deconflict_ident_suffixes_collisions() {
217 let mut seen = std::collections::HashSet::new();
218 let first = deconflict_ident(to_ident("in-progress", Case::Pascal), &mut seen);
221 let second = deconflict_ident(to_ident("inProgress", Case::Pascal), &mut seen);
222 let third = deconflict_ident(to_ident("In_Progress", Case::Pascal), &mut seen);
223 assert_eq!(first.logical(), "InProgress");
224 assert_eq!(second.logical(), "InProgress2");
225 assert_eq!(third.logical(), "InProgress3");
226 let other = deconflict_ident(to_ident("done", Case::Pascal), &mut seen);
228 assert_eq!(other.logical(), "Done");
229 }
230
231 #[test]
232 fn casing_word_boundaries() {
233 let snake = [
235 ("CamelCase", "camel_case"),
236 ("XMLHttpRequest", "xml_http_request"),
237 ("FIELD_NAME11", "field_name11"),
238 (
239 "this-contains_ ALLKinds OfWord_Boundaries",
240 "this_contains_all_kinds_of_word_boundaries",
241 ),
242 ];
243 for (input, expected) in snake {
244 assert_eq!(casing::to_snake_case(input), expected, "snake {input}");
245 }
246
247 let pascal = [
248 ("CamelCase", "CamelCase"),
249 ("XMLHttpRequest", "XmlHttpRequest"),
250 ("SHOUTY_SNAKE_CASE", "ShoutySnakeCase"),
251 (
252 "this-contains_ ALLKinds OfWord_Boundaries",
253 "ThisContainsAllKindsOfWordBoundaries",
254 ),
255 ];
256 for (input, expected) in pascal {
257 assert_eq!(casing::to_upper_camel_case(input), expected, "pascal {input}");
258 }
259 }
260}