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 fn unified_to_rust(
46 unified_type: &UnifiedType,
47 context: RenderContext,
48 runtime: Runtime,
49) -> String {
50 let base_type_str = match &unified_type.base_type {
51 BaseType::String => {
52 if matches!(context, RenderContext::Constructor)
54 && !unified_type.is_optional
55 && !unified_type.is_repeated
56 {
57 "impl Into<String>".to_string()
58 } else {
59 "String".to_string()
60 }
61 }
62 BaseType::Int32 => "i32".to_string(),
63 BaseType::Int64 => "i64".to_string(),
64 BaseType::Bool => "bool".to_string(),
65 BaseType::Float64 => "f64".to_string(),
66 BaseType::Float32 => "f32".to_string(),
67 BaseType::Bytes => "Vec<u8>".to_string(),
68 BaseType::Unit => "()".to_string(),
69 BaseType::Message(name) => match JsonType::of(name) {
70 Some(json) => json.rust_path(runtime).to_string(),
71 None => extract_simple_type_name(name),
72 },
73 BaseType::Enum(name) => {
74 if matches!(
75 context,
76 RenderContext::Extractor | RenderContext::NapiParameter
77 ) {
78 "i32".to_string()
79 } else {
80 convert_protobuf_enum_to_rust_type(name)
81 }
82 }
83 BaseType::OneOf(name) => extract_simple_type_name(name),
84 BaseType::Map(key_type, value_type) => {
85 if matches!(context, RenderContext::Constructor) && !unified_type.is_optional {
86 let value = match value_type.base_type {
87 BaseType::String => "impl Into<String>".to_string(),
88 _ => unified_to_rust(value_type, RenderContext::Parameter, runtime),
89 };
90 format!("impl IntoIterator<Item = (impl Into<String>, {value})>")
91 } else {
92 let key_str = unified_to_rust(key_type, context, runtime);
93 let value_str = unified_to_rust(value_type, context, runtime);
94 format!("HashMap<{key_str}, {value_str}>")
95 }
96 }
97 };
98
99 let mut result = base_type_str;
100
101 if unified_type.is_repeated && !matches!(context, RenderContext::BuilderMethod) {
103 result = format!("Vec<{result}>");
104 }
105
106 if should_wrap_in_option(context, unified_type) {
107 result = format!("Option<{result}>");
108 }
109
110 result
111}
112
113fn should_wrap_in_option(ctx: RenderContext, ty: &UnifiedType) -> bool {
125 let is_optional_non_builder = ty.is_optional && !matches!(ctx, RenderContext::BuilderMethod);
126 let is_ffi = matches!(
127 ctx,
128 RenderContext::PythonParameter | RenderContext::NapiParameter
129 );
130 let is_ffi_collection =
135 is_ffi && (matches!(ty.base_type, BaseType::Map(_, _)) || ty.is_repeated);
136 is_optional_non_builder || is_ffi_collection
137}
138
139pub fn field_assignment(
149 unified_type: &UnifiedType,
150 field_ident: &proc_macro2::Ident,
151 ctx: &RenderContext,
152 runtime: Runtime,
153) -> TokenStream {
154 if matches!(ctx, RenderContext::BuilderMethod) {
155 return flexible_optional_field_assignment(unified_type, field_ident, runtime);
156 }
157 match &unified_type.base_type {
158 BaseType::String if !unified_type.is_optional && !unified_type.is_repeated => {
160 quote! { #field_ident.into() }
161 }
162 BaseType::Enum(_) => match runtime {
163 Runtime::Prost if unified_type.is_repeated => {
164 quote! { #field_ident.into_iter().map(|v| v as i32).collect() }
165 }
166 Runtime::Prost => quote! { #field_ident as i32 },
167 Runtime::Buffa if unified_type.is_repeated => {
168 quote! { #field_ident.into_iter().map(buffa::EnumValue::Known).collect() }
169 }
170 Runtime::Buffa => quote! { buffa::EnumValue::Known(#field_ident) },
171 },
172 BaseType::Map(_, value)
174 if matches!(ctx, RenderContext::Constructor)
175 && !matches!(value.base_type, BaseType::String) =>
176 {
177 quote! { #field_ident.into_iter().map(|(k, v)| (k.into(), v)).collect() }
178 }
179 BaseType::Map(_, _) => quote! {
180 #field_ident.into_iter().map(|(k, v)| (k.into(), v.into())).collect()
181 },
182 BaseType::OneOf(_) if !unified_type.is_repeated => {
187 quote! { Some(#field_ident) }
188 }
189 BaseType::Message(_) if !unified_type.is_repeated => match runtime {
190 Runtime::Prost => quote! { Some(#field_ident) },
192 Runtime::Buffa => quote! { buffa::MessageField::some(#field_ident) },
194 },
195 _ => quote! { #field_ident },
196 }
197}
198
199pub fn unified_to_napi(unified_type: &UnifiedType) -> String {
204 let base_type_str = match &unified_type.base_type {
205 BaseType::String => "String".to_string(),
206 BaseType::Int32 => "i32".to_string(),
207 BaseType::Int64 => "i64".to_string(),
208 BaseType::Bool => "bool".to_string(),
209 BaseType::Float64 => "f64".to_string(),
210 BaseType::Float32 => "f32".to_string(),
211 BaseType::Bytes => "Vec<u8>".to_string(),
212 BaseType::Unit => "()".to_string(),
213 BaseType::Message(name) => extract_simple_type_name(name),
214 BaseType::Enum(name) => convert_protobuf_enum_to_rust_type(name),
215 BaseType::OneOf(name) => extract_simple_type_name(name),
216 BaseType::Map(key_type, value_type) => {
217 let key_str = unified_to_napi(key_type);
218 let value_str = unified_to_napi(value_type);
219 format!("HashMap<{key_str}, {value_str}>")
220 }
221 };
222
223 let mut result = base_type_str;
224
225 if unified_type.is_repeated {
226 result = format!("Vec<{result}>");
227 }
228
229 if unified_type.is_optional
230 || matches!(unified_type.base_type, BaseType::Map(_, _))
231 || unified_type.is_repeated
232 {
233 result = format!("Option<{result}>");
234 }
235
236 result
237}
238
239pub fn unified_to_typescript(unified_type: &UnifiedType) -> String {
243 let base_type_str = match &unified_type.base_type {
244 BaseType::String => "string".to_string(),
245 BaseType::Int32 | BaseType::Int64 => "number".to_string(),
246 BaseType::Bool => "boolean".to_string(),
247 BaseType::Float64 | BaseType::Float32 => "number".to_string(),
248 BaseType::Bytes => "Uint8Array".to_string(),
249 BaseType::Unit => "void".to_string(),
250 BaseType::Message(name) => extract_simple_type_name(name),
251 BaseType::Enum(_) => "number".to_string(),
252 BaseType::OneOf(name) => extract_simple_type_name(name),
253 BaseType::Map(key_type, value_type) => {
254 let key_str = unified_to_typescript(key_type);
255 let value_str = unified_to_typescript(value_type);
256 format!("Record<{key_str}, {value_str}>")
257 }
258 };
259
260 let mut result = base_type_str;
261
262 if unified_type.is_repeated {
263 result = format!("{result}[]");
264 }
265
266 if unified_type.is_optional {
267 result = format!("{result} | undefined");
268 }
269
270 result
271}
272
273fn flexible_optional_field_assignment(
274 unified_type: &UnifiedType,
275 field_ident: &proc_macro2::Ident,
276 runtime: Runtime,
277) -> TokenStream {
278 if unified_type.is_optional {
279 match (&unified_type.base_type, runtime) {
280 (BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident.into().map(|e| e as i32) },
281 (BaseType::Enum(_), Runtime::Buffa) => {
282 quote! { #field_ident.into().map(buffa::EnumValue::Known) }
283 }
284 (BaseType::Message(_) | BaseType::OneOf(_), Runtime::Buffa) => quote! {
289 {
290 let #field_ident: ::core::option::Option<_> = #field_ident.into();
291 buffa::MessageField::from(#field_ident)
292 }
293 },
294 _ => quote! { #field_ident.into() },
295 }
296 } else {
297 match (&unified_type.base_type, runtime) {
298 (
299 BaseType::String
300 | BaseType::Int32
301 | BaseType::Int64
302 | BaseType::Bool
303 | BaseType::Float64
304 | BaseType::Float32,
305 _,
306 ) => quote! { #field_ident.into() },
307 (BaseType::Enum(_), Runtime::Prost) => quote! { #field_ident as i32 },
308 (BaseType::Enum(_), Runtime::Buffa) => quote! { buffa::EnumValue::Known(#field_ident) },
309 _ => quote! { #field_ident },
312 }
313 }
314}
315
316pub(crate) fn convert_protobuf_enum_to_rust_type(enum_name: &str) -> String {
331 let enum_name = enum_name.trim_start_matches('.');
332 let Some((parent_part, enum_simple_name)) = enum_name.rsplit_once('.') else {
333 return convert_enum_name_to_rust(enum_name);
334 };
335 let enum_rust_name = convert_enum_name_to_rust(enum_simple_name);
336
337 let parent = last_segment(parent_part);
340 let is_nested = parent.starts_with(char::is_uppercase)
341 && parent != "V1"
342 && !parent.chars().all(|c| c.is_lowercase() || c.is_numeric());
343 if is_nested {
344 format!("{}::{enum_rust_name}", parent.to_case(Case::Snake))
346 } else {
347 enum_rust_name
348 }
349}
350
351fn convert_enum_name_to_rust(enum_name: &str) -> String {
353 if enum_name.contains('_') && enum_name.chars().all(|c| c.is_uppercase() || c == '_') {
355 enum_name.to_case(Case::Pascal)
356 } else {
357 enum_name.to_string()
358 }
359}
360
361#[derive(Debug, Clone)]
369pub struct UnifiedType {
370 pub base_type: BaseType,
372 pub is_optional: bool,
374 pub is_repeated: bool,
376}
377
378#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub enum JsonType {
382 Value,
384 Struct,
386 ListValue,
388}
389
390impl JsonType {
391 pub fn of(name: &str) -> Option<Self> {
393 match name.trim_start_matches('.') {
394 "google.protobuf.Value" => Some(Self::Value),
395 "google.protobuf.Struct" => Some(Self::Struct),
396 "google.protobuf.ListValue" => Some(Self::ListValue),
397 _ => None,
398 }
399 }
400
401 pub fn of_field(ty: &UnifiedType) -> Option<Self> {
403 match &ty.base_type {
404 BaseType::Message(name) => Self::of(name),
405 BaseType::Map(_, value) => Self::of_field(value),
406 _ => None,
407 }
408 }
409
410 pub fn rust_path(self, runtime: Runtime) -> &'static str {
412 match (runtime, self) {
413 (Runtime::Buffa, Self::Value) => "::buffa_types::google::protobuf::Value",
414 (Runtime::Buffa, Self::Struct) => "::buffa_types::google::protobuf::Struct",
415 (Runtime::Buffa, Self::ListValue) => "::buffa_types::google::protobuf::ListValue",
416 (Runtime::Prost, Self::Value) => "::prost_types::Value",
417 (Runtime::Prost, Self::Struct) => "::prost_types::Struct",
418 (Runtime::Prost, Self::ListValue) => "::prost_types::ListValue",
419 }
420 }
421
422 pub fn fn_suffix(self) -> &'static str {
424 match self {
425 Self::Value => "value",
426 Self::Struct => "struct",
427 Self::ListValue => "list",
428 }
429 }
430}
431
432#[derive(Debug, Clone)]
438pub enum BaseType {
439 String,
441 Int32,
443 Int64,
445 Bool,
447 Float64,
449 Float32,
451 Bytes,
453 Message(String),
455 Enum(String),
457 OneOf(String),
459 Map(Box<UnifiedType>, Box<UnifiedType>),
461 Unit,
463}
464
465impl UnifiedType {
466 pub fn type_ident(&self) -> syn::Ident {
468 let name = match &self.base_type {
469 BaseType::Message(n) | BaseType::Enum(n) | BaseType::OneOf(n) => last_segment(n),
470 BaseType::String => "String",
471 BaseType::Int32 => "i32",
472 BaseType::Int64 => "i64",
473 BaseType::Bool => "bool",
474 BaseType::Float64 => "f64",
475 BaseType::Float32 => "f32",
476 BaseType::Bytes => "Bytes",
477 BaseType::Unit => "()",
478 BaseType::Map(_, _) => "HashMap",
479 };
480 quote::format_ident!("{}", name)
481 }
482
483 pub fn is_singular_bytes(&self) -> bool {
485 matches!(self.base_type, BaseType::Bytes) && !self.is_repeated
486 }
487
488 pub fn string() -> Self {
490 Self {
491 base_type: BaseType::String,
492 is_optional: false,
493 is_repeated: false,
494 }
495 }
496
497 pub fn optional(mut self) -> Self {
499 self.is_optional = true;
500 self
501 }
502
503 pub fn repeated(mut self) -> Self {
505 self.is_repeated = true;
506 self
507 }
508
509 pub fn map(key: UnifiedType, value: UnifiedType) -> Self {
511 Self {
512 base_type: BaseType::Map(Box::new(key), Box::new(value)),
513 is_optional: false,
514 is_repeated: false,
515 }
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn json_types_render_as_the_runtime_well_known_type() {
525 let structs = UnifiedType::map(
526 UnifiedType::string(),
527 UnifiedType {
528 base_type: BaseType::Message(".google.protobuf.Struct".to_string()),
529 is_optional: false,
530 is_repeated: false,
531 },
532 );
533 assert_eq!(
534 unified_to_rust(&structs, RenderContext::Parameter, Runtime::Prost),
535 "HashMap<String, ::prost_types::Struct>"
536 );
537 assert_eq!(
538 unified_to_rust(&structs, RenderContext::Parameter, Runtime::Buffa),
539 "HashMap<String, ::buffa_types::google::protobuf::Struct>"
540 );
541 }
542}