1use convert_case::{Case, Casing};
7use proc_macro2::TokenStream;
8use quote::quote;
9
10use crate::codegen::Runtime;
11use crate::utils::{extract_simple_type_name, last_segment};
12
13#[derive(Debug, Clone, Copy)]
15pub enum RenderContext {
16 Constructor,
18 Extractor,
21 QueryExtractor,
24 Parameter,
26 ReturnType,
28 FieldType,
30 BuilderMethod,
32 PythonParameter,
34 NapiParameter,
36}
37
38pub trait TypeRenderer {
43 fn render(&self, ty: &UnifiedType) -> String;
44}
45
46pub struct RustRenderer(pub RenderContext, pub Runtime);
48
49impl TypeRenderer for RustRenderer {
50 fn render(&self, ty: &UnifiedType) -> String {
51 unified_to_rust(ty, self.0, self.1)
52 }
53}
54
55pub struct NapiRenderer;
57
58impl TypeRenderer for NapiRenderer {
59 fn render(&self, ty: &UnifiedType) -> String {
60 unified_to_napi(ty)
61 }
62}
63
64pub struct TypeScriptRenderer;
66
67impl TypeRenderer for TypeScriptRenderer {
68 fn render(&self, ty: &UnifiedType) -> String {
69 unified_to_typescript(ty)
70 }
71}
72
73pub fn unified_to_rust(
81 unified_type: &UnifiedType,
82 context: RenderContext,
83 runtime: Runtime,
84) -> String {
85 let base_type_str = match &unified_type.base_type {
86 BaseType::String => {
87 if matches!(context, RenderContext::Constructor)
89 && !unified_type.is_optional
90 && !unified_type.is_repeated
91 {
92 "impl Into<String>".to_string()
93 } else {
94 "String".to_string()
95 }
96 }
97 BaseType::Int32 => "i32".to_string(),
98 BaseType::Int64 => "i64".to_string(),
99 BaseType::Bool => "bool".to_string(),
100 BaseType::Float64 => "f64".to_string(),
101 BaseType::Float32 => "f32".to_string(),
102 BaseType::Bytes => "Vec<u8>".to_string(),
103 BaseType::Unit => "()".to_string(),
104 BaseType::Message(name) => match JsonType::of(name) {
105 Some(json) => json.rust_path(runtime).to_string(),
106 None => extract_simple_type_name(name),
107 },
108 BaseType::Enum(name) => {
109 if matches!(
110 context,
111 RenderContext::Extractor | RenderContext::NapiParameter
112 ) {
113 "i32".to_string()
114 } else {
115 convert_protobuf_enum_to_rust_type(name)
116 }
117 }
118 BaseType::OneOf(name) => extract_simple_type_name(name),
119 BaseType::Map(key_type, value_type) => {
120 if matches!(context, RenderContext::Constructor) && !unified_type.is_optional {
121 let value = match value_type.base_type {
122 BaseType::String => "impl Into<String>".to_string(),
123 _ => unified_to_rust(value_type, RenderContext::Parameter, runtime),
124 };
125 format!("impl IntoIterator<Item = (impl Into<String>, {value})>")
126 } else {
127 let key_str = unified_to_rust(key_type, context, runtime);
128 let value_str = unified_to_rust(value_type, context, runtime);
129 format!("HashMap<{key_str}, {value_str}>")
130 }
131 }
132 };
133
134 let mut result = base_type_str;
135
136 if unified_type.is_repeated && !matches!(context, RenderContext::BuilderMethod) {
138 result = format!("Vec<{result}>");
139 }
140
141 if should_wrap_in_option(context, unified_type) {
142 result = format!("Option<{result}>");
143 }
144
145 result
146}
147
148fn should_wrap_in_option(ctx: RenderContext, ty: &UnifiedType) -> bool {
160 let is_optional_non_builder = ty.is_optional && !matches!(ctx, RenderContext::BuilderMethod);
161 let is_ffi = matches!(
162 ctx,
163 RenderContext::PythonParameter | RenderContext::NapiParameter
164 );
165 let is_ffi_collection =
170 is_ffi && (matches!(ty.base_type, BaseType::Map(_, _)) || ty.is_repeated);
171 is_optional_non_builder || is_ffi_collection
172}
173
174pub fn field_assignment(
184 unified_type: &UnifiedType,
185 field_ident: &proc_macro2::Ident,
186 ctx: &RenderContext,
187 runtime: Runtime,
188) -> TokenStream {
189 if matches!(ctx, RenderContext::BuilderMethod) {
190 return flexible_optional_field_assignment(unified_type, field_ident, runtime);
191 }
192 match &unified_type.base_type {
193 BaseType::String if !unified_type.is_optional && !unified_type.is_repeated => {
195 quote! { #field_ident.into() }
196 }
197 BaseType::Enum(_) => match runtime {
198 Runtime::Prost if unified_type.is_repeated => {
199 quote! { #field_ident.into_iter().map(|v| v as i32).collect() }
200 }
201 Runtime::Prost => quote! { #field_ident as i32 },
202 Runtime::Buffa if unified_type.is_repeated => {
203 quote! { #field_ident.into_iter().map(buffa::EnumValue::Known).collect() }
204 }
205 Runtime::Buffa => quote! { buffa::EnumValue::Known(#field_ident) },
206 },
207 BaseType::Map(_, value)
209 if matches!(ctx, RenderContext::Constructor)
210 && !matches!(value.base_type, BaseType::String) =>
211 {
212 quote! { #field_ident.into_iter().map(|(k, v)| (k.into(), v)).collect() }
213 }
214 BaseType::Map(_, _) => quote! {
215 #field_ident.into_iter().map(|(k, v)| (k.into(), v.into())).collect()
216 },
217 BaseType::OneOf(_) if !unified_type.is_repeated => {
222 quote! { Some(#field_ident) }
223 }
224 BaseType::Message(_) if !unified_type.is_repeated => match runtime {
225 Runtime::Prost => quote! { Some(#field_ident) },
227 Runtime::Buffa => quote! { buffa::MessageField::some(#field_ident) },
229 },
230 _ => quote! { #field_ident },
231 }
232}
233
234pub fn unified_to_napi(unified_type: &UnifiedType) -> String {
239 let base_type_str = match &unified_type.base_type {
240 BaseType::String => "String".to_string(),
241 BaseType::Int32 => "i32".to_string(),
242 BaseType::Int64 => "i64".to_string(),
243 BaseType::Bool => "bool".to_string(),
244 BaseType::Float64 => "f64".to_string(),
245 BaseType::Float32 => "f32".to_string(),
246 BaseType::Bytes => "Vec<u8>".to_string(),
247 BaseType::Unit => "()".to_string(),
248 BaseType::Message(name) => extract_simple_type_name(name),
249 BaseType::Enum(name) => convert_protobuf_enum_to_rust_type(name),
250 BaseType::OneOf(name) => extract_simple_type_name(name),
251 BaseType::Map(key_type, value_type) => {
252 let key_str = unified_to_napi(key_type);
253 let value_str = unified_to_napi(value_type);
254 format!("HashMap<{key_str}, {value_str}>")
255 }
256 };
257
258 let mut result = base_type_str;
259
260 if unified_type.is_repeated {
261 result = format!("Vec<{result}>");
262 }
263
264 if unified_type.is_optional
265 || matches!(unified_type.base_type, BaseType::Map(_, _))
266 || unified_type.is_repeated
267 {
268 result = format!("Option<{result}>");
269 }
270
271 result
272}
273
274pub fn unified_to_typescript(unified_type: &UnifiedType) -> String {
278 let base_type_str = match &unified_type.base_type {
279 BaseType::String => "string".to_string(),
280 BaseType::Int32 | BaseType::Int64 => "number".to_string(),
281 BaseType::Bool => "boolean".to_string(),
282 BaseType::Float64 | BaseType::Float32 => "number".to_string(),
283 BaseType::Bytes => "Uint8Array".to_string(),
284 BaseType::Unit => "void".to_string(),
285 BaseType::Message(name) => extract_simple_type_name(name),
286 BaseType::Enum(_) => "number".to_string(),
287 BaseType::OneOf(name) => extract_simple_type_name(name),
288 BaseType::Map(key_type, value_type) => {
289 let key_str = unified_to_typescript(key_type);
290 let value_str = unified_to_typescript(value_type);
291 format!("Record<{key_str}, {value_str}>")
292 }
293 };
294
295 let mut result = base_type_str;
296
297 if unified_type.is_repeated {
298 result = format!("{result}[]");
299 }
300
301 if unified_type.is_optional {
302 result = format!("{result} | undefined");
303 }
304
305 result
306}
307
308fn flexible_optional_field_assignment(
309 unified_type: &UnifiedType,
310 field_ident: &proc_macro2::Ident,
311 runtime: Runtime,
312) -> TokenStream {
313 if unified_type.is_optional {
314 match (&unified_type.base_type, runtime) {
315 (BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident.into().map(|e| e as i32) },
316 (BaseType::Enum(_), Runtime::Buffa) => {
317 quote! { #field_ident.into().map(buffa::EnumValue::Known) }
318 }
319 (BaseType::Message(_) | BaseType::OneOf(_), Runtime::Buffa) => quote! {
324 {
325 let #field_ident: ::core::option::Option<_> = #field_ident.into();
326 buffa::MessageField::from(#field_ident)
327 }
328 },
329 _ => quote! { #field_ident.into() },
330 }
331 } else {
332 match (&unified_type.base_type, runtime) {
333 (
334 BaseType::String
335 | BaseType::Int32
336 | BaseType::Int64
337 | BaseType::Bool
338 | BaseType::Float64
339 | BaseType::Float32,
340 _,
341 ) => quote! { #field_ident.into() },
342 (BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident as i32 },
343 (BaseType::Enum(_), Runtime::Buffa) => quote! { buffa::EnumValue::Known(#field_ident) },
344 _ => quote! { #field_ident },
347 }
348 }
349}
350
351pub(crate) fn convert_protobuf_enum_to_rust_type(enum_name: &str) -> String {
366 let enum_name = enum_name.trim_start_matches('.');
367 let Some((parent_part, enum_simple_name)) = enum_name.rsplit_once('.') else {
368 return convert_enum_name_to_rust(enum_name);
369 };
370 let enum_rust_name = convert_enum_name_to_rust(enum_simple_name);
371
372 let parent = last_segment(parent_part);
375 let is_nested = parent.starts_with(char::is_uppercase)
376 && parent != "V1"
377 && !parent.chars().all(|c| c.is_lowercase() || c.is_numeric());
378 if is_nested {
379 format!("{}::{enum_rust_name}", parent.to_case(Case::Snake))
381 } else {
382 enum_rust_name
383 }
384}
385
386fn convert_enum_name_to_rust(enum_name: &str) -> String {
388 if enum_name.contains('_') && enum_name.chars().all(|c| c.is_uppercase() || c == '_') {
390 enum_name.to_case(Case::Pascal)
391 } else {
392 enum_name.to_string()
393 }
394}
395
396#[derive(Debug, Clone)]
404pub struct UnifiedType {
405 pub base_type: BaseType,
407 pub is_optional: bool,
409 pub is_repeated: bool,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum JsonType {
417 Value,
419 Struct,
421 ListValue,
423}
424
425impl JsonType {
426 pub fn of(name: &str) -> Option<Self> {
428 match name.trim_start_matches('.') {
429 "google.protobuf.Value" => Some(Self::Value),
430 "google.protobuf.Struct" => Some(Self::Struct),
431 "google.protobuf.ListValue" => Some(Self::ListValue),
432 _ => None,
433 }
434 }
435
436 pub fn of_field(ty: &UnifiedType) -> Option<Self> {
438 match &ty.base_type {
439 BaseType::Message(name) => Self::of(name),
440 BaseType::Map(_, value) => Self::of_field(value),
441 _ => None,
442 }
443 }
444
445 pub fn rust_path(self, runtime: Runtime) -> &'static str {
447 match (runtime, self) {
448 (Runtime::Buffa, Self::Value) => "::buffa_types::google::protobuf::Value",
449 (Runtime::Buffa, Self::Struct) => "::buffa_types::google::protobuf::Struct",
450 (Runtime::Buffa, Self::ListValue) => "::buffa_types::google::protobuf::ListValue",
451 (Runtime::Prost, Self::Value) => "::prost_types::Value",
452 (Runtime::Prost, Self::Struct) => "::prost_types::Struct",
453 (Runtime::Prost, Self::ListValue) => "::prost_types::ListValue",
454 }
455 }
456
457 pub fn fn_suffix(self) -> &'static str {
459 match self {
460 Self::Value => "value",
461 Self::Struct => "struct",
462 Self::ListValue => "list",
463 }
464 }
465}
466
467#[derive(Debug, Clone)]
473pub enum BaseType {
474 String,
476 Int32,
478 Int64,
480 Bool,
482 Float64,
484 Float32,
486 Bytes,
488 Message(String),
490 Enum(String),
492 OneOf(String),
494 Map(Box<UnifiedType>, Box<UnifiedType>),
496 Unit,
498}
499
500impl UnifiedType {
501 pub fn type_ident(&self) -> syn::Ident {
503 let name = match &self.base_type {
504 BaseType::Message(n) | BaseType::Enum(n) | BaseType::OneOf(n) => last_segment(n),
505 BaseType::String => "String",
506 BaseType::Int32 => "i32",
507 BaseType::Int64 => "i64",
508 BaseType::Bool => "bool",
509 BaseType::Float64 => "f64",
510 BaseType::Float32 => "f32",
511 BaseType::Bytes => "Bytes",
512 BaseType::Unit => "()",
513 BaseType::Map(_, _) => "HashMap",
514 };
515 quote::format_ident!("{}", name)
516 }
517
518 pub fn is_singular_bytes(&self) -> bool {
520 matches!(self.base_type, BaseType::Bytes) && !self.is_repeated
521 }
522
523 pub fn string() -> Self {
525 Self {
526 base_type: BaseType::String,
527 is_optional: false,
528 is_repeated: false,
529 }
530 }
531
532 pub fn optional(mut self) -> Self {
534 self.is_optional = true;
535 self
536 }
537
538 pub fn repeated(mut self) -> Self {
540 self.is_repeated = true;
541 self
542 }
543
544 pub fn map(key: UnifiedType, value: UnifiedType) -> Self {
546 Self {
547 base_type: BaseType::Map(Box::new(key), Box::new(value)),
548 is_optional: false,
549 is_repeated: false,
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn json_types_render_as_the_runtime_well_known_type() {
560 let structs = UnifiedType::map(
561 UnifiedType::string(),
562 UnifiedType {
563 base_type: BaseType::Message(".google.protobuf.Struct".to_string()),
564 is_optional: false,
565 is_repeated: false,
566 },
567 );
568 assert_eq!(
569 unified_to_rust(&structs, RenderContext::Parameter, Runtime::Prost),
570 "HashMap<String, ::prost_types::Struct>"
571 );
572 assert_eq!(
573 unified_to_rust(&structs, RenderContext::Parameter, Runtime::Buffa),
574 "HashMap<String, ::buffa_types::google::protobuf::Struct>"
575 );
576 }
577}