Skip to main content

ploidy_codegen_rust/
naming.rs

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