baobao_codegen_typescript/ast/
interface.rs1use baobao_codegen::builder::{CodeBuilder, CodeFragment, Renderable};
4
5#[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#[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 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 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 pub fn field_with(mut self, field: InterfaceField) -> Self {
66 self.fields.push(field);
67 self
68 }
69
70 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 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 pub fn build(&self) -> String {
103 self.render(CodeBuilder::typescript()).build()
104 }
105
106 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}