Skip to main content

ploidy_codegen_rust/
naming.rs

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
12// Keywords that can't be used as identifiers, even with `r#`.
13const KEYWORDS: &[&str] = &["crate", "self", "super", "Self"];
14
15/// An identifier that's unique within its [`UniqueIdents`] scope.
16///
17/// Only a scope can construct these, ensuring that identifiers won't collide
18/// within that scope. Pass a [`UniqueIdent`] to a [`CodegenIdentUsage`] variant
19/// to emit it as an [`Ident`] token.
20#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
21pub struct UniqueIdent<'a>(UniqueName<'a>);
22
23/// Emits a [`UniqueIdent`] as an idiomatic Rust identifier.
24///
25/// Each [`CodegenIdentUsage`] variant determines the case transformation
26/// applied to the identifier: module, field, parameter, and method names
27/// become snake_case; type and enum variant names become PascalCase.
28///
29/// Implements [`ToTokens`] for use in [`quote`] macros. For string interpolation,
30/// use [`display`](Self::display).
31#[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    /// Returns a formattable representation of this identifier.
43    ///
44    /// [`CodegenIdentUsage`] doesn't implement [`Display`] directly, to help catch
45    /// context mismatches: using `.display()` in a [`quote`] macro, or
46    /// `.to_token_stream()` in a [`format`] string, stands out during review.
47    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                    // Rust identifiers must start with an `XID_Start` character
54                    // or `_`. `clean()` explicitly treats `_` as a separator,
55                    // so prepending `_` to the unique name here is guaranteed
56                    // not to collide with any other identifier.
57                    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        // Assume `s` is a keyword that must be rendered as a raw identifier
100        // if `parse_str` fails. A string that's not a valid identifier here
101        // is a logic error.
102        let ident = syn::parse_str(&s).unwrap_or_else(|_| Ident::new_raw(&s, Span::call_site()));
103        tokens.append(ident);
104    }
105}
106
107/// A key used to group a resource's operations into modules
108/// and derive Cargo features for resource operations and types.
109///
110/// [`Named`] wraps a uniquified resource name; [`Default`] represents
111/// operations and types without a resource name.
112///
113/// [`Named`]: Self::Named
114/// [`Default`]: Self::Default
115#[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    /// Returns the resource name for a [`Named`][Self::Named] group.
124    #[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    /// Returns whether this group represents operations and types
133    /// without a resource name.
134    #[inline]
135    pub fn is_default(&self) -> bool {
136        matches!(self, Self::Default)
137    }
138}
139
140/// Formats a uniquified resource name as a Cargo feature name.
141#[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/// A scope for generating unique, valid Rust identifiers.
152#[derive(Debug)]
153pub struct UniqueIdents<'a>(UniqueNames<'a>);
154
155impl<'a> UniqueIdents<'a> {
156    /// Creates a new identifier scope that's backed by the given arena.
157    #[inline]
158    pub fn new(arena: &'a Arena) -> Self {
159        Self::with_reserved(arena, &[])
160    }
161
162    /// Creates a new identifier scope that's backed by the given arena,
163    /// with additional pre-reserved names.
164    #[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    /// Uniquifies an identifier fragment.
174    #[inline]
175    pub fn claim(&mut self, name: &str) -> UniqueIdent<'a> {
176        UniqueIdent(self.0.claim(clean(name)))
177    }
178
179    /// Uniquifies an identifier from another scope.
180    #[inline]
181    pub fn adopt(&mut self, ident: UniqueIdent<'a>) -> UniqueIdent<'a> {
182        UniqueIdent(self.0.adopt(ident.0))
183    }
184}
185
186/// Splits an identifier fragment into name parts for [`UniqueNames`].
187///
188/// Returns non-empty text spans as text parts, with one boundary
189/// between adjacent spans. Text spans contain all `XID_Continue` characters
190/// except `_`; all others are separators. Leading, trailing, and repeated
191/// separators are discarded.
192#[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    // MARK: Usages
212
213    #[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    // MARK: Special characters
280
281    #[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    // MARK: `clean()`
332
333    #[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    // MARK: Edge cases
383
384    #[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    // MARK: Cargo features
569
570    #[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}