1use convert_case::{Case, Casing};
7use proc_macro2::TokenStream;
8use quote::quote;
9
10use crate::codegen::Runtime;
11use crate::utils::extract_simple_type_name;
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);
48
49impl TypeRenderer for RustRenderer {
50 fn render(&self, ty: &UnifiedType) -> String {
51 unified_to_rust(ty, self.0)
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(unified_type: &UnifiedType, context: RenderContext) -> String {
82 let base_type_str = match &unified_type.base_type {
83 BaseType::String => {
84 if matches!(context, RenderContext::Constructor)
86 && !unified_type.is_optional
87 && !unified_type.is_repeated
88 {
89 "impl Into<String>".to_string()
90 } else {
91 "String".to_string()
92 }
93 }
94 BaseType::Int32 => "i32".to_string(),
95 BaseType::Int64 => "i64".to_string(),
96 BaseType::Bool => "bool".to_string(),
97 BaseType::Float64 => "f64".to_string(),
98 BaseType::Float32 => "f32".to_string(),
99 BaseType::Bytes => "Vec<u8>".to_string(),
100 BaseType::Unit => "()".to_string(),
101 BaseType::Message(name) => match JsonType::of(name) {
102 Some(json) => json.rust_path().to_string(),
103 None => extract_simple_type_name(name),
104 },
105 BaseType::Enum(name) => {
106 if matches!(
107 context,
108 RenderContext::Extractor | RenderContext::NapiParameter
109 ) {
110 "i32".to_string()
111 } else {
112 convert_protobuf_enum_to_rust_type(&format!("TYPE_ENUM:{name}"))
113 }
114 }
115 BaseType::OneOf(name) => extract_simple_type_name(name),
116 BaseType::Map(key_type, value_type) => {
117 if matches!(context, RenderContext::Constructor) && !unified_type.is_optional {
118 let value = match value_type.base_type {
119 BaseType::String => "impl Into<String>".to_string(),
120 _ => unified_to_rust(value_type, RenderContext::Parameter),
121 };
122 format!("impl IntoIterator<Item = (impl Into<String>, {value})>")
123 } else {
124 let key_str = unified_to_rust(key_type, context);
125 let value_str = unified_to_rust(value_type, context);
126 format!("HashMap<{key_str}, {value_str}>")
127 }
128 }
129 };
130
131 let mut result = base_type_str;
132
133 if unified_type.is_repeated && !matches!(context, RenderContext::BuilderMethod) {
135 result = format!("Vec<{result}>");
136 }
137
138 if should_wrap_in_option(context, unified_type) {
139 result = format!("Option<{result}>");
140 }
141
142 result
143}
144
145fn should_wrap_in_option(ctx: RenderContext, ty: &UnifiedType) -> bool {
157 let is_optional_non_builder = ty.is_optional && !matches!(ctx, RenderContext::BuilderMethod);
158 let is_ffi = matches!(
159 ctx,
160 RenderContext::PythonParameter | RenderContext::NapiParameter
161 );
162 let is_ffi_collection =
167 is_ffi && (matches!(ty.base_type, BaseType::Map(_, _)) || ty.is_repeated);
168 is_optional_non_builder || is_ffi_collection
169}
170
171pub fn field_assignment(
181 unified_type: &UnifiedType,
182 field_ident: &proc_macro2::Ident,
183 ctx: &RenderContext,
184 runtime: Runtime,
185) -> TokenStream {
186 if matches!(ctx, RenderContext::BuilderMethod) {
187 return flexible_optional_field_assignment(unified_type, field_ident, runtime);
188 }
189 match &unified_type.base_type {
190 BaseType::String if !unified_type.is_optional && !unified_type.is_repeated => {
192 quote! { #field_ident.into() }
193 }
194 BaseType::Enum(_) => match runtime {
195 Runtime::Prost if unified_type.is_repeated => {
196 quote! { #field_ident.into_iter().map(|v| v as i32).collect() }
197 }
198 Runtime::Prost => quote! { #field_ident as i32 },
199 Runtime::Buffa if unified_type.is_repeated => {
200 quote! { #field_ident.into_iter().map(buffa::EnumValue::Known).collect() }
201 }
202 Runtime::Buffa => quote! { buffa::EnumValue::Known(#field_ident) },
203 },
204 BaseType::Map(_, value)
206 if matches!(ctx, RenderContext::Constructor)
207 && !matches!(value.base_type, BaseType::String) =>
208 {
209 quote! { #field_ident.into_iter().map(|(k, v)| (k.into(), v)).collect() }
210 }
211 BaseType::Map(_, _) => quote! {
212 #field_ident.into_iter().map(|(k, v)| (k.into(), v.into())).collect()
213 },
214 BaseType::OneOf(_) if !unified_type.is_repeated => {
219 quote! { Some(#field_ident) }
220 }
221 BaseType::Message(_) if !unified_type.is_repeated => match runtime {
222 Runtime::Prost => quote! { Some(#field_ident) },
224 Runtime::Buffa => quote! { buffa::MessageField::some(#field_ident) },
226 },
227 _ => quote! { #field_ident },
228 }
229}
230
231pub fn unified_to_napi(unified_type: &UnifiedType) -> String {
236 let base_type_str = match &unified_type.base_type {
237 BaseType::String => "String".to_string(),
238 BaseType::Int32 => "i32".to_string(),
239 BaseType::Int64 => "i64".to_string(),
240 BaseType::Bool => "bool".to_string(),
241 BaseType::Float64 => "f64".to_string(),
242 BaseType::Float32 => "f32".to_string(),
243 BaseType::Bytes => "Vec<u8>".to_string(),
244 BaseType::Unit => "()".to_string(),
245 BaseType::Message(name) => extract_simple_type_name(name),
246 BaseType::Enum(name) => convert_protobuf_enum_to_rust_type(&format!("TYPE_ENUM:{name}")),
247 BaseType::OneOf(name) => extract_simple_type_name(name),
248 BaseType::Map(key_type, value_type) => {
249 let key_str = unified_to_napi(key_type);
250 let value_str = unified_to_napi(value_type);
251 format!("HashMap<{key_str}, {value_str}>")
252 }
253 };
254
255 let mut result = base_type_str;
256
257 if unified_type.is_repeated {
258 result = format!("Vec<{result}>");
259 }
260
261 if unified_type.is_optional
262 || matches!(unified_type.base_type, BaseType::Map(_, _))
263 || unified_type.is_repeated
264 {
265 result = format!("Option<{result}>");
266 }
267
268 result
269}
270
271pub fn unified_to_typescript(unified_type: &UnifiedType) -> String {
275 let base_type_str = match &unified_type.base_type {
276 BaseType::String => "string".to_string(),
277 BaseType::Int32 | BaseType::Int64 => "number".to_string(),
278 BaseType::Bool => "boolean".to_string(),
279 BaseType::Float64 | BaseType::Float32 => "number".to_string(),
280 BaseType::Bytes => "Uint8Array".to_string(),
281 BaseType::Unit => "void".to_string(),
282 BaseType::Message(name) => extract_simple_type_name(name),
283 BaseType::Enum(_) => "number".to_string(),
284 BaseType::OneOf(name) => extract_simple_type_name(name),
285 BaseType::Map(key_type, value_type) => {
286 let key_str = unified_to_typescript(key_type);
287 let value_str = unified_to_typescript(value_type);
288 format!("Record<{key_str}, {value_str}>")
289 }
290 };
291
292 let mut result = base_type_str;
293
294 if unified_type.is_repeated {
295 result = format!("{result}[]");
296 }
297
298 if unified_type.is_optional {
299 result = format!("{result} | undefined");
300 }
301
302 result
303}
304
305fn flexible_optional_field_assignment(
306 unified_type: &UnifiedType,
307 field_ident: &proc_macro2::Ident,
308 runtime: Runtime,
309) -> TokenStream {
310 if unified_type.is_optional {
311 match (&unified_type.base_type, runtime) {
312 (BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident.into().map(|e| e as i32) },
313 (BaseType::Enum(_), Runtime::Buffa) => {
314 quote! { #field_ident.into().map(buffa::EnumValue::Known) }
315 }
316 (BaseType::Message(_) | BaseType::OneOf(_), Runtime::Buffa) => quote! {
321 {
322 let #field_ident: ::core::option::Option<_> = #field_ident.into();
323 buffa::MessageField::from(#field_ident)
324 }
325 },
326 _ => quote! { #field_ident.into() },
327 }
328 } else {
329 match (&unified_type.base_type, runtime) {
330 (BaseType::String, _) => quote! { #field_ident.into() },
331 (
332 BaseType::Int32
333 | BaseType::Int64
334 | BaseType::Bool
335 | BaseType::Float64
336 | BaseType::Float32,
337 _,
338 ) => {
339 quote! { #field_ident.into() }
340 }
341 (BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident as i32 },
342 (BaseType::Enum(_), Runtime::Buffa) => quote! { buffa::EnumValue::Known(#field_ident) },
343 _ => quote! { #field_ident },
346 }
347 }
348}
349
350pub(crate) fn convert_protobuf_enum_to_rust_type(proto_type: &str) -> String {
365 if let Some(enum_name) = proto_type.strip_prefix("TYPE_ENUM:") {
366 let enum_name = enum_name.trim_start_matches('.');
368
369 if let Some(last_dot) = enum_name.rfind('.') {
372 let parent_part = &enum_name[..last_dot];
373 let enum_simple_name = &enum_name[last_dot + 1..];
374
375 let parent_parts: Vec<&str> = parent_part.split('.').collect();
378 if let Some(last_part) = parent_parts.last() {
379 if last_part.chars().next().is_some_and(|c| c.is_uppercase())
382 && *last_part != "V1"
383 && !last_part
384 .chars()
385 .all(|c| c.is_lowercase() || c.is_numeric())
386 {
387 let snake_case_module = last_part.to_case(Case::Snake);
389
390 let enum_rust_name = if enum_simple_name.contains('_')
392 && enum_simple_name
393 .chars()
394 .all(|c| c.is_uppercase() || c == '_')
395 {
396 enum_simple_name.to_case(Case::Pascal)
397 } else {
398 enum_simple_name.to_string()
399 };
400
401 format!("{snake_case_module}::{enum_rust_name}")
402 } else {
403 convert_enum_name_to_rust(enum_simple_name)
405 }
406 } else {
407 convert_enum_name_to_rust(enum_simple_name)
409 }
410 } else {
411 convert_enum_name_to_rust(enum_name)
413 }
414 } else {
415 "i32".to_string() }
417}
418
419fn convert_enum_name_to_rust(enum_name: &str) -> String {
421 if enum_name.contains('_') && enum_name.chars().all(|c| c.is_uppercase() || c == '_') {
423 enum_name.to_case(Case::Pascal)
424 } else {
425 enum_name.to_string()
426 }
427}
428
429#[derive(Debug, Clone)]
437pub struct UnifiedType {
438 pub base_type: BaseType,
440 pub is_optional: bool,
442 pub is_repeated: bool,
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum JsonType {
450 Value,
452 Struct,
454 ListValue,
456}
457
458impl JsonType {
459 pub fn of(name: &str) -> Option<Self> {
461 match name.trim_start_matches('.') {
462 "google.protobuf.Value" => Some(Self::Value),
463 "google.protobuf.Struct" => Some(Self::Struct),
464 "google.protobuf.ListValue" => Some(Self::ListValue),
465 _ => None,
466 }
467 }
468
469 pub fn of_field(ty: &UnifiedType) -> Option<Self> {
471 match &ty.base_type {
472 BaseType::Message(name) => Self::of(name),
473 BaseType::Map(_, value) => Self::of_field(value),
474 _ => None,
475 }
476 }
477
478 pub fn rust_path(self) -> &'static str {
480 match self {
481 Self::Value => "::buffa_types::google::protobuf::Value",
482 Self::Struct => "::buffa_types::google::protobuf::Struct",
483 Self::ListValue => "::buffa_types::google::protobuf::ListValue",
484 }
485 }
486
487 pub fn fn_suffix(self) -> &'static str {
489 match self {
490 Self::Value => "value",
491 Self::Struct => "struct",
492 Self::ListValue => "list",
493 }
494 }
495}
496
497#[derive(Debug, Clone)]
503pub enum BaseType {
504 String,
506 Int32,
508 Int64,
510 Bool,
512 Float64,
514 Float32,
516 Bytes,
518 Message(String),
520 Enum(String),
522 OneOf(String),
524 Map(Box<UnifiedType>, Box<UnifiedType>),
526 Unit,
528}
529
530impl UnifiedType {
531 pub fn type_ident(&self) -> syn::Ident {
533 let name = match &self.base_type {
534 BaseType::Message(n) | BaseType::Enum(n) | BaseType::OneOf(n) => {
535 n.split('.').next_back().unwrap_or(n)
536 }
537 BaseType::String => "String",
538 BaseType::Int32 => "i32",
539 BaseType::Int64 => "i64",
540 BaseType::Bool => "bool",
541 BaseType::Float64 => "f64",
542 BaseType::Float32 => "f32",
543 BaseType::Bytes => "Bytes",
544 BaseType::Unit => "()",
545 BaseType::Map(_, _) => "HashMap",
546 };
547 quote::format_ident!("{}", name)
548 }
549
550 pub fn is_singular_bytes(&self) -> bool {
552 matches!(self.base_type, BaseType::Bytes) && !self.is_repeated
553 }
554
555 pub fn string() -> Self {
557 Self {
558 base_type: BaseType::String,
559 is_optional: false,
560 is_repeated: false,
561 }
562 }
563
564 pub fn optional(mut self) -> Self {
566 self.is_optional = true;
567 self
568 }
569
570 pub fn repeated(mut self) -> Self {
572 self.is_repeated = true;
573 self
574 }
575
576 pub fn map(key: UnifiedType, value: UnifiedType) -> Self {
578 Self {
579 base_type: BaseType::Map(Box::new(key), Box::new(value)),
580 is_optional: false,
581 is_repeated: false,
582 }
583 }
584}