Skip to main content

olai_codegen/parsing/
types.rs

1//! Type conversion utilities for code generation
2//!
3//! This module provides utilities for converting between protobuf types and Rust types
4//! during code generation.
5
6use 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/// Context for rendering types in different situations
14#[derive(Debug, Clone, Copy)]
15pub enum RenderContext {
16    /// A constructor (new method) in Rust
17    Constructor,
18    /// when extracting from a request inside implementations of FromRequest or FromRequestParts
19    /// (path params: renders enum as i32 for direct axum::extract::Path deserialization)
20    Extractor,
21    /// when building the QueryParams serde struct for query string deserialization
22    /// (renders enum as its actual Rust type so serde can deserialize from string variant names)
23    QueryExtractor,
24    /// Regular parameter type
25    Parameter,
26    /// Return type
27    ReturnType,
28    /// Field type in a struct
29    FieldType,
30    /// Builder method parameter
31    BuilderMethod,
32    /// Python parameter type (Rust FFI signatures for PyO3 bindings)
33    PythonParameter,
34    /// NAPI parameter type (Rust NAPI function signatures for Node.js bindings)
35    NapiParameter,
36}
37
38/// Trait for rendering a [`UnifiedType`] into a language-specific string.
39///
40/// Each language backend provides a concrete impl over its free function (`unified_to_rust`,
41/// `unified_to_napi`, `unified_to_typescript`).
42pub trait TypeRenderer {
43    fn render(&self, ty: &UnifiedType) -> String;
44}
45
46/// Rust type renderer — handles optional `impl Into<>` / `Option<>` wrapping.
47pub 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
55/// Rust NAPI parameter type renderer.
56pub struct NapiRenderer;
57
58impl TypeRenderer for NapiRenderer {
59    fn render(&self, ty: &UnifiedType) -> String {
60        unified_to_napi(ty)
61    }
62}
63
64/// TypeScript type annotation renderer.
65pub struct TypeScriptRenderer;
66
67impl TypeRenderer for TypeScriptRenderer {
68    fn render(&self, ty: &UnifiedType) -> String {
69        unified_to_typescript(ty)
70    }
71}
72
73/// Convert a unified type to a Rust type string.
74///
75/// Note: the *type* a field renders as is currently runtime-independent. Under both prost
76/// and buffa, generated builder/constructor params and the query-deserialization struct use
77/// the bare enum type `E` (serde deserializes the variant-name string into it), and the
78/// runtime-specific wrapping (`as i32` / `EnumValue::Known`) is applied by
79/// [`field_assignment`]. If a future runtime needs a different field *type* (e.g. buffa
80/// zero-copy view types), thread a `Runtime` argument back in here.
81pub fn unified_to_rust(unified_type: &UnifiedType, context: RenderContext) -> String {
82    let base_type_str = match &unified_type.base_type {
83        BaseType::String => {
84            // A repeated string stays a plain `Vec<String>`.
85            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    // In builder methods we require the inner type only.
134    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
145/// Return `true` when the Rust representation of `ty` in `ctx` should be wrapped in `Option<T>`.
146///
147/// Two independent conditions trigger wrapping:
148///
149/// 1. **Optional field outside a builder method** — builder methods use `impl Into<Option<T>>`
150///    for their parameter type and leave the inner `Option<T>` implicit, so wrapping is skipped
151///    in that context.
152///
153/// 2. **FFI boundaries (Python / NAPI) with maps or repeated fields** — at FFI boundaries we
154///    distinguish "field absent" from "field is an empty collection" by wrapping maps and
155///    repeated fields in `Option<T>`, even when the field is not marked optional in proto.
156fn 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    // At FFI boundaries, collections (maps, repeated) are wrapped in `Option` so callers can
163    // distinguish "absent" from "empty". Message/oneof optionality is carried by `ty.is_optional`
164    // (set during analysis from the field's `REQUIRED` behavior), so a required message body
165    // renders as a bare `T` required param while an optional one renders as `Option<T>`.
166    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
171/// Generate field assignment code.
172///
173/// Wraps a constructor/builder parameter so it can be stored into the request struct field.
174/// `runtime` selects the ABI of the protobuf library the generated code consumes:
175///
176/// - [`Runtime::Prost`]: open enums are `i32` (`#field as i32`); singular message/oneof
177///   fields are `Option<Box<T>>` (`Some(#field)`).
178/// - [`Runtime::Buffa`]: open enums are `EnumValue<E>` (`buffa::EnumValue::Known(#field)`);
179///   singular message/oneof fields are `MessageField<T>` (`buffa::MessageField::some(#field)`).
180pub 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        // A single string is an `impl Into<String>`; a repeated one is already `Vec<String>`.
191        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        // Only string map values go through `.into()`; others already have the field's type.
205        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        // Singular message/oneof fields wrap the bare `T` constructor param into the request
215        // struct's optional-message representation. (Repeated messages are `Vec<T>`, not
216        // optional, so they fall through unchanged.) Oneof fields are `Option<Oneof>` under both
217        // runtimes, so they always take the `Some(..)` form regardless of `runtime`.
218        BaseType::OneOf(_) if !unified_type.is_repeated => {
219            quote! { Some(#field_ident) }
220        }
221        BaseType::Message(_) if !unified_type.is_repeated => match runtime {
222            // prost: `Option<Box<T>>`
223            Runtime::Prost => quote! { Some(#field_ident) },
224            // buffa: `MessageField<T>`
225            Runtime::Buffa => quote! { buffa::MessageField::some(#field_ident) },
226        },
227        _ => quote! { #field_ident },
228    }
229}
230
231/// Convert a unified type to a Rust NAPI parameter type string.
232///
233/// NAPI requires concrete types (no `impl Into<>`). Optional fields
234/// become `Option<T>`, maps become `Option<HashMap<K, V>>`.
235pub 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
271/// Convert a unified type to a TypeScript type annotation string.
272///
273/// Enums are mapped to `number` since they cross the NAPI boundary as `i32`.
274pub 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            // buffa stores singular message/oneof fields as `MessageField<T>`. The
317            // setter arg is `impl Into<Option<T>>`, so resolve to `Option<T>` first
318            // (annotating the intermediate disambiguates the chained `.into()`), then
319            // convert via `MessageField: From<Option<T>>`.
320            (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            // Message, OneOf, and Map types are always handled by their own
344            // dedicated branches in `builder_with_impl` and never reach this path.
345            _ => quote! { #field_ident },
346        }
347    }
348}
349
350/// Convert protobuf enum type to Rust type
351///
352/// # FIXME: Fragile heuristic for nested vs package-level enums
353///
354/// This function guesses whether an enum is nested inside a message (e.g.
355/// `example.Catalog.Status`) or defined at package level (e.g.
356/// `example.catalog.v1.CatalogType`) by inspecting the casing of the last
357/// path segment before the enum name.  The special-case for `"V1"` and the
358/// all-lowercase check are workarounds for common proto package naming
359/// conventions, not a principled solution.
360///
361/// The correct fix (Phase 3): accept a `package_prefix: &str` parameter so
362/// callers can strip the known package prefix and always identify the boundary
363/// between package segments and message/enum names deterministically.
364pub(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        // Remove leading dot if present
367        let enum_name = enum_name.trim_start_matches('.');
368
369        // Check if this is a nested enum (has a parent message)
370        // Nested enums have PascalCase message names before the enum name
371        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            // Check if the parent part ends with a PascalCase message name (nested enum)
376            // vs. a package name like "v1" (package-level enum)
377            let parent_parts: Vec<&str> = parent_part.split('.').collect();
378            if let Some(last_part) = parent_parts.last() {
379                // If the last part starts with uppercase, it's likely a message name (nested enum)
380                // If it's "v1" or similar version, it's a package-level enum
381                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                    // This is a nested enum - convert parent message to snake_case module
388                    let snake_case_module = last_part.to_case(Case::Snake);
389
390                    // Convert enum name from UPPER_SNAKE_CASE to PascalCase if needed
391                    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                    // This is a package-level enum - just use the simple name
404                    convert_enum_name_to_rust(enum_simple_name)
405                }
406            } else {
407                // Fallback to simple enum name
408                convert_enum_name_to_rust(enum_simple_name)
409            }
410        } else {
411            // Simple enum name without dots
412            convert_enum_name_to_rust(enum_name)
413        }
414    } else {
415        "i32".to_string() // Fallback for unknown enum types
416    }
417}
418
419/// Convert enum name from proto format to Rust format
420fn convert_enum_name_to_rust(enum_name: &str) -> String {
421    // Convert from proto naming to Rust naming if needed
422    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/// A language-neutral description of a protobuf field's type.
430///
431/// `UnifiedType` is the bridge between parsing and generation: a proto field's type and
432/// cardinality are normalized into this single shape once, then each language backend renders it
433/// via a [`TypeRenderer`] (e.g. [`unified_to_rust`]). The [`base_type`](Self::base_type) carries
434/// the underlying category while [`is_optional`](Self::is_optional) and
435/// [`is_repeated`](Self::is_repeated) describe the cardinality wrapping applied around it.
436#[derive(Debug, Clone)]
437pub struct UnifiedType {
438    /// The underlying type category.
439    pub base_type: BaseType,
440    /// Whether this type is optional (`Option<T>` in Rust).
441    pub is_optional: bool,
442    /// Whether this type is repeated (`Vec<T>` in Rust).
443    pub is_repeated: bool,
444}
445
446/// A free-form JSON type of `google/protobuf/struct.proto`: buffa's type in Rust, a plain
447/// object in Python.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum JsonType {
450    /// `google.protobuf.Value`: any JSON value.
451    Value,
452    /// `google.protobuf.Struct`: a JSON object.
453    Struct,
454    /// `google.protobuf.ListValue`: a JSON array.
455    ListValue,
456}
457
458impl JsonType {
459    /// The JSON type of the proto message `name`, if it is one.
460    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    /// The JSON type of a field type: the type itself, or the value type of a map.
470    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    /// The path of the buffa type.
479    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    /// The suffix of the type's conversion functions: `value`, `struct`, `list`.
488    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/// The underlying category of a [`UnifiedType`], independent of optional/repeated wrapping.
498///
499/// Scalars map directly to a primitive in each target language; the named variants
500/// ([`Message`](Self::Message), [`Enum`](Self::Enum), [`OneOf`](Self::OneOf)) carry the
501/// fully-qualified protobuf type name, from which renderers take the final segment.
502#[derive(Debug, Clone)]
503pub enum BaseType {
504    /// String type
505    String,
506    /// 32-bit integer
507    Int32,
508    /// 64-bit integer
509    Int64,
510    /// Boolean
511    Bool,
512    /// 64-bit float
513    Float64,
514    /// 32-bit float
515    Float32,
516    /// Byte array
517    Bytes,
518    /// Protobuf message type
519    Message(String),
520    /// Protobuf enum type
521    Enum(String),
522    /// Protobuf oneof type
523    OneOf(String),
524    /// Map type with key and value types
525    Map(Box<UnifiedType>, Box<UnifiedType>),
526    /// Unit/void type
527    Unit,
528}
529
530impl UnifiedType {
531    /// Extract a syn::Ident for the inner type name (last segment of a qualified name).
532    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    /// A singular `bytes` value: not repeated.
551    pub fn is_singular_bytes(&self) -> bool {
552        matches!(self.base_type, BaseType::Bytes) && !self.is_repeated
553    }
554
555    /// Create a simple string type
556    pub fn string() -> Self {
557        Self {
558            base_type: BaseType::String,
559            is_optional: false,
560            is_repeated: false,
561        }
562    }
563
564    /// Create an optional version of this type
565    pub fn optional(mut self) -> Self {
566        self.is_optional = true;
567        self
568    }
569
570    /// Create a repeated version of this type
571    pub fn repeated(mut self) -> Self {
572        self.is_repeated = true;
573        self
574    }
575
576    /// Create a map type
577    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}