baobao_codegen_typescript/ast/
types.rs

1//! TypeScript type alias and union builders.
2
3use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5/// A field in a TypeScript object type.
6#[derive(Debug, Clone)]
7pub struct Field {
8    pub name: String,
9    pub ty: String,
10    pub doc: Option<String>,
11    pub optional: bool,
12    pub readonly: bool,
13}
14
15impl Field {
16    pub fn new(name: impl Into<String>, ty: impl Into<String>) -> Self {
17        Self {
18            name: name.into(),
19            ty: ty.into(),
20            doc: None,
21            optional: false,
22            readonly: false,
23        }
24    }
25
26    pub fn doc(mut self, doc: impl Into<String>) -> Self {
27        self.doc = Some(doc.into());
28        self
29    }
30
31    pub fn optional(mut self) -> Self {
32        self.optional = true;
33        self
34    }
35
36    pub fn readonly(mut self) -> Self {
37        self.readonly = true;
38        self
39    }
40}
41
42/// Builder for TypeScript object types (`type Foo = { ... }`).
43#[derive(Debug, Clone)]
44pub struct ObjectType {
45    name: String,
46    doc: Option<String>,
47    fields: Vec<Field>,
48    exported: bool,
49}
50
51impl ObjectType {
52    pub fn new(name: impl Into<String>) -> Self {
53        Self {
54            name: name.into(),
55            doc: None,
56            fields: Vec::new(),
57            exported: true,
58        }
59    }
60
61    pub fn doc(mut self, doc: impl Into<String>) -> Self {
62        self.doc = Some(doc.into());
63        self
64    }
65
66    pub fn field(mut self, field: Field) -> Self {
67        self.fields.push(field);
68        self
69    }
70
71    pub fn private(mut self) -> Self {
72        self.exported = false;
73        self
74    }
75
76    /// Render the object type to a CodeBuilder.
77    pub fn render(&self, builder: CodeBuilder) -> CodeBuilder {
78        let export = if self.exported { "export " } else { "" };
79
80        let builder = if let Some(doc) = &self.doc {
81            builder.jsdoc(doc)
82        } else {
83            builder
84        };
85
86        if self.fields.is_empty() {
87            builder.line(&format!("{}type {} = {{}};", export, self.name))
88        } else {
89            let builder = builder
90                .line(&format!("{}type {} = {{", export, self.name))
91                .indent();
92            self.render_fields(builder).dedent().line("};")
93        }
94    }
95
96    fn render_fields(&self, builder: CodeBuilder) -> CodeBuilder {
97        self.fields.iter().fold(builder, |b, field| {
98            let b = if let Some(doc) = &field.doc {
99                b.jsdoc(doc)
100            } else {
101                b
102            };
103
104            let readonly = if field.readonly { "readonly " } else { "" };
105            let optional = if field.optional { "?" } else { "" };
106
107            b.line(&format!(
108                "{}{}{}: {};",
109                readonly, field.name, optional, field.ty
110            ))
111        })
112    }
113
114    /// Build the object type as a string.
115    pub fn build(&self) -> String {
116        self.render(CodeBuilder::typescript()).build()
117    }
118
119    /// Convert fields to code fragments.
120    fn fields_to_fragments(&self) -> Vec<CodeFragment> {
121        self.fields
122            .iter()
123            .flat_map(|field| {
124                let mut fragments = Vec::new();
125                if let Some(doc) = &field.doc {
126                    fragments.push(CodeFragment::JsDoc(doc.clone()));
127                }
128                let readonly = if field.readonly { "readonly " } else { "" };
129                let optional = if field.optional { "?" } else { "" };
130                fragments.push(CodeFragment::Line(format!(
131                    "{}{}{}: {};",
132                    readonly, field.name, optional, field.ty
133                )));
134                fragments
135            })
136            .collect()
137    }
138}
139
140impl Renderable for ObjectType {
141    fn to_fragments(&self) -> Vec<CodeFragment> {
142        let export = if self.exported { "export " } else { "" };
143        let mut fragments = Vec::new();
144
145        if let Some(doc) = &self.doc {
146            fragments.push(CodeFragment::JsDoc(doc.clone()));
147        }
148
149        if self.fields.is_empty() {
150            fragments.push(CodeFragment::Line(format!(
151                "{}type {} = {{}};",
152                export, self.name
153            )));
154        } else {
155            fragments.push(CodeFragment::Block {
156                header: format!("{}type {} = {{", export, self.name),
157                body: self.fields_to_fragments(),
158                close: Some("};".to_string()),
159            });
160        }
161
162        fragments
163    }
164}
165
166/// Builder for TypeScript type aliases.
167#[derive(Debug, Clone)]
168pub struct TypeAlias {
169    name: String,
170    doc: Option<String>,
171    ty: String,
172    exported: bool,
173}
174
175impl TypeAlias {
176    pub fn new(name: impl Into<String>, ty: impl Into<String>) -> Self {
177        Self {
178            name: name.into(),
179            doc: None,
180            ty: ty.into(),
181            exported: true,
182        }
183    }
184
185    pub fn doc(mut self, doc: impl Into<String>) -> Self {
186        self.doc = Some(doc.into());
187        self
188    }
189
190    pub fn private(mut self) -> Self {
191        self.exported = false;
192        self
193    }
194
195    /// Render the type alias to a CodeBuilder.
196    pub fn render(&self, builder: CodeBuilder) -> CodeBuilder {
197        let export = if self.exported { "export " } else { "" };
198
199        let builder = if let Some(doc) = &self.doc {
200            builder.jsdoc(doc)
201        } else {
202            builder
203        };
204
205        builder.line(&format!("{}type {} = {};", export, self.name, self.ty))
206    }
207
208    /// Build the type alias as a string.
209    pub fn build(&self) -> String {
210        self.render(CodeBuilder::typescript()).build()
211    }
212}
213
214impl Renderable for TypeAlias {
215    fn to_fragments(&self) -> Vec<CodeFragment> {
216        let export = if self.exported { "export " } else { "" };
217        let mut fragments = Vec::new();
218
219        if let Some(doc) = &self.doc {
220            fragments.push(CodeFragment::JsDoc(doc.clone()));
221        }
222
223        fragments.push(CodeFragment::Line(format!(
224            "{}type {} = {};",
225            export, self.name, self.ty
226        )));
227
228        fragments
229    }
230}
231
232/// Builder for TypeScript union types.
233#[derive(Debug, Clone)]
234pub struct Union {
235    name: String,
236    doc: Option<String>,
237    variants: Vec<String>,
238    exported: bool,
239}
240
241impl Union {
242    pub fn new(name: impl Into<String>) -> Self {
243        Self {
244            name: name.into(),
245            doc: None,
246            variants: Vec::new(),
247            exported: true,
248        }
249    }
250
251    pub fn doc(mut self, doc: impl Into<String>) -> Self {
252        self.doc = Some(doc.into());
253        self
254    }
255
256    pub fn variant(mut self, variant: impl Into<String>) -> Self {
257        self.variants.push(variant.into());
258        self
259    }
260
261    pub fn private(mut self) -> Self {
262        self.exported = false;
263        self
264    }
265
266    /// Render the union type to a CodeBuilder.
267    pub fn render(&self, builder: CodeBuilder) -> CodeBuilder {
268        let export = if self.exported { "export " } else { "" };
269
270        let builder = if let Some(doc) = &self.doc {
271            builder.jsdoc(doc)
272        } else {
273            builder
274        };
275
276        let variants_str = self.variants.join(" | ");
277        builder.line(&format!("{}type {} = {};", export, self.name, variants_str))
278    }
279
280    /// Build the union type as a string.
281    pub fn build(&self) -> String {
282        self.render(CodeBuilder::typescript()).build()
283    }
284}
285
286impl Renderable for Union {
287    fn to_fragments(&self) -> Vec<CodeFragment> {
288        let export = if self.exported { "export " } else { "" };
289        let mut fragments = Vec::new();
290
291        if let Some(doc) = &self.doc {
292            fragments.push(CodeFragment::JsDoc(doc.clone()));
293        }
294
295        let variants_str = self.variants.join(" | ");
296        fragments.push(CodeFragment::Line(format!(
297            "{}type {} = {};",
298            export, self.name, variants_str
299        )));
300
301        fragments
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_object_type_empty() {
311        let t = ObjectType::new("Empty").build();
312        assert_eq!(t, "export type Empty = {};\n");
313    }
314
315    #[test]
316    fn test_object_type_with_fields() {
317        let t = ObjectType::new("Person")
318            .field(Field::new("name", "string"))
319            .field(Field::new("age", "number"))
320            .build();
321        assert!(t.contains("export type Person = {"));
322        assert!(t.contains("name: string;"));
323        assert!(t.contains("age: number;"));
324        assert!(t.contains("};"));
325    }
326
327    #[test]
328    fn test_object_type_with_optional_field() {
329        let t = ObjectType::new("Config")
330            .field(Field::new("debug", "boolean").optional())
331            .build();
332        assert!(t.contains("debug?: boolean;"));
333    }
334
335    #[test]
336    fn test_object_type_with_readonly_field() {
337        let t = ObjectType::new("Point")
338            .field(Field::new("x", "number").readonly())
339            .build();
340        assert!(t.contains("readonly x: number;"));
341    }
342
343    #[test]
344    fn test_type_alias() {
345        let t = TypeAlias::new("UserId", "string").build();
346        assert_eq!(t, "export type UserId = string;\n");
347    }
348
349    #[test]
350    fn test_type_alias_with_doc() {
351        let t = TypeAlias::new("Callback", "() => void")
352            .doc("A callback function")
353            .build();
354        assert!(t.contains("/** A callback function */"));
355        assert!(t.contains("export type Callback = () => void;"));
356    }
357
358    #[test]
359    fn test_private_type_alias() {
360        let t = TypeAlias::new("Internal", "number").private().build();
361        assert!(!t.contains("export"));
362        assert!(t.contains("type Internal = number;"));
363    }
364
365    #[test]
366    fn test_union() {
367        let u = Union::new("Status")
368            .variant("\"pending\"")
369            .variant("\"active\"")
370            .variant("\"completed\"")
371            .build();
372        assert!(u.contains("export type Status = \"pending\" | \"active\" | \"completed\";"));
373    }
374
375    #[test]
376    fn test_union_with_doc() {
377        let u = Union::new("Result")
378            .doc("Success or failure")
379            .variant("Success")
380            .variant("Failure")
381            .build();
382        assert!(u.contains("/** Success or failure */"));
383        assert!(u.contains("export type Result = Success | Failure;"));
384    }
385}