1use typewriter_core::ir::*;
4use typewriter_core::mapper::TypeMapper;
5use typewriter_core::naming::{to_file_style, FileStyle};
6
7use crate::emitter;
8
9pub struct TypeScriptMapper {
13 pub readonly: bool,
15 pub file_style: FileStyle,
17}
18
19impl TypeScriptMapper {
20 pub fn new() -> Self {
21 Self {
22 readonly: false,
23 file_style: FileStyle::KebabCase,
24 }
25 }
26
27 pub fn with_readonly(mut self, readonly: bool) -> Self {
28 self.readonly = readonly;
29 self
30 }
31
32 pub fn with_file_style(mut self, style: FileStyle) -> Self {
33 self.file_style = style;
34 self
35 }
36}
37
38impl Default for TypeScriptMapper {
39 fn default() -> Self {
40 Self::new()
41 }
42}
43
44impl TypeMapper for TypeScriptMapper {
45 fn map_primitive(&self, ty: &PrimitiveType) -> String {
46 match ty {
47 PrimitiveType::String => "string".to_string(),
48 PrimitiveType::Bool => "boolean".to_string(),
49 PrimitiveType::U8 | PrimitiveType::U16 | PrimitiveType::U32 => "number".to_string(),
50 PrimitiveType::I8 | PrimitiveType::I16 | PrimitiveType::I32 => "number".to_string(),
51 PrimitiveType::F32 | PrimitiveType::F64 => "number".to_string(),
52 PrimitiveType::U64 | PrimitiveType::U128 => "bigint".to_string(),
53 PrimitiveType::I64 | PrimitiveType::I128 => "bigint".to_string(),
54 PrimitiveType::Uuid => "string".to_string(),
55 PrimitiveType::DateTime | PrimitiveType::NaiveDate => "string".to_string(),
56 PrimitiveType::JsonValue => "unknown".to_string(),
57 }
58 }
59
60 fn map_option(&self, inner: &TypeKind) -> String {
61 format!("{} | undefined", self.map_type(inner))
62 }
63
64 fn map_vec(&self, inner: &TypeKind) -> String {
65 let inner_type = self.map_type(inner);
66 if inner_type.contains('|') {
68 format!("({}){}", inner_type, "[]")
69 } else {
70 format!("{}{}", inner_type, "[]")
71 }
72 }
73
74 fn map_hashmap(&self, key: &TypeKind, value: &TypeKind) -> String {
75 format!("Record<{}, {}>", self.map_type(key), self.map_type(value))
76 }
77
78 fn map_tuple(&self, elements: &[TypeKind]) -> String {
79 let inner: Vec<String> = elements.iter().map(|e| self.map_type(e)).collect();
80 format!("[{}]", inner.join(", "))
81 }
82
83 fn map_named(&self, name: &str) -> String {
84 name.to_string()
85 }
86
87 fn emit_struct(&self, def: &StructDef) -> String {
88 emitter::render_interface(self, def)
89 }
90
91 fn emit_enum(&self, def: &EnumDef) -> String {
92 emitter::render_enum(self, def)
93 }
94
95 fn file_header(&self, type_name: &str) -> String {
96 format!(
97 "// Auto-generated by typewriter v0.1.1. DO NOT EDIT.\n\
98 // Source: {} \n\
99 // Regenerate: cargo typewriter generate\n\n",
100 type_name
101 )
102 }
103
104 fn file_extension(&self) -> &str {
105 "ts"
106 }
107
108 fn file_naming(&self, type_name: &str) -> String {
109 to_file_style(type_name, self.file_style)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn mapper() -> TypeScriptMapper {
118 TypeScriptMapper::new()
119 }
120
121 #[test]
122 fn test_primitive_mappings() {
123 let m = mapper();
124 assert_eq!(m.map_primitive(&PrimitiveType::String), "string");
125 assert_eq!(m.map_primitive(&PrimitiveType::Bool), "boolean");
126 assert_eq!(m.map_primitive(&PrimitiveType::U32), "number");
127 assert_eq!(m.map_primitive(&PrimitiveType::I32), "number");
128 assert_eq!(m.map_primitive(&PrimitiveType::F64), "number");
129 assert_eq!(m.map_primitive(&PrimitiveType::U64), "bigint");
130 assert_eq!(m.map_primitive(&PrimitiveType::I64), "bigint");
131 assert_eq!(m.map_primitive(&PrimitiveType::Uuid), "string");
132 assert_eq!(m.map_primitive(&PrimitiveType::DateTime), "string");
133 assert_eq!(m.map_primitive(&PrimitiveType::JsonValue), "unknown");
134 }
135
136 #[test]
137 fn test_option_mapping() {
138 let m = mapper();
139 assert_eq!(
140 m.map_option(&TypeKind::Primitive(PrimitiveType::U32)),
141 "number | undefined"
142 );
143 }
144
145 #[test]
146 fn test_vec_mapping() {
147 let m = mapper();
148 assert_eq!(
149 m.map_vec(&TypeKind::Primitive(PrimitiveType::String)),
150 "string[]"
151 );
152 }
153
154 #[test]
155 fn test_hashmap_mapping() {
156 let m = mapper();
157 assert_eq!(
158 m.map_hashmap(
159 &TypeKind::Primitive(PrimitiveType::String),
160 &TypeKind::Primitive(PrimitiveType::U32)
161 ),
162 "Record<string, number>"
163 );
164 }
165
166 #[test]
167 fn test_tuple_mapping() {
168 let m = mapper();
169 assert_eq!(
170 m.map_tuple(&[
171 TypeKind::Primitive(PrimitiveType::String),
172 TypeKind::Primitive(PrimitiveType::U32)
173 ]),
174 "[string, number]"
175 );
176 }
177
178 #[test]
179 fn test_file_naming_kebab() {
180 let m = mapper();
181 assert_eq!(m.file_naming("UserProfile"), "user-profile");
182 assert_eq!(m.file_naming("User"), "user");
183 assert_eq!(m.file_naming("HTTPResponse"), "http-response");
184 }
185
186 #[test]
187 fn test_file_naming_snake() {
188 let m = TypeScriptMapper::new().with_file_style(FileStyle::SnakeCase);
189 assert_eq!(m.file_naming("UserProfile"), "user_profile");
190 assert_eq!(m.file_naming("HTTPResponse"), "http_response");
191 }
192
193 #[test]
194 fn test_file_naming_pascal() {
195 let m = TypeScriptMapper::new().with_file_style(FileStyle::PascalCase);
196 assert_eq!(m.file_naming("UserProfile"), "UserProfile");
197 assert_eq!(m.file_naming("HTTPResponse"), "HTTPResponse");
198 }
199
200 #[test]
201 fn test_output_filename() {
202 let m = mapper();
203 assert_eq!(m.output_filename("UserProfile"), "user-profile.ts");
204 }
205
206 #[test]
207 fn test_output_filename_pascal() {
208 let m = TypeScriptMapper::new().with_file_style(FileStyle::PascalCase);
209 assert_eq!(m.output_filename("UserProfile"), "UserProfile.ts");
210 }
211
212 #[test]
213 fn test_emit_simple_struct() {
214 let m = mapper();
215 let def = StructDef {
216 name: "User".to_string(),
217 fields: vec![
218 FieldDef {
219 name: "id".to_string(),
220 ty: TypeKind::Primitive(PrimitiveType::Uuid),
221 optional: false,
222 rename: None,
223 doc: None,
224 skip: false,
225 flatten: false,
226 },
227 FieldDef {
228 name: "email".to_string(),
229 ty: TypeKind::Primitive(PrimitiveType::String),
230 optional: false,
231 rename: None,
232 doc: None,
233 skip: false,
234 flatten: false,
235 },
236 FieldDef {
237 name: "age".to_string(),
238 ty: TypeKind::Option(Box::new(TypeKind::Primitive(PrimitiveType::U32))),
239 optional: true,
240 rename: None,
241 doc: None,
242 skip: false,
243 flatten: false,
244 },
245 ],
246 doc: None,
247 generics: vec![],
248 };
249
250 let output = m.emit_struct(&def);
251 assert!(output.contains("export interface User {"));
252 assert!(output.contains("id: string;"));
253 assert!(output.contains("email: string;"));
254 assert!(output.contains("age?: number | undefined;"));
255 }
256
257 #[test]
258 fn test_skipped_field() {
259 let m = mapper();
260 let def = StructDef {
261 name: "User".to_string(),
262 fields: vec![
263 FieldDef {
264 name: "email".to_string(),
265 ty: TypeKind::Primitive(PrimitiveType::String),
266 optional: false,
267 rename: None,
268 doc: None,
269 skip: false,
270 flatten: false,
271 },
272 FieldDef {
273 name: "password_hash".to_string(),
274 ty: TypeKind::Primitive(PrimitiveType::String),
275 optional: false,
276 rename: None,
277 doc: None,
278 skip: true,
279 flatten: false,
280 },
281 ],
282 doc: None,
283 generics: vec![],
284 };
285
286 let output = m.emit_struct(&def);
287 assert!(output.contains("email: string;"));
288 assert!(!output.contains("password_hash"));
289 }
290
291 #[test]
292 fn test_renamed_field() {
293 let m = mapper();
294 let def = StructDef {
295 name: "User".to_string(),
296 fields: vec![FieldDef {
297 name: "user_name".to_string(),
298 ty: TypeKind::Primitive(PrimitiveType::String),
299 optional: false,
300 rename: Some("userName".to_string()),
301 doc: None,
302 skip: false,
303 flatten: false,
304 }],
305 doc: None,
306 generics: vec![],
307 };
308
309 let output = m.emit_struct(&def);
310 assert!(output.contains("userName: string;"));
311 }
312
313 #[test]
314 fn test_simple_enum() {
315 let m = mapper();
316 let def = EnumDef {
317 name: "Role".to_string(),
318 variants: vec![
319 VariantDef {
320 name: "Admin".to_string(),
321 rename: None,
322 kind: VariantKind::Unit,
323 doc: None,
324 },
325 VariantDef {
326 name: "User".to_string(),
327 rename: None,
328 kind: VariantKind::Unit,
329 doc: None,
330 },
331 VariantDef {
332 name: "Guest".to_string(),
333 rename: None,
334 kind: VariantKind::Unit,
335 doc: None,
336 },
337 ],
338 representation: EnumRepr::External,
339 doc: None,
340 };
341
342 let output = m.emit_enum(&def);
343 assert!(output.contains("export type Role ="));
344 assert!(output.contains("\"Admin\""));
345 assert!(output.contains("\"User\""));
346 assert!(output.contains("\"Guest\""));
347 }
348}