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::utils::extract_simple_type_name;
11
12/// Context for rendering types in different situations
13#[derive(Debug, Clone, Copy)]
14pub enum RenderContext {
15    /// A constructor (new method) in Rust
16    Constructor,
17    /// when extracting from a request inside implementations of FromRequest or FromRequestParts
18    /// (path params: renders enum as i32 for direct axum::extract::Path deserialization)
19    Extractor,
20    /// when building the QueryParams serde struct for query string deserialization
21    /// (renders enum as its actual Rust type so serde can deserialize from string variant names)
22    QueryExtractor,
23    /// Regular parameter type
24    Parameter,
25    /// Return type
26    ReturnType,
27    /// Field type in a struct
28    FieldType,
29    /// Builder method parameter
30    BuilderMethod,
31    /// Python parameter type (Rust FFI signatures for PyO3 bindings)
32    PythonParameter,
33    /// NAPI parameter type (Rust NAPI function signatures for Node.js bindings)
34    NapiParameter,
35}
36
37/// Trait for rendering a [`UnifiedType`] into a language-specific string.
38///
39/// Each language backend provides a concrete impl. The four existing free functions
40/// (`unified_to_rust`, `unified_to_python_type`, `unified_to_napi`, `unified_to_typescript`)
41/// are kept as thin wrappers so call-sites don't change.
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/// Python type annotation renderer.
56pub struct PythonRenderer;
57
58impl TypeRenderer for PythonRenderer {
59    fn render(&self, ty: &UnifiedType) -> String {
60        unified_to_python_type(ty)
61    }
62}
63
64/// Rust NAPI parameter type renderer.
65pub struct NapiRenderer;
66
67impl TypeRenderer for NapiRenderer {
68    fn render(&self, ty: &UnifiedType) -> String {
69        unified_to_napi(ty)
70    }
71}
72
73/// TypeScript type annotation renderer.
74pub struct TypeScriptRenderer;
75
76impl TypeRenderer for TypeScriptRenderer {
77    fn render(&self, ty: &UnifiedType) -> String {
78        unified_to_typescript(ty)
79    }
80}
81
82/// Convert a unified type to a Rust type string
83pub fn unified_to_rust(unified_type: &UnifiedType, context: RenderContext) -> String {
84    let base_type_str = match &unified_type.base_type {
85        BaseType::String => {
86            if matches!(context, RenderContext::Constructor) && !unified_type.is_optional {
87                "impl Into<String>".to_string()
88            } else {
89                "String".to_string()
90            }
91        }
92        BaseType::Int32 => "i32".to_string(),
93        BaseType::Int64 => "i64".to_string(),
94        BaseType::Bool => "bool".to_string(),
95        BaseType::Float64 => "f64".to_string(),
96        BaseType::Float32 => "f32".to_string(),
97        BaseType::Bytes => "Vec<u8>".to_string(),
98        BaseType::Unit => "()".to_string(),
99        BaseType::Message(name) => extract_simple_type_name(name),
100        BaseType::Enum(name) => {
101            if matches!(
102                context,
103                RenderContext::Extractor | RenderContext::NapiParameter
104            ) {
105                "i32".to_string()
106            } else {
107                convert_protobuf_enum_to_rust_type(&format!("TYPE_ENUM:{}", name))
108            }
109        }
110        BaseType::OneOf(name) => extract_simple_type_name(name),
111        BaseType::Map(key_type, value_type) => {
112            if matches!(context, RenderContext::Constructor) && !unified_type.is_optional {
113                "impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>".to_string()
114            } else {
115                let key_str = unified_to_rust(key_type, context);
116                let value_str = unified_to_rust(value_type, context);
117                format!("HashMap<{}, {}>", key_str, value_str)
118            }
119        }
120    };
121
122    let mut result = base_type_str;
123
124    // In builder methods we require the inner type only.
125    if unified_type.is_repeated && !matches!(context, RenderContext::BuilderMethod) {
126        result = format!("Vec<{}>", result);
127    }
128
129    if should_wrap_in_option(context, unified_type) {
130        result = format!("Option<{}>", result);
131    }
132
133    result
134}
135
136/// Return `true` when the Rust representation of `ty` in `ctx` should be wrapped in `Option<T>`.
137///
138/// Two independent conditions trigger wrapping:
139///
140/// 1. **Optional field outside a builder method** — builder methods use `impl Into<Option<T>>`
141///    for their parameter type and leave the inner `Option<T>` implicit, so wrapping is skipped
142///    in that context.
143///
144/// 2. **FFI boundaries (Python / NAPI) with maps or repeated fields** — at FFI boundaries we
145///    distinguish "field absent" from "field is an empty collection" by wrapping maps and
146///    repeated fields in `Option<T>`, even when the field is not marked optional in proto.
147fn should_wrap_in_option(ctx: RenderContext, ty: &UnifiedType) -> bool {
148    let is_optional_non_builder = ty.is_optional && !matches!(ctx, RenderContext::BuilderMethod);
149    let is_ffi_collection = matches!(
150        ctx,
151        RenderContext::PythonParameter | RenderContext::NapiParameter
152    ) && (matches!(ty.base_type, BaseType::Map(_, _)) || ty.is_repeated);
153    is_optional_non_builder || is_ffi_collection
154}
155
156/// Generate field assignment code
157pub fn field_assignment(
158    unified_type: &UnifiedType,
159    field_ident: &proc_macro2::Ident,
160    ctx: &RenderContext,
161) -> TokenStream {
162    if matches!(ctx, RenderContext::BuilderMethod) {
163        return flexible_optional_field_assignment(unified_type, field_ident);
164    }
165    match &unified_type.base_type {
166        BaseType::String if !unified_type.is_optional => quote! { #field_ident.into() },
167        BaseType::Enum(_) => {
168            if unified_type.is_repeated {
169                quote! { #field_ident.into_iter().map(|v| v as i32).collect() }
170            } else {
171                quote! { #field_ident as i32 }
172            }
173        }
174        BaseType::Map(_, _) => quote! {
175            #field_ident.into_iter().map(|(k, v)| (k.into(), v.into())).collect()
176        },
177        _ => quote! { #field_ident },
178    }
179}
180
181/// Convert a unified type to a Python type annotation string
182pub fn unified_to_python_type(unified_type: &UnifiedType) -> String {
183    let base_type_str = match &unified_type.base_type {
184        BaseType::String => "str".to_string(),
185        BaseType::Int32 | BaseType::Int64 => "int".to_string(),
186        BaseType::Bool => "bool".to_string(),
187        BaseType::Float64 | BaseType::Float32 => "float".to_string(),
188        BaseType::Bytes => "bytes".to_string(),
189        BaseType::Unit => "None".to_string(),
190        BaseType::Message(name) => extract_simple_type_name(name),
191        BaseType::Enum(name) => extract_simple_type_name(name),
192        BaseType::OneOf(name) => extract_simple_type_name(name),
193        BaseType::Map(key_type, value_type) => {
194            let key_str = unified_to_python_type(key_type);
195            let value_str = unified_to_python_type(value_type);
196            format!("Dict[{}, {}]", key_str, value_str)
197        }
198    };
199
200    let mut result = base_type_str;
201
202    if unified_type.is_repeated {
203        result = format!("List[{}]", result);
204    }
205
206    if unified_type.is_optional {
207        result = format!("Optional[{}]", result);
208    }
209
210    result
211}
212
213/// Convert a unified type to a Rust NAPI parameter type string.
214///
215/// NAPI requires concrete types (no `impl Into<>`). Optional fields
216/// become `Option<T>`, maps become `Option<HashMap<K, V>>`.
217pub fn unified_to_napi(unified_type: &UnifiedType) -> String {
218    let base_type_str = match &unified_type.base_type {
219        BaseType::String => "String".to_string(),
220        BaseType::Int32 => "i32".to_string(),
221        BaseType::Int64 => "i64".to_string(),
222        BaseType::Bool => "bool".to_string(),
223        BaseType::Float64 => "f64".to_string(),
224        BaseType::Float32 => "f32".to_string(),
225        BaseType::Bytes => "Vec<u8>".to_string(),
226        BaseType::Unit => "()".to_string(),
227        BaseType::Message(name) => extract_simple_type_name(name),
228        BaseType::Enum(name) => convert_protobuf_enum_to_rust_type(&format!("TYPE_ENUM:{}", name)),
229        BaseType::OneOf(name) => extract_simple_type_name(name),
230        BaseType::Map(key_type, value_type) => {
231            let key_str = unified_to_napi(key_type);
232            let value_str = unified_to_napi(value_type);
233            format!("HashMap<{}, {}>", key_str, value_str)
234        }
235    };
236
237    let mut result = base_type_str;
238
239    if unified_type.is_repeated {
240        result = format!("Vec<{}>", result);
241    }
242
243    if unified_type.is_optional
244        || matches!(unified_type.base_type, BaseType::Map(_, _))
245        || unified_type.is_repeated
246    {
247        result = format!("Option<{}>", result);
248    }
249
250    result
251}
252
253/// Convert a unified type to a TypeScript type annotation string.
254///
255/// Enums are mapped to `number` since they cross the NAPI boundary as `i32`.
256pub fn unified_to_typescript(unified_type: &UnifiedType) -> String {
257    let base_type_str = match &unified_type.base_type {
258        BaseType::String => "string".to_string(),
259        BaseType::Int32 | BaseType::Int64 => "number".to_string(),
260        BaseType::Bool => "boolean".to_string(),
261        BaseType::Float64 | BaseType::Float32 => "number".to_string(),
262        BaseType::Bytes => "Uint8Array".to_string(),
263        BaseType::Unit => "void".to_string(),
264        BaseType::Message(name) => extract_simple_type_name(name),
265        BaseType::Enum(_) => "number".to_string(),
266        BaseType::OneOf(name) => extract_simple_type_name(name),
267        BaseType::Map(key_type, value_type) => {
268            let key_str = unified_to_typescript(key_type);
269            let value_str = unified_to_typescript(value_type);
270            format!("Record<{}, {}>", key_str, value_str)
271        }
272    };
273
274    let mut result = base_type_str;
275
276    if unified_type.is_repeated {
277        result = format!("{}[]", result);
278    }
279
280    if unified_type.is_optional {
281        result = format!("{} | undefined", result);
282    }
283
284    result
285}
286
287fn flexible_optional_field_assignment(
288    unified_type: &UnifiedType,
289    field_ident: &proc_macro2::Ident,
290) -> TokenStream {
291    if unified_type.is_optional {
292        match &unified_type.base_type {
293            BaseType::Enum(_) => quote! { #field_ident.into().map(|e| e as i32) },
294            _ => quote! { #field_ident.into() },
295        }
296    } else {
297        match &unified_type.base_type {
298            BaseType::String => quote! { #field_ident.into() },
299            BaseType::Int32
300            | BaseType::Int64
301            | BaseType::Bool
302            | BaseType::Float64
303            | BaseType::Float32 => {
304                quote! { #field_ident.into() }
305            }
306            BaseType::Enum(_) => {
307                quote! { #field_ident as i32 }
308            }
309            // Message, OneOf, and Map types are always handled by their own
310            // dedicated branches in `builder_with_impl` and never reach this path.
311            _ => quote! { #field_ident },
312        }
313    }
314}
315
316/// Convert protobuf enum type to Rust type
317///
318/// # FIXME: Fragile heuristic for nested vs package-level enums
319///
320/// This function guesses whether an enum is nested inside a message (e.g.
321/// `example.Catalog.Status`) or defined at package level (e.g.
322/// `example.catalog.v1.CatalogType`) by inspecting the casing of the last
323/// path segment before the enum name.  The special-case for `"V1"` and the
324/// all-lowercase check are workarounds for common proto package naming
325/// conventions, not a principled solution.
326///
327/// The correct fix (Phase 3): accept a `package_prefix: &str` parameter so
328/// callers can strip the known package prefix and always identify the boundary
329/// between package segments and message/enum names deterministically.
330fn convert_protobuf_enum_to_rust_type(proto_type: &str) -> String {
331    if let Some(enum_name) = proto_type.strip_prefix("TYPE_ENUM:") {
332        // Remove leading dot if present
333        let enum_name = enum_name.trim_start_matches('.');
334
335        // Check if this is a nested enum (has a parent message)
336        // Nested enums have PascalCase message names before the enum name
337        if let Some(last_dot) = enum_name.rfind('.') {
338            let parent_part = &enum_name[..last_dot];
339            let enum_simple_name = &enum_name[last_dot + 1..];
340
341            // Check if the parent part ends with a PascalCase message name (nested enum)
342            // vs. a package name like "v1" (package-level enum)
343            let parent_parts: Vec<&str> = parent_part.split('.').collect();
344            if let Some(last_part) = parent_parts.last() {
345                // If the last part starts with uppercase, it's likely a message name (nested enum)
346                // If it's "v1" or similar version, it's a package-level enum
347                if last_part.chars().next().is_some_and(|c| c.is_uppercase())
348                    && *last_part != "V1"
349                    && !last_part
350                        .chars()
351                        .all(|c| c.is_lowercase() || c.is_numeric())
352                {
353                    // This is a nested enum - convert parent message to snake_case module
354                    let snake_case_module = last_part.to_case(Case::Snake);
355
356                    // Convert enum name from UPPER_SNAKE_CASE to PascalCase if needed
357                    let enum_rust_name = if enum_simple_name.contains('_')
358                        && enum_simple_name
359                            .chars()
360                            .all(|c| c.is_uppercase() || c == '_')
361                    {
362                        enum_simple_name.to_case(Case::Pascal)
363                    } else {
364                        enum_simple_name.to_string()
365                    };
366
367                    format!("{}::{}", snake_case_module, enum_rust_name)
368                } else {
369                    // This is a package-level enum - just use the simple name
370                    convert_enum_name_to_rust(enum_simple_name)
371                }
372            } else {
373                // Fallback to simple enum name
374                convert_enum_name_to_rust(enum_simple_name)
375            }
376        } else {
377            // Simple enum name without dots
378            convert_enum_name_to_rust(enum_name)
379        }
380    } else {
381        "i32".to_string() // Fallback for unknown enum types
382    }
383}
384
385/// Convert enum name from proto format to Rust format
386fn convert_enum_name_to_rust(enum_name: &str) -> String {
387    // Convert from proto naming to Rust naming if needed
388    if enum_name.contains('_') && enum_name.chars().all(|c| c.is_uppercase() || c == '_') {
389        enum_name.to_case(Case::Pascal)
390    } else {
391        enum_name.to_string()
392    }
393}
394
395/// Unified type representation that can be converted to different target languages
396#[derive(Debug, Clone)]
397pub struct UnifiedType {
398    /// The base type
399    pub base_type: BaseType,
400    /// Whether this type is optional (Option<T> in Rust)
401    pub is_optional: bool,
402    /// Whether this type is repeated (Vec<T> in Rust)
403    pub is_repeated: bool,
404}
405
406/// Base type categories that can be converted to specific language types
407#[derive(Debug, Clone)]
408pub enum BaseType {
409    /// String type
410    String,
411    /// 32-bit integer
412    Int32,
413    /// 64-bit integer
414    Int64,
415    /// Boolean
416    Bool,
417    /// 64-bit float
418    Float64,
419    /// 32-bit float
420    Float32,
421    /// Byte array
422    Bytes,
423    /// Protobuf message type
424    Message(String),
425    /// Protobuf enum type
426    Enum(String),
427    /// Protobuf oneof type
428    OneOf(String),
429    /// Map type with key and value types
430    Map(Box<UnifiedType>, Box<UnifiedType>),
431    /// Unit/void type
432    Unit,
433}
434
435impl UnifiedType {
436    /// Extract a syn::Ident for the inner type name (last segment of a qualified name).
437    pub fn type_ident(&self) -> syn::Ident {
438        let name = match &self.base_type {
439            BaseType::Message(n) | BaseType::Enum(n) | BaseType::OneOf(n) => {
440                n.split('.').next_back().unwrap_or(n)
441            }
442            BaseType::String => "String",
443            BaseType::Int32 => "i32",
444            BaseType::Int64 => "i64",
445            BaseType::Bool => "bool",
446            BaseType::Float64 => "f64",
447            BaseType::Float32 => "f32",
448            BaseType::Bytes => "Bytes",
449            BaseType::Unit => "()",
450            BaseType::Map(_, _) => "HashMap",
451        };
452        quote::format_ident!("{}", name)
453    }
454
455    /// Create a simple string type
456    pub fn string() -> Self {
457        Self {
458            base_type: BaseType::String,
459            is_optional: false,
460            is_repeated: false,
461        }
462    }
463
464    /// Create an optional version of this type
465    pub fn optional(mut self) -> Self {
466        self.is_optional = true;
467        self
468    }
469
470    /// Create a repeated version of this type
471    pub fn repeated(mut self) -> Self {
472        self.is_repeated = true;
473        self
474    }
475
476    /// Create a map type
477    pub fn map(key: UnifiedType, value: UnifiedType) -> Self {
478        Self {
479            base_type: BaseType::Map(Box::new(key), Box::new(value)),
480            is_optional: false,
481            is_repeated: false,
482        }
483    }
484}