baobao_codegen_typescript/
structure_renderer.rs1use baobao_codegen::builder::{
13 AttributeSpec, EnumSpec, FieldSpec, StructSpec, StructureRenderer, TypeMapper, VariantKind,
14 VariantSpec, Visibility,
15};
16
17use crate::type_mapper::TypeScriptCodeTypeMapper;
18
19#[derive(Debug, Clone, Copy, Default)]
23pub struct TypeScriptStructureRenderer {
24 type_mapper: TypeScriptCodeTypeMapper,
25}
26
27impl TypeScriptStructureRenderer {
28 pub fn new() -> Self {
30 Self {
31 type_mapper: TypeScriptCodeTypeMapper,
32 }
33 }
34
35 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 fn render_struct(&self, spec: &StructSpec) -> String {
47 let mut result = String::new();
48
49 result.push_str(&self.render_doc(&spec.doc, ""));
51
52 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 fn render_enum(&self, spec: &EnumSpec) -> String {
76 let mut result = String::new();
77
78 result.push_str(&self.render_doc(&spec.doc, ""));
80
81 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 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 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 fn render_field(&self, spec: &FieldSpec) -> String {
109 let mut result = String::new();
110
111 result.push_str(&self.render_doc(&spec.doc, " "));
113
114 result.push_str(" ");
116 result.push_str(&spec.name);
117
118 if spec.ty.is_optional() {
120 result.push('?');
121 }
122
123 result.push_str(": ");
124 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 fn render_variant(&self, spec: &VariantSpec) -> String {
142 let mut result = String::new();
143
144 result.push_str(&self.render_doc(&spec.doc, " "));
146
147 result.push_str(" ");
148
149 match &spec.kind {
150 VariantKind::Unit => {
151 result.push_str("{ kind: \"");
153 result.push_str(&spec.name);
154 result.push_str("\" };\n");
155 }
156 VariantKind::Tuple(fields) => {
157 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 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 fn render_attribute(&self, _spec: &AttributeSpec) -> String {
197 String::new()
200 }
201
202 fn render_visibility(&self, vis: Visibility) -> &'static str {
203 match vis {
204 Visibility::Public => "export",
205 Visibility::Private => "",
206 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}