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>),
40    Inline(&'a InlineTypeView<'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>) -> Self {
135        Self(CodegenTypeName::Schema(view))
136    }
137
138    #[inline]
139    pub fn for_inline(view: &'a InlineTypeView<'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 that
232    /// doesn't have an explicit name in the spec.
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
245impl ToOwned for CodegenIdentRef {
246    type Owned = CodegenIdent;
247
248    fn to_owned(&self) -> Self::Owned {
249        CodegenIdent(self.0.to_owned())
250    }
251}
252
253/// A Cargo feature for conditionally compiling generated code.
254///
255/// Feature names appear in the `Cargo.toml` `[features]` table,
256/// and in `#[cfg(feature = "...")]` attributes. The special `default` feature
257/// enables all other features.
258#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
259pub enum CargoFeature {
260    #[default]
261    Default,
262    Named(CodegenIdent),
263}
264
265impl CargoFeature {
266    #[inline]
267    pub fn from_name(name: &str) -> Self {
268        match name {
269            // `default` can't be used as a literal feature name; ignore it.
270            "default" => Self::Default,
271
272            // Cargo and crates.io limit which characters can appear in feature names;
273            // further, we use feature names as module names for operations, so
274            // the feature name needs to be usable as a Rust identifier.
275            name => Self::Named(CodegenIdent::new(name)),
276        }
277    }
278
279    #[inline]
280    pub fn as_ident(&self) -> &CodegenIdentRef {
281        match self {
282            Self::Named(name) => name,
283            Self::Default => CodegenIdentRef::new("default"),
284        }
285    }
286
287    #[inline]
288    pub fn display(&self) -> impl Display {
289        match self {
290            Self::Named(name) => AsKebabCase(name.0.as_str()),
291            Self::Default => AsKebabCase("default"),
292        }
293    }
294}
295
296/// A context-aware wrapper for emitting a [`CodegenIdentRef`] as a Rust identifier.
297///
298/// [`CodegenIdentUsage`] is a lower-level building block for generating
299/// identifiers. For schema and inline types, prefer [`CodegenTypeName`] instead.
300///
301/// Each [`CodegenIdentUsage`] variant determines the case transformation
302/// applied to the identifier: module, field, parameter, and method names
303/// become snake_case; type and enum variant names become PascalCase.
304///
305/// Implements [`ToTokens`] for use in [`quote`] macros. For string interpolation,
306/// use [`display`](Self::display).
307#[derive(Clone, Copy, Debug)]
308pub enum CodegenIdentUsage<'a> {
309    Module(&'a CodegenIdentRef),
310    Type(&'a CodegenIdentRef),
311    Field(&'a CodegenIdentRef),
312    Variant(&'a CodegenIdentRef),
313    Param(&'a CodegenIdentRef),
314    Method(&'a CodegenIdentRef),
315}
316
317impl CodegenIdentUsage<'_> {
318    /// Returns a formattable representation of this identifier.
319    ///
320    /// [`CodegenIdentUsage`] doesn't implement [`Display`] directly, to help catch
321    /// context mismatches: using `.display()` in a [`quote`] macro, or
322    /// `.to_token_stream()` in a [`format`] string, stands out during review.
323    pub fn display(self) -> impl Display {
324        struct DisplayUsage<'a>(CodegenIdentUsage<'a>);
325        impl Display for DisplayUsage<'_> {
326            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327                use CodegenIdentUsage::*;
328                match self.0 {
329                    Module(name) | Field(name) | Param(name) | Method(name) => {
330                        if name.0.starts_with(unicode_ident::is_xid_start) {
331                            write!(f, "{}", AsSnekCase(&name.0))
332                        } else {
333                            // `name` doesn't start with `XID_Start` (e.g., "1099KStatus"),
334                            // so prefix it with `_`; everything after is known to be
335                            // `XID_Continue`.
336                            write!(f, "_{}", AsSnekCase(&name.0))
337                        }
338                    }
339                    Type(name) | Variant(name) => {
340                        if name.0.starts_with(unicode_ident::is_xid_start) {
341                            write!(f, "{}", AsPascalCase(&name.0))
342                        } else {
343                            write!(f, "_{}", AsPascalCase(&name.0))
344                        }
345                    }
346                }
347            }
348        }
349        DisplayUsage(self)
350    }
351}
352
353impl IdentFragment for CodegenIdentUsage<'_> {
354    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
355        write!(f, "{}", self.display())
356    }
357}
358
359impl ToTokens for CodegenIdentUsage<'_> {
360    fn to_tokens(&self, tokens: &mut TokenStream) {
361        let s = self.display().to_string();
362        let ident = syn::parse_str(&s).unwrap_or_else(|_| Ident::new_raw(&s, Span::call_site()));
363        tokens.append(ident);
364    }
365}
366
367/// A scope for generating unique, valid Rust identifiers.
368#[derive(Debug)]
369pub struct CodegenIdentScope<'a>(UniqueNamesScope<'a>);
370
371impl<'a> CodegenIdentScope<'a> {
372    /// Creates a new identifier scope that's backed by the given arena.
373    pub fn new(arena: &'a UniqueNames) -> Self {
374        Self::with_reserved(arena, &[])
375    }
376
377    /// Creates a new identifier scope that's backed by the given arena,
378    /// with additional pre-reserved names.
379    pub fn with_reserved(arena: &'a UniqueNames, reserved: &[&str]) -> Self {
380        Self(arena.scope_with_reserved(itertools::chain!(
381            reserved.iter().copied(),
382            KEYWORDS.iter().copied(),
383            std::iter::once("")
384        )))
385    }
386
387    /// Cleans the input string and returns a name that's unique
388    /// within this scope, and valid for any [`CodegenIdentUsage`].
389    pub fn uniquify(&mut self, name: &str) -> CodegenIdent {
390        CodegenIdent(self.0.uniquify(&clean(name)).into_owned())
391    }
392}
393
394#[derive(Clone, Copy, Debug)]
395pub struct CodegenUntaggedVariantName(pub UntaggedVariantNameHint);
396
397impl ToTokens for CodegenUntaggedVariantName {
398    fn to_tokens(&self, tokens: &mut TokenStream) {
399        use UntaggedVariantNameHint::*;
400        let s = match self.0 {
401            Primitive(PrimitiveType::String) => "String".into(),
402            Primitive(PrimitiveType::I8) => "I8".into(),
403            Primitive(PrimitiveType::U8) => "U8".into(),
404            Primitive(PrimitiveType::I16) => "I16".into(),
405            Primitive(PrimitiveType::U16) => "U16".into(),
406            Primitive(PrimitiveType::I32) => "I32".into(),
407            Primitive(PrimitiveType::U32) => "U32".into(),
408            Primitive(PrimitiveType::I64) => "I64".into(),
409            Primitive(PrimitiveType::U64) => "U64".into(),
410            Primitive(PrimitiveType::F32) => "F32".into(),
411            Primitive(PrimitiveType::F64) => "F64".into(),
412            Primitive(PrimitiveType::Bool) => "Bool".into(),
413            Primitive(PrimitiveType::DateTime) => "DateTime".into(),
414            Primitive(PrimitiveType::UnixTime) => "UnixTime".into(),
415            Primitive(PrimitiveType::Date) => "Date".into(),
416            Primitive(PrimitiveType::Url) => "Url".into(),
417            Primitive(PrimitiveType::Uuid) => "Uuid".into(),
418            Primitive(PrimitiveType::Bytes) => "Bytes".into(),
419            Primitive(PrimitiveType::Binary) => "Binary".into(),
420            Array => "Array".into(),
421            Map => "Map".into(),
422            Index(index) => Cow::Owned(format!("V{index}")),
423        };
424        tokens.append(Ident::new(&s, Span::call_site()));
425    }
426}
427
428#[derive(Clone, Copy, Debug)]
429pub struct CodegenTypePathSegment<'a>(&'a InlineTypePathSegment<'a>);
430
431impl<'a> CodegenTypePathSegment<'a> {
432    pub fn display(&self) -> impl Display {
433        struct DisplaySegment<'a>(&'a InlineTypePathSegment<'a>);
434        impl Display for DisplaySegment<'_> {
435            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436                use InlineTypePathSegment::*;
437                match self.0 {
438                    // Segments are always part of an identifier, never emitted directly;
439                    // so we don't need to check for `XID_Start`.
440                    Operation(name) => write!(f, "{}", AsPascalCase(clean(name))),
441                    Parameter(name) => write!(f, "{}", AsPascalCase(clean(name))),
442                    Request => f.write_str("Request"),
443                    Response => f.write_str("Response"),
444                    Field(StructFieldName::Name(name)) => {
445                        write!(f, "{}", AsPascalCase(clean(name)))
446                    }
447                    Field(StructFieldName::Hint(StructFieldNameHint::Index(index))) => {
448                        write!(f, "Variant{index}")
449                    }
450                    Field(StructFieldName::Hint(StructFieldNameHint::AdditionalProperties)) => {
451                        f.write_str("AdditionalProperties")
452                    }
453                    MapValue => f.write_str("Value"),
454                    ArrayItem => f.write_str("Item"),
455                    Variant(index) => write!(f, "V{index}"),
456                    Parent(index) => write!(f, "P{index}"),
457                    TaggedVariant(name) => write!(f, "{}", AsPascalCase(clean(name))),
458                }
459            }
460        }
461        DisplaySegment(self.0)
462    }
463}
464
465/// Makes a string suitable for inclusion within a Rust identifier.
466///
467/// Cleaning segments the string on word boundaries, collapses all
468/// non-`XID_Continue` characters into new boundaries, and
469/// reassembles the string. This makes the string resilient to
470/// case transformations, which also collapse boundaries, and so
471/// can produce duplicates in some cases.
472///
473/// Note that the result may not itself be a valid Rust identifier,
474/// because Rust identifiers must start with `XID_Start`.
475/// This is checked and handled in [`CodegenIdentUsage`].
476fn clean(s: &str) -> String {
477    WordSegments::new(s)
478        .flat_map(|s| s.split(|c| !unicode_ident::is_xid_continue(c)))
479        .join("_")
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485
486    use pretty_assertions::assert_eq;
487    use syn::parse_quote;
488
489    // MARK: Cargo features
490
491    #[test]
492    fn test_feature_from_name() {
493        let feature = CargoFeature::from_name("customers");
494        assert_eq!(feature.display().to_string(), "customers");
495    }
496
497    #[test]
498    fn test_feature_default() {
499        let feature = CargoFeature::Default;
500        assert_eq!(feature.display().to_string(), "default");
501
502        let feature = CargoFeature::from_name("default");
503        assert_eq!(feature, CargoFeature::Default);
504    }
505
506    #[test]
507    fn test_features_from_multiple_words() {
508        let feature = CargoFeature::from_name("foo_bar");
509        assert_eq!(feature.display().to_string(), "foo-bar");
510
511        let feature = CargoFeature::from_name("foo.bar");
512        assert_eq!(feature.display().to_string(), "foo-bar");
513
514        let feature = CargoFeature::from_name("fooBar");
515        assert_eq!(feature.display().to_string(), "foo-bar");
516
517        let feature = CargoFeature::from_name("FooBar");
518        assert_eq!(feature.display().to_string(), "foo-bar");
519    }
520
521    // MARK: Usages
522
523    #[test]
524    fn test_codegen_ident_type() {
525        let ident = CodegenIdent::new("pet_store");
526        let usage = CodegenIdentUsage::Type(&ident);
527        let actual: syn::Ident = parse_quote!(#usage);
528        let expected: syn::Ident = parse_quote!(PetStore);
529        assert_eq!(actual, expected);
530    }
531
532    #[test]
533    fn test_codegen_ident_field() {
534        let ident = CodegenIdent::new("petStore");
535        let usage = CodegenIdentUsage::Field(&ident);
536        let actual: syn::Ident = parse_quote!(#usage);
537        let expected: syn::Ident = parse_quote!(pet_store);
538        assert_eq!(actual, expected);
539    }
540
541    #[test]
542    fn test_codegen_ident_module() {
543        let ident = CodegenIdent::new("MyModule");
544        let usage = CodegenIdentUsage::Module(&ident);
545        let actual: syn::Ident = parse_quote!(#usage);
546        let expected: syn::Ident = parse_quote!(my_module);
547        assert_eq!(actual, expected);
548    }
549
550    #[test]
551    fn test_codegen_ident_variant() {
552        let ident = CodegenIdent::new("http_error");
553        let usage = CodegenIdentUsage::Variant(&ident);
554        let actual: syn::Ident = parse_quote!(#usage);
555        let expected: syn::Ident = parse_quote!(HttpError);
556        assert_eq!(actual, expected);
557    }
558
559    #[test]
560    fn test_codegen_ident_param() {
561        let ident = CodegenIdent::new("userId");
562        let usage = CodegenIdentUsage::Param(&ident);
563        let actual: syn::Ident = parse_quote!(#usage);
564        let expected: syn::Ident = parse_quote!(user_id);
565        assert_eq!(actual, expected);
566    }
567
568    #[test]
569    fn test_codegen_ident_method() {
570        let ident = CodegenIdent::new("getUserById");
571        let usage = CodegenIdentUsage::Method(&ident);
572        let actual: syn::Ident = parse_quote!(#usage);
573        let expected: syn::Ident = parse_quote!(get_user_by_id);
574        assert_eq!(actual, expected);
575    }
576
577    // MARK: Special characters
578
579    #[test]
580    fn test_codegen_ident_handles_rust_keywords() {
581        let ident = CodegenIdent::new("type");
582        let usage = CodegenIdentUsage::Field(&ident);
583        let actual: syn::Ident = parse_quote!(#usage);
584        let expected: syn::Ident = parse_quote!(r#type);
585        assert_eq!(actual, expected);
586    }
587
588    #[test]
589    fn test_codegen_ident_handles_invalid_start_chars() {
590        let ident = CodegenIdent::new("123foo");
591        let usage = CodegenIdentUsage::Field(&ident);
592        let actual: syn::Ident = parse_quote!(#usage);
593        let expected: syn::Ident = parse_quote!(_123_foo);
594        assert_eq!(actual, expected);
595    }
596
597    #[test]
598    fn test_codegen_ident_handles_special_chars() {
599        let ident = CodegenIdent::new("foo-bar-baz");
600        let usage = CodegenIdentUsage::Field(&ident);
601        let actual: syn::Ident = parse_quote!(#usage);
602        let expected: syn::Ident = parse_quote!(foo_bar_baz);
603        assert_eq!(actual, expected);
604    }
605
606    #[test]
607    fn test_codegen_ident_handles_number_prefix() {
608        let ident = CodegenIdent::new("1099KStatus");
609
610        let usage = CodegenIdentUsage::Field(&ident);
611        let actual: syn::Ident = parse_quote!(#usage);
612        let expected: syn::Ident = parse_quote!(_1099_k_status);
613        assert_eq!(actual, expected);
614
615        let usage = CodegenIdentUsage::Type(&ident);
616        let actual: syn::Ident = parse_quote!(#usage);
617        let expected: syn::Ident = parse_quote!(_1099KStatus);
618        assert_eq!(actual, expected);
619    }
620
621    // MARK: Untagged variant names
622
623    #[test]
624    fn test_untagged_variant_name_string() {
625        let variant_name =
626            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::String));
627        let actual: syn::Ident = parse_quote!(#variant_name);
628        let expected: syn::Ident = parse_quote!(String);
629        assert_eq!(actual, expected);
630    }
631
632    #[test]
633    fn test_untagged_variant_name_i32() {
634        let variant_name =
635            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::I32));
636        let actual: syn::Ident = parse_quote!(#variant_name);
637        let expected: syn::Ident = parse_quote!(I32);
638        assert_eq!(actual, expected);
639    }
640
641    #[test]
642    fn test_untagged_variant_name_i64() {
643        let variant_name =
644            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::I64));
645        let actual: syn::Ident = parse_quote!(#variant_name);
646        let expected: syn::Ident = parse_quote!(I64);
647        assert_eq!(actual, expected);
648    }
649
650    #[test]
651    fn test_untagged_variant_name_f32() {
652        let variant_name =
653            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::F32));
654        let actual: syn::Ident = parse_quote!(#variant_name);
655        let expected: syn::Ident = parse_quote!(F32);
656        assert_eq!(actual, expected);
657    }
658
659    #[test]
660    fn test_untagged_variant_name_f64() {
661        let variant_name =
662            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::F64));
663        let actual: syn::Ident = parse_quote!(#variant_name);
664        let expected: syn::Ident = parse_quote!(F64);
665        assert_eq!(actual, expected);
666    }
667
668    #[test]
669    fn test_untagged_variant_name_bool() {
670        let variant_name =
671            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::Bool));
672        let actual: syn::Ident = parse_quote!(#variant_name);
673        let expected: syn::Ident = parse_quote!(Bool);
674        assert_eq!(actual, expected);
675    }
676
677    #[test]
678    fn test_untagged_variant_name_datetime() {
679        let variant_name =
680            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::DateTime));
681        let actual: syn::Ident = parse_quote!(#variant_name);
682        let expected: syn::Ident = parse_quote!(DateTime);
683        assert_eq!(actual, expected);
684    }
685
686    #[test]
687    fn test_untagged_variant_name_date() {
688        let variant_name =
689            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::Date));
690        let actual: syn::Ident = parse_quote!(#variant_name);
691        let expected: syn::Ident = parse_quote!(Date);
692        assert_eq!(actual, expected);
693    }
694
695    #[test]
696    fn test_untagged_variant_name_url() {
697        let variant_name =
698            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::Url));
699        let actual: syn::Ident = parse_quote!(#variant_name);
700        let expected: syn::Ident = parse_quote!(Url);
701        assert_eq!(actual, expected);
702    }
703
704    #[test]
705    fn test_untagged_variant_name_uuid() {
706        let variant_name =
707            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::Uuid));
708        let actual: syn::Ident = parse_quote!(#variant_name);
709        let expected: syn::Ident = parse_quote!(Uuid);
710        assert_eq!(actual, expected);
711    }
712
713    #[test]
714    fn test_untagged_variant_name_bytes() {
715        let variant_name =
716            CodegenUntaggedVariantName(UntaggedVariantNameHint::Primitive(PrimitiveType::Bytes));
717        let actual: syn::Ident = parse_quote!(#variant_name);
718        let expected: syn::Ident = parse_quote!(Bytes);
719        assert_eq!(actual, expected);
720    }
721
722    #[test]
723    fn test_untagged_variant_name_index() {
724        let variant_name = CodegenUntaggedVariantName(UntaggedVariantNameHint::Index(0));
725        let actual: syn::Ident = parse_quote!(#variant_name);
726        let expected: syn::Ident = parse_quote!(V0);
727        assert_eq!(actual, expected);
728
729        let variant_name = CodegenUntaggedVariantName(UntaggedVariantNameHint::Index(42));
730        let actual: syn::Ident = parse_quote!(#variant_name);
731        let expected: syn::Ident = parse_quote!(V42);
732        assert_eq!(actual, expected);
733    }
734
735    #[test]
736    fn test_untagged_variant_name_array() {
737        let variant_name = CodegenUntaggedVariantName(UntaggedVariantNameHint::Array);
738        let actual: syn::Ident = parse_quote!(#variant_name);
739        let expected: syn::Ident = parse_quote!(Array);
740        assert_eq!(actual, expected);
741    }
742
743    #[test]
744    fn test_untagged_variant_name_map() {
745        let variant_name = CodegenUntaggedVariantName(UntaggedVariantNameHint::Map);
746        let actual: syn::Ident = parse_quote!(#variant_name);
747        let expected: syn::Ident = parse_quote!(Map);
748        assert_eq!(actual, expected);
749    }
750
751    // MARK: Struct field names
752
753    #[test]
754    fn test_struct_field_name_index() {
755        let field_name = CodegenIdentRef::from_field_name_hint(StructFieldNameHint::Index(0));
756        let usage = CodegenIdentUsage::Field(&field_name);
757        let actual: syn::Ident = parse_quote!(#usage);
758        let expected: syn::Ident = parse_quote!(variant_0);
759        assert_eq!(actual, expected);
760
761        let field_name = CodegenIdentRef::from_field_name_hint(StructFieldNameHint::Index(5));
762        let usage = CodegenIdentUsage::Field(&field_name);
763        let actual: syn::Ident = parse_quote!(#usage);
764        let expected: syn::Ident = parse_quote!(variant_5);
765        assert_eq!(actual, expected);
766    }
767
768    #[test]
769    fn test_struct_field_name_additional_properties() {
770        let field_name =
771            CodegenIdentRef::from_field_name_hint(StructFieldNameHint::AdditionalProperties);
772        let usage = CodegenIdentUsage::Field(&field_name);
773        let actual: syn::Ident = parse_quote!(#usage);
774        let expected: syn::Ident = parse_quote!(additional_properties);
775        assert_eq!(actual, expected);
776    }
777
778    // MARK: `clean()`
779
780    #[test]
781    fn test_clean() {
782        assert_eq!(clean("foo-bar"), "foo_bar");
783        assert_eq!(clean("foo.bar"), "foo_bar");
784        assert_eq!(clean("foo bar"), "foo_bar");
785        assert_eq!(clean("foo@bar"), "foo_bar");
786        assert_eq!(clean("foo#bar"), "foo_bar");
787        assert_eq!(clean("foo!bar"), "foo_bar");
788
789        assert_eq!(clean("foo_bar"), "foo_bar");
790        assert_eq!(clean("FooBar"), "Foo_Bar");
791        assert_eq!(clean("foo123"), "foo123");
792        assert_eq!(clean("_foo"), "foo");
793
794        assert_eq!(clean("_foo"), "foo");
795        assert_eq!(clean("__foo"), "foo");
796
797        // Digits are in `XID_Continue`, so they should be preserved.
798        assert_eq!(clean("123foo"), "123_foo");
799        assert_eq!(clean("9bar"), "9_bar");
800
801        // Non-ASCII characters that are valid in identifiers should be preserved;
802        // characters that aren't should be replaced.
803        assert_eq!(clean("café"), "café");
804        assert_eq!(clean("foo™bar"), "foo_bar");
805
806        // Invalid characters should be collapsed.
807        assert_eq!(clean("foo---bar"), "foo_bar");
808        assert_eq!(clean("foo...bar"), "foo_bar");
809    }
810
811    // MARK: Scopes
812
813    #[test]
814    fn test_codegen_ident_scope_handles_empty() {
815        let unique = UniqueNames::new();
816        let mut scope = CodegenIdentScope::new(&unique);
817        let ident = scope.uniquify("");
818
819        let usage = CodegenIdentUsage::Field(&ident);
820        let actual: syn::Ident = parse_quote!(#usage);
821        let expected: syn::Ident = parse_quote!(_2);
822        assert_eq!(actual, expected);
823
824        let usage = CodegenIdentUsage::Type(&ident);
825        let actual: syn::Ident = parse_quote!(#usage);
826        let expected: syn::Ident = parse_quote!(_2);
827        assert_eq!(actual, expected);
828    }
829}