Skip to main content

ploidy_codegen_rust/
naming.rs

1use std::{
2    borrow::{Borrow, Cow},
3    cmp::Ordering,
4    fmt::Display,
5    ops::Deref,
6};
7
8use heck::{AsKebabCase, AsPascalCase, AsSnekCase};
9use itertools::Itertools;
10use ploidy_core::{
11    codegen::{
12        UniqueNames,
13        unique::{UniqueNamesScope, WordSegments},
14    },
15    ir::{
16        ExtendableView, InlineTypePathSegment, InlineTypeView, PrimitiveType, SchemaTypeView,
17        StructFieldName, StructFieldNameHint, UntaggedVariantNameHint,
18    },
19};
20use proc_macro2::{Ident, Span, TokenStream};
21use quote::{IdentFragment, ToTokens, TokenStreamExt};
22use ref_cast::{RefCastCustom, ref_cast_custom};
23
24// Keywords that can't be used as identifiers, even with `r#`.
25const KEYWORDS: &[&str] = &["crate", "self", "super", "Self"];
26
27/// A name for a schema or an inline type, used in generated Rust code.
28///
29/// [`CodegenTypeName`] is the high-level representation of a type name.
30/// For emitting arbitrary identifiers, like fields, parameters, and methods,
31/// use [`CodegenIdent`] and [`CodegenIdentUsage`] instead.
32///
33/// [`CodegenTypeName`] implements [`ToTokens`] to produce PascalCase identifiers
34/// (e.g., `Pet`, `GetItemsFilter`) in [`quote`] macros.
35/// Use [`into_module_name`](Self::into_module_name) for the corresponding module name,
36/// and [`into_sort_key`](Self::into_sort_key) for deterministic sorting.
37#[derive(Clone, Copy, Debug)]
38pub enum CodegenTypeName<'a> {
39    Schema(&'a SchemaTypeView<'a, 'a>),
40    Inline(&'a InlineTypeView<'a, 'a>),
41}
42
43impl<'a> CodegenTypeName<'a> {
44    #[inline]
45    pub fn into_module_name(self) -> CodegenModuleName<'a> {
46        CodegenModuleName(self)
47    }
48
49    #[inline]
50    pub fn into_sort_key(self) -> CodegenTypeNameSortKey<'a> {
51        CodegenTypeNameSortKey(self)
52    }
53}
54
55impl ToTokens for CodegenTypeName<'_> {
56    fn to_tokens(&self, tokens: &mut TokenStream) {
57        match self {
58            Self::Schema(view) => {
59                let ident = view.extensions().get::<CodegenIdent>().unwrap();
60                CodegenIdentUsage::Type(&ident).to_tokens(tokens);
61            }
62            Self::Inline(view) => {
63                let ident = CodegenIdent::from_segments(view.path().segments);
64                CodegenIdentUsage::Type(&ident).to_tokens(tokens);
65            }
66        }
67    }
68}
69
70/// A module name derived from a [`CodegenTypeName`].
71///
72/// Implements [`ToTokens`] to produce a snake_case identifier. For
73/// string interpolation (e.g., file paths), use [`display`](Self::display),
74/// which returns an `impl Display` that can be used with `format!`.
75#[derive(Clone, Copy, Debug)]
76pub struct CodegenModuleName<'a>(CodegenTypeName<'a>);
77
78impl<'a> CodegenModuleName<'a> {
79    #[inline]
80    pub fn into_type_name(self) -> CodegenTypeName<'a> {
81        self.0
82    }
83
84    /// Returns a formattable representation of this module name.
85    ///
86    /// [`CodegenModuleName`] doesn't implement [`Display`] directly, to help catch
87    /// context mismatches: using `.display()` in a [`quote`] macro, or
88    /// `.to_token_stream()` in a [`format`] string, stands out during review.
89    pub fn display(&self) -> impl Display {
90        struct DisplayModuleName<'a>(CodegenTypeName<'a>);
91        impl Display for DisplayModuleName<'_> {
92            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93                match self.0 {
94                    CodegenTypeName::Schema(view) => {
95                        let ident = view.extensions().get::<CodegenIdent>().unwrap();
96                        write!(f, "{}", CodegenIdentUsage::Module(&ident).display())
97                    }
98                    CodegenTypeName::Inline(view) => {
99                        let ident = CodegenIdent::from_segments(view.path().segments);
100                        write!(f, "{}", CodegenIdentUsage::Module(&ident).display())
101                    }
102                }
103            }
104        }
105        DisplayModuleName(self.0)
106    }
107}
108
109impl ToTokens for CodegenModuleName<'_> {
110    fn to_tokens(&self, tokens: &mut TokenStream) {
111        match self.0 {
112            CodegenTypeName::Schema(view) => {
113                let ident = view.extensions().get::<CodegenIdent>().unwrap();
114                CodegenIdentUsage::Module(&ident).to_tokens(tokens);
115            }
116            CodegenTypeName::Inline(view) => {
117                let ident = CodegenIdent::from_segments(view.path().segments);
118                CodegenIdentUsage::Module(&ident).to_tokens(tokens);
119            }
120        }
121    }
122}
123
124/// A sort key for deterministic ordering of [`CodegenTypeName`]s.
125///
126/// Sorts schema types before inline types, then lexicographically by name.
127/// This ensures that code generation produces stable output regardless of
128/// declaration order.
129#[derive(Clone, Copy, Debug)]
130pub struct CodegenTypeNameSortKey<'a>(CodegenTypeName<'a>);
131
132impl<'a> CodegenTypeNameSortKey<'a> {
133    #[inline]
134    pub fn for_schema(view: &'a SchemaTypeView<'a, 'a>) -> Self {
135        Self(CodegenTypeName::Schema(view))
136    }
137
138    #[inline]
139    pub fn for_inline(view: &'a InlineTypeView<'a, 'a>) -> Self {
140        Self(CodegenTypeName::Inline(view))
141    }
142
143    #[inline]
144    pub fn into_name(self) -> CodegenTypeName<'a> {
145        self.0
146    }
147}
148
149impl Eq for CodegenTypeNameSortKey<'_> {}
150
151impl Ord for CodegenTypeNameSortKey<'_> {
152    fn cmp(&self, other: &Self) -> Ordering {
153        match (&self.0, &other.0) {
154            (CodegenTypeName::Schema(a), CodegenTypeName::Schema(b)) => a.name().cmp(b.name()),
155            (CodegenTypeName::Inline(a), CodegenTypeName::Inline(b)) => a.path().cmp(&b.path()),
156            (CodegenTypeName::Schema(_), CodegenTypeName::Inline(_)) => Ordering::Less,
157            (CodegenTypeName::Inline(_), CodegenTypeName::Schema(_)) => Ordering::Greater,
158        }
159    }
160}
161
162impl PartialEq for CodegenTypeNameSortKey<'_> {
163    fn eq(&self, other: &Self) -> bool {
164        self.cmp(other).is_eq()
165    }
166}
167
168impl PartialOrd for CodegenTypeNameSortKey<'_> {
169    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
170        Some(self.cmp(other))
171    }
172}
173
174/// A string that's statically guaranteed to be valid for any
175/// [`CodegenIdentUsage`].
176#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
177pub struct CodegenIdent(String);
178
179impl CodegenIdent {
180    /// Creates an identifier for any usage.
181    pub fn new(s: &str) -> Self {
182        let s = clean(s);
183        if KEYWORDS.contains(&s.as_str()) {
184            Self(format!("_{s}"))
185        } else {
186            Self(s)
187        }
188    }
189
190    /// Creates an identifier from an inline type path.
191    pub fn from_segments(segments: &[InlineTypePathSegment<'_>]) -> Self {
192        Self(format!(
193            "{}",
194            segments
195                .iter()
196                .map(CodegenTypePathSegment)
197                .format_with("", |segment, f| f(&segment.display()))
198        ))
199    }
200}
201
202impl AsRef<CodegenIdentRef> for CodegenIdent {
203    fn as_ref(&self) -> &CodegenIdentRef {
204        self
205    }
206}
207
208impl Borrow<CodegenIdentRef> for CodegenIdent {
209    fn borrow(&self) -> &CodegenIdentRef {
210        self
211    }
212}
213
214impl Deref for CodegenIdent {
215    type Target = CodegenIdentRef;
216
217    fn deref(&self) -> &Self::Target {
218        CodegenIdentRef::new(&self.0)
219    }
220}
221
222/// A string slice that's guaranteed to be valid for any [`CodegenIdentUsage`].
223#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, RefCastCustom)]
224#[repr(transparent)]
225pub struct CodegenIdentRef(str);
226
227impl CodegenIdentRef {
228    #[ref_cast_custom]
229    fn new(s: &str) -> &Self;
230
231    /// Returns a suitable identifier for a struct field,
232    /// before uniquification.
233    pub fn from_field_name_hint(hint: StructFieldNameHint) -> Cow<'static, Self> {
234        match hint {
235            StructFieldNameHint::Index(index) => {
236                Cow::Owned(CodegenIdent(format!("variant_{index}")))
237            }
238            StructFieldNameHint::AdditionalProperties => {
239                Cow::Borrowed(Self::new("additional_properties"))
240            }
241        }
242    }
243
244    /// Returns a suitable identifier for an untagged union variant,
245    /// before uniquification.
246    pub fn from_variant_name_hint(hint: UntaggedVariantNameHint) -> Cow<'static, Self> {
247        use {PrimitiveType::*, UntaggedVariantNameHint::*};
248        match hint {
249            Primitive(String) => Cow::Borrowed(Self::new("String")),
250            Primitive(I8) => Cow::Borrowed(Self::new("I8")),
251            Primitive(U8) => Cow::Borrowed(Self::new("U8")),
252            Primitive(I16) => Cow::Borrowed(Self::new("I16")),
253            Primitive(U16) => Cow::Borrowed(Self::new("U16")),
254            Primitive(I32) => Cow::Borrowed(Self::new("I32")),
255            Primitive(U32) => Cow::Borrowed(Self::new("U32")),
256            Primitive(I64) => Cow::Borrowed(Self::new("I64")),
257            Primitive(U64) => Cow::Borrowed(Self::new("U64")),
258            Primitive(F32) => Cow::Borrowed(Self::new("F32")),
259            Primitive(F64) => Cow::Borrowed(Self::new("F64")),
260            Primitive(Bool) => Cow::Borrowed(Self::new("Bool")),
261            Primitive(DateTime) => Cow::Borrowed(Self::new("DateTime")),
262            Primitive(UnixTime) => Cow::Borrowed(Self::new("UnixTime")),
263            Primitive(Date) => Cow::Borrowed(Self::new("Date")),
264            Primitive(Url) => Cow::Borrowed(Self::new("Url")),
265            Primitive(Uuid) => Cow::Borrowed(Self::new("Uuid")),
266            Primitive(Bytes) => Cow::Borrowed(Self::new("Bytes")),
267            Primitive(Binary) => Cow::Borrowed(Self::new("Binary")),
268            Array => Cow::Borrowed(Self::new("Array")),
269            Map => Cow::Borrowed(Self::new("Map")),
270            Index(index) => Cow::Owned(CodegenIdent(format!("V{index}"))),
271        }
272    }
273}
274
275impl ToOwned for CodegenIdentRef {
276    type Owned = CodegenIdent;
277
278    fn to_owned(&self) -> Self::Owned {
279        CodegenIdent(self.0.to_owned())
280    }
281}
282
283/// A Cargo feature for conditionally compiling generated code.
284///
285/// Feature names appear in the `Cargo.toml` `[features]` table,
286/// and in `#[cfg(feature = "...")]` attributes. The special `default` feature
287/// enables all other features.
288#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
289pub enum CargoFeature {
290    #[default]
291    Default,
292    Named(CodegenIdent),
293}
294
295impl CargoFeature {
296    #[inline]
297    pub fn from_name(name: &str) -> Self {
298        match name {
299            // `default` can't be used as a literal feature name; ignore it.
300            "default" => Self::Default,
301
302            // Cargo and crates.io limit which characters can appear in feature names;
303            // further, we use feature names as module names for operations, so
304            // the feature name needs to be usable as a Rust identifier.
305            name => Self::Named(CodegenIdent::new(name)),
306        }
307    }
308
309    #[inline]
310    pub fn as_ident(&self) -> &CodegenIdentRef {
311        match self {
312            Self::Named(name) => name,
313            Self::Default => CodegenIdentRef::new("default"),
314        }
315    }
316
317    #[inline]
318    pub fn display(&self) -> impl Display {
319        match self {
320            Self::Named(name) => AsKebabCase(name.0.as_str()),
321            Self::Default => AsKebabCase("default"),
322        }
323    }
324}
325
326/// A context-aware wrapper for emitting a [`CodegenIdentRef`] as a Rust identifier.
327///
328/// [`CodegenIdentUsage`] is a lower-level building block for generating
329/// identifiers. For schema and inline types, prefer [`CodegenTypeName`] instead.
330///
331/// Each [`CodegenIdentUsage`] variant determines the case transformation
332/// applied to the identifier: module, field, parameter, and method names
333/// become snake_case; type and enum variant names become PascalCase.
334///
335/// Implements [`ToTokens`] for use in [`quote`] macros. For string interpolation,
336/// use [`display`](Self::display).
337#[derive(Clone, Copy, Debug)]
338pub enum CodegenIdentUsage<'a> {
339    Module(&'a CodegenIdentRef),
340    Type(&'a CodegenIdentRef),
341    Field(&'a CodegenIdentRef),
342    Variant(&'a CodegenIdentRef),
343    Param(&'a CodegenIdentRef),
344    Method(&'a CodegenIdentRef),
345}
346
347impl CodegenIdentUsage<'_> {
348    /// Returns a formattable representation of this identifier.
349    ///
350    /// [`CodegenIdentUsage`] doesn't implement [`Display`] directly, to help catch
351    /// context mismatches: using `.display()` in a [`quote`] macro, or
352    /// `.to_token_stream()` in a [`format`] string, stands out during review.
353    pub fn display(self) -> impl Display {
354        struct DisplayUsage<'a>(CodegenIdentUsage<'a>);
355        impl Display for DisplayUsage<'_> {
356            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357                use CodegenIdentUsage::*;
358                match self.0 {
359                    Module(name) | Field(name) | Param(name) | Method(name) => {
360                        if name.0.starts_with(unicode_ident::is_xid_start) {
361                            write!(f, "{}", AsSnekCase(&name.0))
362                        } else {
363                            // `name` doesn't start with `XID_Start` (e.g., "1099KStatus"),
364                            // so prefix it with `_`; everything after is known to be
365                            // `XID_Continue`.
366                            write!(f, "_{}", AsSnekCase(&name.0))
367                        }
368                    }
369                    Type(name) | Variant(name) => {
370                        if name.0.starts_with(unicode_ident::is_xid_start) {
371                            write!(f, "{}", AsPascalCase(&name.0))
372                        } else {
373                            write!(f, "_{}", AsPascalCase(&name.0))
374                        }
375                    }
376                }
377            }
378        }
379        DisplayUsage(self)
380    }
381}
382
383impl IdentFragment for CodegenIdentUsage<'_> {
384    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
385        write!(f, "{}", self.display())
386    }
387}
388
389impl ToTokens for CodegenIdentUsage<'_> {
390    fn to_tokens(&self, tokens: &mut TokenStream) {
391        let s = self.display().to_string();
392        let ident = syn::parse_str(&s).unwrap_or_else(|_| Ident::new_raw(&s, Span::call_site()));
393        tokens.append(ident);
394    }
395}
396
397/// A scope for generating unique, valid Rust identifiers.
398#[derive(Debug)]
399pub struct CodegenIdentScope<'a>(UniqueNamesScope<'a>);
400
401impl<'a> CodegenIdentScope<'a> {
402    /// Creates a new identifier scope that's backed by the given arena.
403    pub fn new(arena: &'a UniqueNames) -> Self {
404        Self::with_reserved(arena, &[])
405    }
406
407    /// Creates a new identifier scope that's backed by the given arena,
408    /// with additional pre-reserved names.
409    pub fn with_reserved(arena: &'a UniqueNames, reserved: &[&str]) -> Self {
410        Self(arena.scope_with_reserved(itertools::chain!(
411            reserved.iter().copied(),
412            KEYWORDS.iter().copied(),
413            std::iter::once("")
414        )))
415    }
416
417    /// Cleans the input string and returns a name that's unique
418    /// within this scope, and valid for any [`CodegenIdentUsage`].
419    pub fn uniquify(&mut self, name: &str) -> CodegenIdent {
420        CodegenIdent(self.0.uniquify(&clean(name)).into_owned())
421    }
422
423    /// Uniquifies an identifier within this scope.
424    pub fn uniquify_ident(&mut self, ident: &CodegenIdentRef) -> CodegenIdent {
425        CodegenIdent(self.0.uniquify(&ident.0).into_owned())
426    }
427}
428
429#[derive(Clone, Copy, Debug)]
430pub struct CodegenTypePathSegment<'a>(&'a InlineTypePathSegment<'a>);
431
432impl<'a> CodegenTypePathSegment<'a> {
433    pub fn display(&self) -> impl Display {
434        struct DisplaySegment<'a>(&'a InlineTypePathSegment<'a>);
435        impl Display for DisplaySegment<'_> {
436            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437                use InlineTypePathSegment::*;
438                match self.0 {
439                    // Segments are always part of an identifier, never emitted directly;
440                    // so we don't need to check for `XID_Start`.
441                    Operation(name) => write!(f, "{}", AsPascalCase(clean(name))),
442                    Parameter(name) => write!(f, "{}", AsPascalCase(clean(name))),
443                    Request => f.write_str("Request"),
444                    Response => f.write_str("Response"),
445                    Field(StructFieldName::Name(name)) => {
446                        write!(f, "{}", AsPascalCase(clean(name)))
447                    }
448                    Field(StructFieldName::Hint(StructFieldNameHint::Index(index))) => {
449                        write!(f, "Variant{index}")
450                    }
451                    Field(StructFieldName::Hint(StructFieldNameHint::AdditionalProperties)) => {
452                        f.write_str("AdditionalProperties")
453                    }
454                    MapValue => f.write_str("Value"),
455                    ArrayItem => f.write_str("Item"),
456                    Variant(index) => write!(f, "V{index}"),
457                    Parent(index) => write!(f, "P{index}"),
458                    TaggedVariant(name) => write!(f, "{}", AsPascalCase(clean(name))),
459                }
460            }
461        }
462        DisplaySegment(self.0)
463    }
464}
465
466/// Makes a string suitable for inclusion within a Rust identifier.
467///
468/// Cleaning segments the string on word boundaries, collapses all
469/// non-`XID_Continue` characters into new boundaries, and
470/// reassembles the string. This makes the string resilient to
471/// case transformations, which also collapse boundaries, and so
472/// can produce duplicates in some cases.
473///
474/// Note that the result may not itself be a valid Rust identifier,
475/// because Rust identifiers must start with `XID_Start`.
476/// This is checked and handled in [`CodegenIdentUsage`].
477fn clean(s: &str) -> String {
478    WordSegments::new(s)
479        .flat_map(|s| s.split(|c| !unicode_ident::is_xid_continue(c)))
480        .join("_")
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    use pretty_assertions::assert_eq;
488    use syn::parse_quote;
489
490    // MARK: Cargo features
491
492    #[test]
493    fn test_feature_from_name() {
494        let feature = CargoFeature::from_name("customers");
495        assert_eq!(feature.display().to_string(), "customers");
496    }
497
498    #[test]
499    fn test_feature_default() {
500        let feature = CargoFeature::Default;
501        assert_eq!(feature.display().to_string(), "default");
502
503        let feature = CargoFeature::from_name("default");
504        assert_eq!(feature, CargoFeature::Default);
505    }
506
507    #[test]
508    fn test_features_from_multiple_words() {
509        let feature = CargoFeature::from_name("foo_bar");
510        assert_eq!(feature.display().to_string(), "foo-bar");
511
512        let feature = CargoFeature::from_name("foo.bar");
513        assert_eq!(feature.display().to_string(), "foo-bar");
514
515        let feature = CargoFeature::from_name("fooBar");
516        assert_eq!(feature.display().to_string(), "foo-bar");
517
518        let feature = CargoFeature::from_name("FooBar");
519        assert_eq!(feature.display().to_string(), "foo-bar");
520    }
521
522    // MARK: Usages
523
524    #[test]
525    fn test_codegen_ident_type() {
526        let ident = CodegenIdent::new("pet_store");
527        let usage = CodegenIdentUsage::Type(&ident);
528        let actual: syn::Ident = parse_quote!(#usage);
529        let expected: syn::Ident = parse_quote!(PetStore);
530        assert_eq!(actual, expected);
531    }
532
533    #[test]
534    fn test_codegen_ident_field() {
535        let ident = CodegenIdent::new("petStore");
536        let usage = CodegenIdentUsage::Field(&ident);
537        let actual: syn::Ident = parse_quote!(#usage);
538        let expected: syn::Ident = parse_quote!(pet_store);
539        assert_eq!(actual, expected);
540    }
541
542    #[test]
543    fn test_codegen_ident_module() {
544        let ident = CodegenIdent::new("MyModule");
545        let usage = CodegenIdentUsage::Module(&ident);
546        let actual: syn::Ident = parse_quote!(#usage);
547        let expected: syn::Ident = parse_quote!(my_module);
548        assert_eq!(actual, expected);
549    }
550
551    #[test]
552    fn test_codegen_ident_variant() {
553        let ident = CodegenIdent::new("http_error");
554        let usage = CodegenIdentUsage::Variant(&ident);
555        let actual: syn::Ident = parse_quote!(#usage);
556        let expected: syn::Ident = parse_quote!(HttpError);
557        assert_eq!(actual, expected);
558    }
559
560    #[test]
561    fn test_codegen_ident_param() {
562        let ident = CodegenIdent::new("userId");
563        let usage = CodegenIdentUsage::Param(&ident);
564        let actual: syn::Ident = parse_quote!(#usage);
565        let expected: syn::Ident = parse_quote!(user_id);
566        assert_eq!(actual, expected);
567    }
568
569    #[test]
570    fn test_codegen_ident_method() {
571        let ident = CodegenIdent::new("getUserById");
572        let usage = CodegenIdentUsage::Method(&ident);
573        let actual: syn::Ident = parse_quote!(#usage);
574        let expected: syn::Ident = parse_quote!(get_user_by_id);
575        assert_eq!(actual, expected);
576    }
577
578    // MARK: Special characters
579
580    #[test]
581    fn test_codegen_ident_handles_rust_keywords() {
582        let ident = CodegenIdent::new("type");
583        let usage = CodegenIdentUsage::Field(&ident);
584        let actual: syn::Ident = parse_quote!(#usage);
585        let expected: syn::Ident = parse_quote!(r#type);
586        assert_eq!(actual, expected);
587    }
588
589    #[test]
590    fn test_codegen_ident_handles_invalid_start_chars() {
591        let ident = CodegenIdent::new("123foo");
592        let usage = CodegenIdentUsage::Field(&ident);
593        let actual: syn::Ident = parse_quote!(#usage);
594        let expected: syn::Ident = parse_quote!(_123_foo);
595        assert_eq!(actual, expected);
596    }
597
598    #[test]
599    fn test_codegen_ident_handles_special_chars() {
600        let ident = CodegenIdent::new("foo-bar-baz");
601        let usage = CodegenIdentUsage::Field(&ident);
602        let actual: syn::Ident = parse_quote!(#usage);
603        let expected: syn::Ident = parse_quote!(foo_bar_baz);
604        assert_eq!(actual, expected);
605    }
606
607    #[test]
608    fn test_codegen_ident_handles_number_prefix() {
609        let ident = CodegenIdent::new("1099KStatus");
610
611        let usage = CodegenIdentUsage::Field(&ident);
612        let actual: syn::Ident = parse_quote!(#usage);
613        let expected: syn::Ident = parse_quote!(_1099_k_status);
614        assert_eq!(actual, expected);
615
616        let usage = CodegenIdentUsage::Type(&ident);
617        let actual: syn::Ident = parse_quote!(#usage);
618        let expected: syn::Ident = parse_quote!(_1099KStatus);
619        assert_eq!(actual, expected);
620    }
621
622    // MARK: Untagged variant names
623
624    #[test]
625    fn test_untagged_variant_name_index() {
626        let name = CodegenIdentRef::from_variant_name_hint(UntaggedVariantNameHint::Index(0));
627        assert_eq!(&*name, CodegenIdentRef::new("V0"));
628
629        let name = CodegenIdentRef::from_variant_name_hint(UntaggedVariantNameHint::Index(42));
630        assert_eq!(&*name, CodegenIdentRef::new("V42"));
631    }
632
633    // MARK: Struct field names
634
635    #[test]
636    fn test_struct_field_name_index() {
637        let field_name = CodegenIdentRef::from_field_name_hint(StructFieldNameHint::Index(0));
638        let usage = CodegenIdentUsage::Field(&field_name);
639        let actual: syn::Ident = parse_quote!(#usage);
640        let expected: syn::Ident = parse_quote!(variant_0);
641        assert_eq!(actual, expected);
642
643        let field_name = CodegenIdentRef::from_field_name_hint(StructFieldNameHint::Index(5));
644        let usage = CodegenIdentUsage::Field(&field_name);
645        let actual: syn::Ident = parse_quote!(#usage);
646        let expected: syn::Ident = parse_quote!(variant_5);
647        assert_eq!(actual, expected);
648    }
649
650    #[test]
651    fn test_struct_field_name_additional_properties() {
652        let field_name =
653            CodegenIdentRef::from_field_name_hint(StructFieldNameHint::AdditionalProperties);
654        let usage = CodegenIdentUsage::Field(&field_name);
655        let actual: syn::Ident = parse_quote!(#usage);
656        let expected: syn::Ident = parse_quote!(additional_properties);
657        assert_eq!(actual, expected);
658    }
659
660    // MARK: `clean()`
661
662    #[test]
663    fn test_clean() {
664        assert_eq!(clean("foo-bar"), "foo_bar");
665        assert_eq!(clean("foo.bar"), "foo_bar");
666        assert_eq!(clean("foo bar"), "foo_bar");
667        assert_eq!(clean("foo@bar"), "foo_bar");
668        assert_eq!(clean("foo#bar"), "foo_bar");
669        assert_eq!(clean("foo!bar"), "foo_bar");
670
671        assert_eq!(clean("foo_bar"), "foo_bar");
672        assert_eq!(clean("FooBar"), "Foo_Bar");
673        assert_eq!(clean("foo123"), "foo123");
674        assert_eq!(clean("_foo"), "foo");
675
676        assert_eq!(clean("_foo"), "foo");
677        assert_eq!(clean("__foo"), "foo");
678
679        // Digits are in `XID_Continue`, so they should be preserved.
680        assert_eq!(clean("123foo"), "123_foo");
681        assert_eq!(clean("9bar"), "9_bar");
682
683        // Non-ASCII characters that are valid in identifiers should be preserved;
684        // characters that aren't should be replaced.
685        assert_eq!(clean("café"), "café");
686        assert_eq!(clean("foo™bar"), "foo_bar");
687
688        // Invalid characters should be collapsed.
689        assert_eq!(clean("foo---bar"), "foo_bar");
690        assert_eq!(clean("foo...bar"), "foo_bar");
691    }
692
693    // MARK: Scopes
694
695    #[test]
696    fn test_codegen_ident_scope_handles_empty() {
697        let unique = UniqueNames::new();
698        let mut scope = CodegenIdentScope::new(&unique);
699        let ident = scope.uniquify("");
700
701        let usage = CodegenIdentUsage::Field(&ident);
702        let actual: syn::Ident = parse_quote!(#usage);
703        let expected: syn::Ident = parse_quote!(_2);
704        assert_eq!(actual, expected);
705
706        let usage = CodegenIdentUsage::Type(&ident);
707        let actual: syn::Ident = parse_quote!(#usage);
708        let expected: syn::Ident = parse_quote!(_2);
709        assert_eq!(actual, expected);
710    }
711}