1use std::fmt::{Display, Formatter, Result as FmtResult, Write};
2
3use ploidy_core::{
4 arena::Arena,
5 codegen::{AsKebabCase, AsPascalCase, AsSnakeCase, NamePart, UniqueName, UniqueNames},
6};
7
8use proc_macro2::{Ident, Span, TokenStream};
9use quote::{IdentFragment, ToTokens, TokenStreamExt};
10use unicode_ident::{is_xid_continue, is_xid_start};
11
12const KEYWORDS: &[&str] = &["crate", "self", "super", "Self"];
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
21pub struct UniqueIdent<'a>(UniqueName<'a>);
22
23#[derive(Clone, Copy, Debug)]
32pub enum CodegenIdentUsage<'a> {
33 Module(UniqueIdent<'a>),
34 Type(UniqueIdent<'a>),
35 Field(UniqueIdent<'a>),
36 Variant(UniqueIdent<'a>),
37 Param(UniqueIdent<'a>),
38 Method(UniqueIdent<'a>),
39}
40
41impl<'a> CodegenIdentUsage<'a> {
42 pub fn display(self) -> impl Display {
48 struct DisplayUsage<'a>(CodegenIdentUsage<'a>);
49 impl Display for DisplayUsage<'_> {
50 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
51 let name = self.0.to_name();
52 if !name.first_char().is_some_and(is_xid_start) {
53 f.write_char('_')?;
58 }
59 match self.0 {
60 CodegenIdentUsage::Type(_) | CodegenIdentUsage::Variant(_) => {
61 write!(f, "{}", AsPascalCase(name))
62 }
63 CodegenIdentUsage::Module(_)
64 | CodegenIdentUsage::Field(_)
65 | CodegenIdentUsage::Param(_)
66 | CodegenIdentUsage::Method(_) => {
67 write!(f, "{}", AsSnakeCase(name))
68 }
69 }
70 }
71 }
72 DisplayUsage(self)
73 }
74
75 #[inline]
76 fn to_name(self) -> UniqueName<'a> {
77 match self {
78 CodegenIdentUsage::Type(s) => s.0,
79 CodegenIdentUsage::Variant(s) => s.0,
80 CodegenIdentUsage::Module(s) => s.0,
81 CodegenIdentUsage::Field(s) => s.0,
82 CodegenIdentUsage::Param(s) => s.0,
83 CodegenIdentUsage::Method(s) => s.0,
84 }
85 }
86}
87
88impl IdentFragment for CodegenIdentUsage<'_> {
89 #[inline]
90 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
91 write!(f, "{}", self.display())
92 }
93}
94
95impl ToTokens for CodegenIdentUsage<'_> {
96 #[inline]
97 fn to_tokens(&self, tokens: &mut TokenStream) {
98 let s = self.display().to_string();
99 let ident = syn::parse_str(&s).unwrap_or_else(|_| Ident::new_raw(&s, Span::call_site()));
103 tokens.append(ident);
104 }
105}
106
107#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
116pub enum ResourceGroup<'a> {
117 Named(UniqueIdent<'a>),
118 #[default]
119 Default,
120}
121
122impl<'a> ResourceGroup<'a> {
123 #[inline]
125 pub fn name(self) -> Option<UniqueIdent<'a>> {
126 match self {
127 Self::Named(name) => Some(name),
128 Self::Default => None,
129 }
130 }
131
132 #[inline]
135 pub fn is_default(&self) -> bool {
136 matches!(self, Self::Default)
137 }
138}
139
140#[derive(Clone, Copy, Debug)]
142pub struct AsFeatureName<'a>(pub UniqueIdent<'a>);
143
144impl Display for AsFeatureName<'_> {
145 #[inline]
146 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
147 write!(f, "{}", AsKebabCase(self.0.0))
148 }
149}
150
151#[derive(Debug)]
153pub struct UniqueIdents<'a>(UniqueNames<'a>);
154
155impl<'a> UniqueIdents<'a> {
156 #[inline]
158 pub fn new(arena: &'a Arena) -> Self {
159 Self::with_reserved(arena, &[])
160 }
161
162 #[inline]
165 pub fn with_reserved(arena: &'a Arena, reserved: &[&str]) -> Self {
166 let names = UniqueNames::with_reserved(
167 arena,
168 reserved.iter().chain(KEYWORDS).map(|name| clean(name)),
169 );
170 Self(names)
171 }
172
173 #[inline]
175 pub fn claim(&mut self, name: &str) -> UniqueIdent<'a> {
176 UniqueIdent(self.0.claim(clean(name)))
177 }
178
179 #[inline]
181 pub fn adopt(&mut self, ident: UniqueIdent<'a>) -> UniqueIdent<'a> {
182 UniqueIdent(self.0.adopt(ident.0))
183 }
184}
185
186#[inline]
193fn clean(s: &str) -> impl Iterator<Item = NamePart<'_>> {
194 use itertools::intersperse;
195 intersperse(
196 s.split(|c| c == '_' || !is_xid_continue(c))
197 .filter(|s| !s.is_empty())
198 .map(NamePart::Text),
199 NamePart::Boundary,
200 )
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 use itertools::Itertools;
208 use pretty_assertions::assert_eq;
209 use syn::parse_quote;
210
211 #[test]
214 fn test_codegen_ident_type() {
215 let arena = Arena::new();
216 let mut scope = UniqueIdents::new(&arena);
217 let ident = scope.claim("pet_store");
218 let usage = CodegenIdentUsage::Type(ident);
219 let actual: syn::Ident = parse_quote!(#usage);
220 let expected: syn::Ident = parse_quote!(PetStore);
221 assert_eq!(actual, expected);
222 }
223
224 #[test]
225 fn test_codegen_ident_field() {
226 let arena = Arena::new();
227 let mut scope = UniqueIdents::new(&arena);
228 let ident = scope.claim("petStore");
229 let usage = CodegenIdentUsage::Field(ident);
230 let actual: syn::Ident = parse_quote!(#usage);
231 let expected: syn::Ident = parse_quote!(pet_store);
232 assert_eq!(actual, expected);
233 }
234
235 #[test]
236 fn test_codegen_ident_module() {
237 let arena = Arena::new();
238 let mut scope = UniqueIdents::new(&arena);
239 let ident = scope.claim("MyModule");
240 let usage = CodegenIdentUsage::Module(ident);
241 let actual: syn::Ident = parse_quote!(#usage);
242 let expected: syn::Ident = parse_quote!(my_module);
243 assert_eq!(actual, expected);
244 }
245
246 #[test]
247 fn test_codegen_ident_variant() {
248 let arena = Arena::new();
249 let mut scope = UniqueIdents::new(&arena);
250 let ident = scope.claim("http_error");
251 let usage = CodegenIdentUsage::Variant(ident);
252 let actual: syn::Ident = parse_quote!(#usage);
253 let expected: syn::Ident = parse_quote!(HttpError);
254 assert_eq!(actual, expected);
255 }
256
257 #[test]
258 fn test_codegen_ident_param() {
259 let arena = Arena::new();
260 let mut scope = UniqueIdents::new(&arena);
261 let ident = scope.claim("userId");
262 let usage = CodegenIdentUsage::Param(ident);
263 let actual: syn::Ident = parse_quote!(#usage);
264 let expected: syn::Ident = parse_quote!(user_id);
265 assert_eq!(actual, expected);
266 }
267
268 #[test]
269 fn test_codegen_ident_method() {
270 let arena = Arena::new();
271 let mut scope = UniqueIdents::new(&arena);
272 let ident = scope.claim("getUserById");
273 let usage = CodegenIdentUsage::Method(ident);
274 let actual: syn::Ident = parse_quote!(#usage);
275 let expected: syn::Ident = parse_quote!(get_user_by_id);
276 assert_eq!(actual, expected);
277 }
278
279 #[test]
282 fn test_codegen_ident_handles_rust_keywords() {
283 let arena = Arena::new();
284 let mut scope = UniqueIdents::new(&arena);
285 let ident = scope.claim("type");
286 let usage = CodegenIdentUsage::Field(ident);
287 let actual: syn::Ident = parse_quote!(#usage);
288 let expected: syn::Ident = parse_quote!(r#type);
289 assert_eq!(actual, expected);
290 }
291
292 #[test]
293 fn test_codegen_ident_handles_invalid_start_chars() {
294 let arena = Arena::new();
295 let mut scope = UniqueIdents::new(&arena);
296 let ident = scope.claim("123foo");
297 let usage = CodegenIdentUsage::Field(ident);
298 let actual: syn::Ident = parse_quote!(#usage);
299 let expected: syn::Ident = parse_quote!(_123foo);
300 assert_eq!(actual, expected);
301 }
302
303 #[test]
304 fn test_codegen_ident_handles_special_chars() {
305 let arena = Arena::new();
306 let mut scope = UniqueIdents::new(&arena);
307 let ident = scope.claim("foo-bar-baz");
308 let usage = CodegenIdentUsage::Field(ident);
309 let actual: syn::Ident = parse_quote!(#usage);
310 let expected: syn::Ident = parse_quote!(foo_bar_baz);
311 assert_eq!(actual, expected);
312 }
313
314 #[test]
315 fn test_codegen_ident_handles_number_prefix() {
316 let arena = Arena::new();
317 let mut scope = UniqueIdents::new(&arena);
318 let ident = scope.claim("1099KStatus");
319
320 let usage = CodegenIdentUsage::Field(ident);
321 let actual: syn::Ident = parse_quote!(#usage);
322 let expected: syn::Ident = parse_quote!(_1099k_status);
323 assert_eq!(actual, expected);
324
325 let usage = CodegenIdentUsage::Type(ident);
326 let actual: syn::Ident = parse_quote!(#usage);
327 let expected: syn::Ident = parse_quote!(_1099KStatus);
328 assert_eq!(actual, expected);
329 }
330
331 #[test]
334 fn test_clean_classifies_identifier_parts() {
335 use NamePart::{Boundary, Text};
336
337 assert_eq!(
338 clean("foo-bar").collect_vec(),
339 [Text("foo"), Boundary, Text("bar")]
340 );
341 assert_eq!(
342 clean("foo.bar").collect_vec(),
343 [Text("foo"), Boundary, Text("bar")]
344 );
345 assert_eq!(
346 clean("foo bar").collect_vec(),
347 [Text("foo"), Boundary, Text("bar")]
348 );
349 assert_eq!(
350 clean("foo@bar").collect_vec(),
351 [Text("foo"), Boundary, Text("bar")]
352 );
353
354 assert_eq!(
355 clean("foo_bar").collect_vec(),
356 [Text("foo"), Boundary, Text("bar")]
357 );
358 assert_eq!(clean("FooBar").collect_vec(), [Text("FooBar")]);
359 assert_eq!(clean("foo123").collect_vec(), [Text("foo123")]);
360 assert_eq!(clean("_foo").collect_vec(), [Text("foo")]);
361 assert_eq!(clean("__foo").collect_vec(), [Text("foo")]);
362
363 assert_eq!(clean("123foo").collect_vec(), [Text("123foo")]);
364 assert_eq!(clean("9bar").collect_vec(), [Text("9bar")]);
365
366 assert_eq!(clean("caf\u{e9}").collect_vec(), [Text("caf\u{e9}")]);
367 assert_eq!(
368 clean("foo\u{2122}bar").collect_vec(),
369 [Text("foo"), Boundary, Text("bar")]
370 );
371
372 assert_eq!(
373 clean("foo---bar").collect_vec(),
374 [Text("foo"), Boundary, Text("bar")]
375 );
376 assert_eq!(
377 clean("foo...bar").collect_vec(),
378 [Text("foo"), Boundary, Text("bar")]
379 );
380 }
381
382 #[test]
385 fn test_codegen_ident_empty() {
386 let arena = Arena::new();
387 let mut scope = UniqueIdents::new(&arena);
388 let ident = scope.claim("");
389
390 let usage = CodegenIdentUsage::Field(ident);
391 let actual: syn::Ident = parse_quote!(#usage);
392 let expected: syn::Ident = parse_quote!(_1);
393 assert_eq!(actual, expected);
394
395 let usage = CodegenIdentUsage::Type(ident);
396 let actual: syn::Ident = parse_quote!(#usage);
397 let expected: syn::Ident = parse_quote!(_1);
398 assert_eq!(actual, expected);
399 }
400
401 #[test]
402 fn test_codegen_ident_numeric_names() {
403 let arena = Arena::new();
404 let mut scope = UniqueIdents::new(&arena);
405
406 let ident = scope.claim("0");
407 let usage = CodegenIdentUsage::Field(ident);
408 let actual: syn::Ident = parse_quote!(#usage);
409 let expected: syn::Ident = parse_quote!(_1);
410 assert_eq!(actual, expected);
411
412 let ident = scope.claim("1");
413 let usage = CodegenIdentUsage::Type(ident);
414 let actual: syn::Ident = parse_quote!(#usage);
415 let expected: syn::Ident = parse_quote!(_2);
416 assert_eq!(actual, expected);
417 }
418
419 #[test]
420 fn test_codegen_ident_reserved_suffixes() {
421 let arena = Arena::new();
422 let mut scope = UniqueIdents::new(&arena);
423
424 let ident = scope.claim("crate");
425 let usage = CodegenIdentUsage::Method(ident);
426 let actual: syn::Ident = parse_quote!(#usage);
427 let expected: syn::Ident = parse_quote!(crate_2);
428 assert_eq!(actual, expected);
429
430 let ident = scope.claim("crate2");
431 let usage = CodegenIdentUsage::Method(ident);
432 let actual: syn::Ident = parse_quote!(#usage);
433 let expected: syn::Ident = parse_quote!(crate3);
434 assert_eq!(actual, expected);
435 }
436
437 #[test]
438 fn test_codegen_ident_respects_existing_numeric_suffix_boundary() {
439 let arena = Arena::new();
440 let mut scope = UniqueIdents::new(&arena);
441 let ident = scope.claim("get_fees1");
442
443 let usage = CodegenIdentUsage::Method(ident);
444 let actual: syn::Ident = parse_quote!(#usage);
445 let expected: syn::Ident = parse_quote!(get_fees1);
446 assert_eq!(actual, expected);
447
448 let usage = CodegenIdentUsage::Type(ident);
449 let actual: syn::Ident = parse_quote!(#usage);
450 let expected: syn::Ident = parse_quote!(GetFees1);
451 assert_eq!(actual, expected);
452 }
453
454 #[test]
455 fn test_codegen_ident_collapses_letter_digit_boundaries() {
456 let arena = Arena::new();
457 let mut scope = UniqueIdents::new(&arena);
458
459 let ident = scope.claim("s3Upload");
460 let usage = CodegenIdentUsage::Method(ident);
461 let actual: syn::Ident = parse_quote!(#usage);
462 let expected: syn::Ident = parse_quote!(s3_upload);
463 assert_eq!(actual, expected);
464
465 let ident = scope.claim("x509Cert");
466 let usage = CodegenIdentUsage::Method(ident);
467 let actual: syn::Ident = parse_quote!(#usage);
468 let expected: syn::Ident = parse_quote!(x509_cert);
469 assert_eq!(actual, expected);
470
471 let ident = scope.claim("sha256Digest");
472 let usage = CodegenIdentUsage::Method(ident);
473 let actual: syn::Ident = parse_quote!(#usage);
474 let expected: syn::Ident = parse_quote!(sha256_digest);
475 assert_eq!(actual, expected);
476
477 let ident = scope.claim("http2Protocol");
478 let usage = CodegenIdentUsage::Type(ident);
479 let actual: syn::Ident = parse_quote!(#usage);
480 let expected: syn::Ident = parse_quote!(Http2Protocol);
481 assert_eq!(actual, expected);
482 }
483
484 #[test]
485 fn test_codegen_ident_reserves_numeric_suffix_slots() {
486 let arena = Arena::new();
487 let mut scope = UniqueIdents::new(&arena);
488
489 let first = scope.claim("Response2");
490 let usage = CodegenIdentUsage::Type(first);
491 let actual: syn::Ident = parse_quote!(#usage);
492 let expected: syn::Ident = parse_quote!(Response2);
493 assert_eq!(actual, expected);
494
495 let second = scope.claim("Response_2");
496 let usage = CodegenIdentUsage::Type(second);
497 let actual: syn::Ident = parse_quote!(#usage);
498 let expected: syn::Ident = parse_quote!(Response3);
499 assert_eq!(actual, expected);
500
501 let usage = CodegenIdentUsage::Method(first);
502 let actual: syn::Ident = parse_quote!(#usage);
503 let expected: syn::Ident = parse_quote!(response2);
504 assert_eq!(actual, expected);
505
506 let usage = CodegenIdentUsage::Method(second);
507 let actual: syn::Ident = parse_quote!(#usage);
508 let expected: syn::Ident = parse_quote!(response_3);
509 assert_eq!(actual, expected);
510 }
511
512 #[test]
513 fn test_codegen_ident_deduplicates_internal_numeric_boundaries() {
514 let arena = Arena::new();
515 let mut scope = UniqueIdents::new(&arena);
516
517 let compact = scope.claim("Http2Protocol");
518 let usage = CodegenIdentUsage::Type(compact);
519 let actual: syn::Ident = parse_quote!(#usage);
520 let expected: syn::Ident = parse_quote!(Http2Protocol);
521 assert_eq!(actual, expected);
522
523 let explicit = scope.claim("Http_2Protocol");
524 let usage = CodegenIdentUsage::Type(explicit);
525 let actual: syn::Ident = parse_quote!(#usage);
526 let expected: syn::Ident = parse_quote!(Http2Protocol2);
527 assert_eq!(actual, expected);
528
529 let compact = scope.claim("Http2ProtocolVariant");
530 let usage = CodegenIdentUsage::Variant(compact);
531 let actual: syn::Ident = parse_quote!(#usage);
532 let expected: syn::Ident = parse_quote!(Http2ProtocolVariant);
533 assert_eq!(actual, expected);
534
535 let explicit = scope.claim("Http_2ProtocolVariant");
536 let usage = CodegenIdentUsage::Variant(explicit);
537 let actual: syn::Ident = parse_quote!(#usage);
538 let expected: syn::Ident = parse_quote!(Http2ProtocolVariant2);
539 assert_eq!(actual, expected);
540 }
541
542 #[test]
543 fn test_codegen_ident_adopt_preserves_numeric_suffix_boundary() {
544 let arena = Arena::new();
545 let mut schema_scope = UniqueIdents::new(&arena);
546 let schema_ident = schema_scope.claim("Response_2");
547
548 let mut variant_scope = UniqueIdents::new(&arena);
549 let response = variant_scope.claim("Response");
550 let variant_ident = variant_scope.adopt(schema_ident);
551
552 let usage = CodegenIdentUsage::Variant(response);
553 let actual: syn::Ident = parse_quote!(#usage);
554 let expected: syn::Ident = parse_quote!(Response);
555 assert_eq!(actual, expected);
556
557 let usage = CodegenIdentUsage::Variant(variant_ident);
558 let actual: syn::Ident = parse_quote!(#usage);
559 let expected: syn::Ident = parse_quote!(Response2);
560 assert_eq!(actual, expected);
561
562 let usage = CodegenIdentUsage::Method(variant_ident);
563 let actual: syn::Ident = parse_quote!(#usage);
564 let expected: syn::Ident = parse_quote!(response_2);
565 assert_eq!(actual, expected);
566 }
567
568 #[test]
571 fn test_feature_name_respects_existing_numeric_suffix_boundary() {
572 let arena = Arena::new();
573 let mut scope = UniqueIdents::new(&arena);
574 let ident = scope.claim("get_fees1");
575
576 assert_eq!(AsFeatureName(ident).to_string(), "get-fees1");
577 }
578
579 #[test]
580 fn test_feature_name_collapses_letter_digit_boundaries() {
581 let arena = Arena::new();
582 let mut scope = UniqueIdents::new(&arena);
583
584 let compact = scope.claim("oauth2Token");
585 assert_eq!(AsFeatureName(compact).to_string(), "oauth2-token");
586
587 let explicit = scope.claim("oauth_2_token");
588 assert_eq!(AsFeatureName(explicit).to_string(), "oauth-2-token-2");
589 }
590}