baobao_codegen_typescript/ast/
types.rs1use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5#[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#[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 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 pub fn build(&self) -> String {
116 self.render(CodeBuilder::typescript()).build()
117 }
118
119 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#[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 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 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#[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 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 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}