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#[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}