ploidy_codegen_rust/
naming.rs

1use std::borrow::Cow;
2
3use heck::{ToPascalCase, ToSnakeCase};
4use ploidy_core::ir::{
5    InlineIrTypePath, InlineIrTypePathSegment, IrStructFieldName, IrStructFieldNameHint,
6    IrUntaggedVariantNameHint, PrimitiveIrType,
7};
8use proc_macro2::{Ident, Span, TokenStream};
9use quote::{IdentFragment, ToTokens, TokenStreamExt, format_ident};
10
11/// A name for a schema type that's guaranteed to be unique through
12/// different identifier case transformations.
13#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
14pub struct SchemaIdent(pub String);
15
16impl SchemaIdent {
17    pub fn module(&self) -> CodegenIdent<'_> {
18        CodegenIdent::Module(&self.0)
19    }
20
21    pub fn ty(&self) -> CodegenIdent<'_> {
22        CodegenIdent::Type(&self.0)
23    }
24}
25
26#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
27pub enum CodegenTypeName<'a> {
28    Schema(&'a str, &'a SchemaIdent),
29    Inline(&'a InlineIrTypePath<'a>),
30}
31
32impl ToTokens for CodegenTypeName<'_> {
33    fn to_tokens(&self, tokens: &mut TokenStream) {
34        match self {
35            &Self::Schema(_, ident) => tokens.append_all(ident.ty().to_token_stream()),
36            Self::Inline(path) => {
37                let ident = path
38                    .segments
39                    .iter()
40                    .map(CodegenTypePathSegment)
41                    .fold(None, |ident, segment| {
42                        Some(match ident {
43                            Some(ident) => format_ident!("{}{}", ident, segment),
44                            None => format_ident!("{}", segment),
45                        })
46                    })
47                    .ok_or_else(|| syn::Error::new(Span::call_site(), "empty inline type path"));
48                match ident {
49                    Ok(ident) => tokens.append(ident),
50                    Err(err) => tokens.append_all(err.into_compile_error()),
51                }
52            }
53        }
54    }
55}
56
57#[derive(Clone, Copy, Debug)]
58pub enum CodegenIdent<'a> {
59    Module(&'a str),
60    Type(&'a str),
61    Field(&'a str),
62    Variant(&'a str),
63    Param(&'a str),
64    Var(&'a str),
65    Method(&'a str),
66}
67
68impl<'a> CodegenIdent<'a> {
69    fn name(&self) -> &'a str {
70        let (Self::Module(s)
71        | Self::Type(s)
72        | Self::Field(s)
73        | Self::Variant(s)
74        | Self::Param(s)
75        | Self::Var(s)
76        | Self::Method(s)) = self;
77        s
78    }
79}
80
81impl ToTokens for CodegenIdent<'_> {
82    fn to_tokens(&self, tokens: &mut TokenStream) {
83        let cased = match self {
84            Self::Module(name)
85            | Self::Field(name)
86            | Self::Param(name)
87            | Self::Method(name)
88            | Self::Var(name) => name.to_snake_case(),
89            Self::Type(name) | Self::Variant(name) => name.to_pascal_case(),
90        };
91        let cleaned = clean(&cased);
92        let ident: syn::Result<Ident> = syn::parse_str(&cleaned)
93            .or_else(|_| syn::parse_str(&format!("r#{cleaned}")))
94            .or_else(|_| syn::parse_str(&format!("{cleaned}_")))
95            .map_err(|_| {
96                syn::Error::new(
97                    Span::call_site(),
98                    format!(
99                        "`{}` can't be represented as a Rust identifier",
100                        self.name()
101                    ),
102                )
103            });
104        match ident {
105            Ok(ident) => tokens.append(ident),
106            Err(err) => tokens.append_all(err.into_compile_error()),
107        }
108    }
109}
110
111#[derive(Clone, Copy, Debug)]
112pub struct CodegenUntaggedVariantName(pub IrUntaggedVariantNameHint);
113
114impl ToTokens for CodegenUntaggedVariantName {
115    fn to_tokens(&self, tokens: &mut TokenStream) {
116        use IrUntaggedVariantNameHint::*;
117        let s = match self.0 {
118            Primitive(PrimitiveIrType::String) => "String".into(),
119            Primitive(PrimitiveIrType::I32) => "I32".into(),
120            Primitive(PrimitiveIrType::I64) => "I64".into(),
121            Primitive(PrimitiveIrType::F32) => "F32".into(),
122            Primitive(PrimitiveIrType::F64) => "F64".into(),
123            Primitive(PrimitiveIrType::Bool) => "Bool".into(),
124            Primitive(PrimitiveIrType::DateTime) => "DateTime".into(),
125            Primitive(PrimitiveIrType::Date) => "Date".into(),
126            Primitive(PrimitiveIrType::Url) => "Url".into(),
127            Primitive(PrimitiveIrType::Uuid) => "Uuid".into(),
128            Primitive(PrimitiveIrType::Bytes) => "Bytes".into(),
129            Array => "Array".into(),
130            Map => "Map".into(),
131            Index(index) => Cow::Owned(format!("V{index}")),
132        };
133        tokens.append(Ident::new(&s, Span::call_site()));
134    }
135}
136
137#[derive(Clone, Copy, Debug)]
138pub struct CodegenStructFieldName(pub IrStructFieldNameHint);
139
140impl ToTokens for CodegenStructFieldName {
141    fn to_tokens(&self, tokens: &mut TokenStream) {
142        match self.0 {
143            IrStructFieldNameHint::Index(index) => {
144                CodegenIdent::Field(&format!("variant_{index}")).to_tokens(tokens)
145            }
146        }
147    }
148}
149
150#[derive(Clone, Copy, Debug)]
151pub struct CodegenTypePathSegment<'a>(&'a InlineIrTypePathSegment<'a>);
152
153impl IdentFragment for CodegenTypePathSegment<'_> {
154    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
155        match self.0 {
156            InlineIrTypePathSegment::Operation(name) => f.write_str(&name.to_pascal_case()),
157            InlineIrTypePathSegment::Parameter(name) => f.write_str(&name.to_pascal_case()),
158            InlineIrTypePathSegment::Request => f.write_str("Request"),
159            InlineIrTypePathSegment::Response => f.write_str("Response"),
160            InlineIrTypePathSegment::Field(name) => match name {
161                IrStructFieldName::Name(name) => f.write_str(&name.to_pascal_case()),
162                IrStructFieldName::Hint(IrStructFieldNameHint::Index(index)) => {
163                    write!(f, "Variant{index}")
164                }
165            },
166            InlineIrTypePathSegment::MapValue => f.write_str("Value"),
167            InlineIrTypePathSegment::ArrayItem => f.write_str("Item"),
168            InlineIrTypePathSegment::Variant(index) => write!(f, "V{index}"),
169        }
170    }
171}
172
173pub fn clean(s: &str) -> String {
174    let mut chars = s.chars();
175    let Some(first) = chars.next() else {
176        return String::new();
177    };
178    let mut string = String::with_capacity(s.len());
179    if first == '_' || unicode_ident::is_xid_start(first) {
180        string.push(first);
181    } else {
182        string.push('_');
183        chars = s.chars();
184    }
185    string.push_str(
186        &chars
187            .as_str()
188            .replace(|next| !unicode_ident::is_xid_continue(next), "_"),
189    );
190    string
191}