baobao_codegen_typescript/ast/
interface.rs

1//! TypeScript interface builder.
2
3use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5/// A field in a TypeScript interface.
6#[derive(Debug, Clone)]
7pub struct InterfaceField {
8    pub name: String,
9    pub ty: String,
10    pub optional: bool,
11    pub readonly: bool,
12}
13
14impl InterfaceField {
15    pub fn new(name: impl Into<String>, ty: impl Into<String>) -> Self {
16        Self {
17            name: name.into(),
18            ty: ty.into(),
19            optional: false,
20            readonly: false,
21        }
22    }
23
24    pub fn optional(mut self) -> Self {
25        self.optional = true;
26        self
27    }
28
29    pub fn readonly(mut self) -> Self {
30        self.readonly = true;
31        self
32    }
33}
34
35/// Builder for TypeScript interfaces.
36#[derive(Debug, Clone)]
37pub struct Interface {
38    name: String,
39    fields: Vec<InterfaceField>,
40    exported: bool,
41}
42
43impl Interface {
44    pub fn new(name: impl Into<String>) -> Self {
45        Self {
46            name: name.into(),
47            fields: Vec::new(),
48            exported: true,
49        }
50    }
51
52    /// Add a required field.
53    pub fn field(mut self, name: impl Into<String>, ty: impl Into<String>) -> Self {
54        self.fields.push(InterfaceField::new(name, ty));
55        self
56    }
57
58    /// Add an optional field.
59    pub fn optional_field(mut self, name: impl Into<String>, ty: impl Into<String>) -> Self {
60        self.fields.push(InterfaceField::new(name, ty).optional());
61        self
62    }
63
64    /// Add a field with full configuration.
65    pub fn field_with(mut self, field: InterfaceField) -> Self {
66        self.fields.push(field);
67        self
68    }
69
70    /// Make this interface private (not exported).
71    pub fn private(mut self) -> Self {
72        self.exported = false;
73        self
74    }
75
76    /// Render the interface to a CodeBuilder.
77    pub fn render(&self, builder: CodeBuilder) -> CodeBuilder {
78        let export = if self.exported { "export " } else { "" };
79
80        if self.fields.is_empty() {
81            builder.line(&format!("{}interface {} {{}}", export, self.name))
82        } else {
83            let builder = builder
84                .line(&format!("{}interface {} {{", export, self.name))
85                .indent();
86            self.render_fields(builder).dedent().line("}")
87        }
88    }
89
90    fn render_fields(&self, builder: CodeBuilder) -> CodeBuilder {
91        self.fields.iter().fold(builder, |b, field| {
92            let readonly = if field.readonly { "readonly " } else { "" };
93            let optional = if field.optional { "?" } else { "" };
94            b.line(&format!(
95                "{}{}{}: {};",
96                readonly, field.name, optional, field.ty
97            ))
98        })
99    }
100
101    /// Build the interface as a string.
102    pub fn build(&self) -> String {
103        self.render(CodeBuilder::typescript()).build()
104    }
105
106    /// Convert fields to code fragments.
107    fn fields_to_fragments(&self) -> Vec<CodeFragment> {
108        self.fields
109            .iter()
110            .map(|field| {
111                let readonly = if field.readonly { "readonly " } else { "" };
112                let optional = if field.optional { "?" } else { "" };
113                CodeFragment::Line(format!(
114                    "{}{}{}: {};",
115                    readonly, field.name, optional, field.ty
116                ))
117            })
118            .collect()
119    }
120}
121
122impl Renderable for Interface {
123    fn to_fragments(&self) -> Vec<CodeFragment> {
124        let export = if self.exported { "export " } else { "" };
125
126        if self.fields.is_empty() {
127            vec![CodeFragment::Line(format!(
128                "{}interface {} {{}}",
129                export, self.name
130            ))]
131        } else {
132            vec![CodeFragment::Block {
133                header: format!("{}interface {} {{", export, self.name),
134                body: self.fields_to_fragments(),
135                close: Some("}".to_string()),
136            }]
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_empty_interface() {
147        let i = Interface::new("Empty").build();
148        assert_eq!(i, "export interface Empty {}\n");
149    }
150
151    #[test]
152    fn test_interface_with_fields() {
153        let i = Interface::new("Person")
154            .field("name", "string")
155            .field("age", "number")
156            .build();
157        assert!(i.contains("export interface Person {"));
158        assert!(i.contains("name: string;"));
159        assert!(i.contains("age: number;"));
160    }
161
162    #[test]
163    fn test_interface_with_optional_field() {
164        let i = Interface::new("Config")
165            .field("required", "string")
166            .optional_field("optional", "number")
167            .build();
168        assert!(i.contains("required: string;"));
169        assert!(i.contains("optional?: number;"));
170    }
171
172    #[test]
173    fn test_private_interface() {
174        let i = Interface::new("Internal")
175            .private()
176            .field("x", "number")
177            .build();
178        assert!(!i.contains("export"));
179        assert!(i.contains("interface Internal {"));
180    }
181
182    #[test]
183    fn test_readonly_field() {
184        let i = Interface::new("Point")
185            .field_with(InterfaceField::new("x", "number").readonly())
186            .build();
187        assert!(i.contains("readonly x: number;"));
188    }
189}