baobao_codegen_typescript/ast/
fns.rs

1//! TypeScript function builder.
2
3use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5/// A parameter in a TypeScript function.
6#[derive(Debug, Clone)]
7pub struct Param {
8    pub name: String,
9    pub ty: String,
10    pub optional: bool,
11}
12
13impl Param {
14    pub fn new(name: impl Into<String>, ty: impl Into<String>) -> Self {
15        Self {
16            name: name.into(),
17            ty: ty.into(),
18            optional: false,
19        }
20    }
21
22    pub fn optional(mut self) -> Self {
23        self.optional = true;
24        self
25    }
26}
27
28/// Builder for TypeScript functions.
29#[derive(Debug, Clone)]
30pub struct Fn {
31    name: String,
32    doc: Option<String>,
33    exported: bool,
34    is_async: bool,
35    params: Vec<Param>,
36    return_type: Option<String>,
37    body: Vec<String>,
38}
39
40impl Fn {
41    pub fn new(name: impl Into<String>) -> Self {
42        Self {
43            name: name.into(),
44            doc: None,
45            exported: true,
46            is_async: false,
47            params: Vec::new(),
48            return_type: None,
49            body: Vec::new(),
50        }
51    }
52
53    pub fn doc(mut self, doc: impl Into<String>) -> Self {
54        self.doc = Some(doc.into());
55        self
56    }
57
58    pub fn private(mut self) -> Self {
59        self.exported = false;
60        self
61    }
62
63    pub fn async_(mut self) -> Self {
64        self.is_async = true;
65        self
66    }
67
68    pub fn param(mut self, param: Param) -> Self {
69        self.params.push(param);
70        self
71    }
72
73    pub fn returns(mut self, ty: impl Into<String>) -> Self {
74        self.return_type = Some(ty.into());
75        self
76    }
77
78    /// Add a line to the function body.
79    pub fn body_line(mut self, line: impl Into<String>) -> Self {
80        self.body.push(line.into());
81        self
82    }
83
84    /// Add raw body content (can contain multiple lines).
85    pub fn body(mut self, content: impl Into<String>) -> Self {
86        for line in content.into().lines() {
87            self.body.push(line.to_string());
88        }
89        self
90    }
91
92    /// Render the function to a CodeBuilder.
93    pub fn render(&self, builder: CodeBuilder) -> CodeBuilder {
94        let builder = if let Some(doc) = &self.doc {
95            builder.jsdoc(doc)
96        } else {
97            builder
98        };
99
100        let export = if self.exported { "export " } else { "" };
101        let async_kw = if self.is_async { "async " } else { "" };
102
103        let params_str = self
104            .params
105            .iter()
106            .map(|p| {
107                let optional = if p.optional { "?" } else { "" };
108                format!("{}{}: {}", p.name, optional, p.ty)
109            })
110            .collect::<Vec<_>>()
111            .join(", ");
112
113        let signature = match &self.return_type {
114            Some(ret) => format!(
115                "{}{}function {}({}): {} {{",
116                export, async_kw, self.name, params_str, ret
117            ),
118            None => format!(
119                "{}{}function {}({}) {{",
120                export, async_kw, self.name, params_str
121            ),
122        };
123
124        let builder = builder.line(&signature).indent();
125        let builder = self.body.iter().fold(builder, |b, line| b.line(line));
126        builder.dedent().line("}")
127    }
128
129    /// Build the function as a string.
130    pub fn build(&self) -> String {
131        self.render(CodeBuilder::typescript()).build()
132    }
133
134    /// Format the function signature.
135    fn format_signature(&self) -> String {
136        let export = if self.exported { "export " } else { "" };
137        let async_kw = if self.is_async { "async " } else { "" };
138
139        let params_str = self
140            .params
141            .iter()
142            .map(|p| {
143                let optional = if p.optional { "?" } else { "" };
144                format!("{}{}: {}", p.name, optional, p.ty)
145            })
146            .collect::<Vec<_>>()
147            .join(", ");
148
149        match &self.return_type {
150            Some(ret) => format!(
151                "{}{}function {}({}): {} {{",
152                export, async_kw, self.name, params_str, ret
153            ),
154            None => format!(
155                "{}{}function {}({}) {{",
156                export, async_kw, self.name, params_str
157            ),
158        }
159    }
160}
161
162impl Renderable for Fn {
163    fn to_fragments(&self) -> Vec<CodeFragment> {
164        let mut fragments = Vec::new();
165
166        // Doc comment
167        if let Some(doc) = &self.doc {
168            fragments.push(CodeFragment::JsDoc(doc.clone()));
169        }
170
171        // Function body
172        let body: Vec<CodeFragment> = self
173            .body
174            .iter()
175            .map(|line| CodeFragment::Line(line.clone()))
176            .collect();
177
178        fragments.push(CodeFragment::Block {
179            header: self.format_signature(),
180            body,
181            close: Some("}".to_string()),
182        });
183
184        fragments
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_simple_fn() {
194        let f = Fn::new("greet").build();
195        assert!(f.contains("export function greet() {"));
196    }
197
198    #[test]
199    fn test_fn_with_params() {
200        let f = Fn::new("add")
201            .param(Param::new("a", "number"))
202            .param(Param::new("b", "number"))
203            .returns("number")
204            .body_line("return a + b;")
205            .build();
206        assert!(f.contains("export function add(a: number, b: number): number {"));
207        assert!(f.contains("return a + b;"));
208    }
209
210    #[test]
211    fn test_async_fn() {
212        let f = Fn::new("fetch").async_().returns("Promise<string>").build();
213        assert!(f.contains("export async function fetch(): Promise<string> {"));
214    }
215
216    #[test]
217    fn test_private_fn() {
218        let f = Fn::new("helper").private().build();
219        assert!(f.contains("function helper() {"));
220        assert!(!f.contains("export"));
221    }
222
223    #[test]
224    fn test_fn_with_doc() {
225        let f = Fn::new("run").doc("Execute the command").build();
226        assert!(f.contains("/** Execute the command */"));
227    }
228
229    #[test]
230    fn test_fn_with_optional_param() {
231        let f = Fn::new("greet")
232            .param(Param::new("name", "string").optional())
233            .build();
234        assert!(f.contains("export function greet(name?: string) {"));
235    }
236}