baobao_codegen_typescript/
structure_renderer.rs

1//! TypeScript implementation of StructureRenderer for Code IR types.
2//!
3//! This module provides rendering of language-agnostic structure specifications
4//! (`StructSpec`, `EnumSpec`) to TypeScript code.
5//!
6//! # TypeScript Mapping
7//!
8//! - `StructSpec` → TypeScript `interface` or `type`
9//! - `EnumSpec` → TypeScript `type` union or const enum
10//! - `Visibility` → `export` modifier (Public) or nothing (Private)
11
12use baobao_codegen::builder::{
13    AttributeSpec, EnumSpec, FieldSpec, StructSpec, StructureRenderer, TypeMapper, VariantKind,
14    VariantSpec, Visibility,
15};
16
17use crate::type_mapper::TypeScriptCodeTypeMapper;
18
19/// TypeScript implementation of StructureRenderer.
20///
21/// Renders language-agnostic `StructSpec` and `EnumSpec` to TypeScript code.
22#[derive(Debug, Clone, Copy, Default)]
23pub struct TypeScriptStructureRenderer {
24    type_mapper: TypeScriptCodeTypeMapper,
25}
26
27impl TypeScriptStructureRenderer {
28    /// Create a new TypeScript structure renderer.
29    pub fn new() -> Self {
30        Self {
31            type_mapper: TypeScriptCodeTypeMapper,
32        }
33    }
34
35    /// Render doc comment if present.
36    fn render_doc(&self, doc: &Option<String>, indent: &str) -> String {
37        match doc {
38            Some(d) => format!("{}/** {} */\n", indent, d),
39            None => String::new(),
40        }
41    }
42}
43
44impl StructureRenderer for TypeScriptStructureRenderer {
45    /// Render a struct as a TypeScript interface.
46    fn render_struct(&self, spec: &StructSpec) -> String {
47        let mut result = String::new();
48
49        // Doc comment
50        result.push_str(&self.render_doc(&spec.doc, ""));
51
52        // Visibility and interface declaration
53        let vis = self.render_visibility(spec.visibility);
54        if !vis.is_empty() {
55            result.push_str(vis);
56            result.push(' ');
57        }
58        result.push_str("interface ");
59        result.push_str(&spec.name);
60
61        if spec.fields.is_empty() {
62            result.push_str(" {}\n");
63        } else {
64            result.push_str(" {\n");
65            for field in &spec.fields {
66                result.push_str(&self.render_field(field));
67            }
68            result.push_str("}\n");
69        }
70
71        result
72    }
73
74    /// Render an enum as a TypeScript type union.
75    fn render_enum(&self, spec: &EnumSpec) -> String {
76        let mut result = String::new();
77
78        // Doc comment
79        result.push_str(&self.render_doc(&spec.doc, ""));
80
81        // Visibility and type declaration
82        let vis = self.render_visibility(spec.visibility);
83        if !vis.is_empty() {
84            result.push_str(vis);
85            result.push(' ');
86        }
87        result.push_str("type ");
88        result.push_str(&spec.name);
89        result.push_str(" =\n");
90
91        // Render variants as a discriminated union
92        let variant_count = spec.variants.len();
93        for (i, variant) in spec.variants.iter().enumerate() {
94            result.push_str(&self.render_variant(variant));
95            if i < variant_count - 1 {
96                // Remove trailing semicolon and newline, add pipe
97                if result.ends_with(";\n") {
98                    result.truncate(result.len() - 2);
99                }
100                result.push_str(" |\n");
101            }
102        }
103
104        result
105    }
106
107    /// Render a field as a TypeScript interface property.
108    fn render_field(&self, spec: &FieldSpec) -> String {
109        let mut result = String::new();
110
111        // Doc comment
112        result.push_str(&self.render_doc(&spec.doc, "  "));
113
114        // Field declaration (no visibility in interface fields)
115        result.push_str("  ");
116        result.push_str(&spec.name);
117
118        // Check if optional (TypeRef::Optional)
119        if spec.ty.is_optional() {
120            result.push('?');
121        }
122
123        result.push_str(": ");
124        // For optional types, render the inner type
125        let type_str = if spec.ty.is_optional() {
126            if let Some(inner) = spec.ty.inner_type() {
127                self.type_mapper.render_type(inner)
128            } else {
129                self.type_mapper.render_type(&spec.ty)
130            }
131        } else {
132            self.type_mapper.render_type(&spec.ty)
133        };
134        result.push_str(&type_str);
135        result.push_str(";\n");
136
137        result
138    }
139
140    /// Render a variant as part of a TypeScript discriminated union.
141    fn render_variant(&self, spec: &VariantSpec) -> String {
142        let mut result = String::new();
143
144        // Doc comment
145        result.push_str(&self.render_doc(&spec.doc, "  "));
146
147        result.push_str("  ");
148
149        match &spec.kind {
150            VariantKind::Unit => {
151                // Unit variant: { kind: "VariantName" }
152                result.push_str("{ kind: \"");
153                result.push_str(&spec.name);
154                result.push_str("\" };\n");
155            }
156            VariantKind::Tuple(fields) => {
157                // Tuple variant: { kind: "VariantName", value: T } or { kind: "...", values: [T, U] }
158                result.push_str("{ kind: \"");
159                result.push_str(&spec.name);
160                result.push('"');
161
162                if fields.len() == 1 {
163                    result.push_str("; value: ");
164                    result.push_str(&self.type_mapper.render_type(&fields[0]));
165                } else {
166                    result.push_str("; values: [");
167                    let types: Vec<String> = fields
168                        .iter()
169                        .map(|f| self.type_mapper.render_type(f))
170                        .collect();
171                    result.push_str(&types.join(", "));
172                    result.push(']');
173                }
174                result.push_str(" };\n");
175            }
176            VariantKind::Struct(fields) => {
177                // Struct variant: { kind: "VariantName", field1: T, field2: U }
178                result.push_str("{ kind: \"");
179                result.push_str(&spec.name);
180                result.push('"');
181
182                for field in fields {
183                    result.push_str("; ");
184                    result.push_str(&field.name);
185                    result.push_str(": ");
186                    result.push_str(&self.type_mapper.render_type(&field.ty));
187                }
188                result.push_str(" };\n");
189            }
190        }
191
192        result
193    }
194
195    /// Render an attribute (TypeScript uses decorators, but we'll skip for now).
196    fn render_attribute(&self, _spec: &AttributeSpec) -> String {
197        // TypeScript doesn't have attributes in the same way as Rust
198        // Decorators are different and typically used with classes
199        String::new()
200    }
201
202    fn render_visibility(&self, vis: Visibility) -> &'static str {
203        match vis {
204            Visibility::Public => "export",
205            Visibility::Private => "",
206            // TypeScript doesn't have crate/super visibility
207            Visibility::Crate => "",
208            Visibility::Super => "",
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use baobao_codegen::builder::TypeRef;
216
217    use super::*;
218
219    #[test]
220    fn test_render_simple_interface() {
221        let renderer = TypeScriptStructureRenderer::new();
222        let spec = StructSpec::new("User")
223            .doc("A user in the system")
224            .field(FieldSpec::new("id", TypeRef::int()))
225            .field(FieldSpec::new("name", TypeRef::string()));
226
227        let result = renderer.render_struct(&spec);
228        assert!(result.contains("/** A user in the system */"));
229        assert!(result.contains("export interface User {"));
230        assert!(result.contains("id: number;"));
231        assert!(result.contains("name: string;"));
232    }
233
234    #[test]
235    fn test_render_interface_with_optional() {
236        let renderer = TypeScriptStructureRenderer::new();
237        let spec = StructSpec::new("Config")
238            .field(FieldSpec::new("name", TypeRef::string()))
239            .field(FieldSpec::new("timeout", TypeRef::optional(TypeRef::int())));
240
241        let result = renderer.render_struct(&spec);
242        assert!(result.contains("name: string;"));
243        assert!(result.contains("timeout?: number;"));
244    }
245
246    #[test]
247    fn test_render_type_union() {
248        let renderer = TypeScriptStructureRenderer::new();
249        let spec = EnumSpec::new("Status")
250            .doc("Status of an operation")
251            .unit_variant("Pending")
252            .unit_variant("Active")
253            .variant(VariantSpec::tuple("Error", vec![TypeRef::string()]));
254
255        let result = renderer.render_enum(&spec);
256        assert!(result.contains("/** Status of an operation */"));
257        assert!(result.contains("export type Status ="));
258        assert!(result.contains("{ kind: \"Pending\" }"));
259        assert!(result.contains("{ kind: \"Active\" }"));
260        assert!(result.contains("{ kind: \"Error\"; value: string }"));
261    }
262
263    #[test]
264    fn test_render_private_interface() {
265        let renderer = TypeScriptStructureRenderer::new();
266        let spec = StructSpec::new("Internal").private();
267
268        let result = renderer.render_struct(&spec);
269        assert!(result.contains("interface Internal {}"));
270        assert!(!result.contains("export"));
271    }
272
273    #[test]
274    fn test_render_struct_variant() {
275        let renderer = TypeScriptStructureRenderer::new();
276        let spec = EnumSpec::new("Event").variant(VariantSpec::struct_(
277            "Click",
278            vec![
279                FieldSpec::new("x", TypeRef::int()),
280                FieldSpec::new("y", TypeRef::int()),
281            ],
282        ));
283
284        let result = renderer.render_enum(&spec);
285        assert!(result.contains("{ kind: \"Click\"; x: number; y: number }"));
286    }
287
288    #[test]
289    fn test_render_empty_interface() {
290        let renderer = TypeScriptStructureRenderer::new();
291        let spec = StructSpec::new("Empty");
292
293        let result = renderer.render_struct(&spec);
294        assert!(result.contains("export interface Empty {}"));
295    }
296}