facet_typescript/
lib.rs

1//! Generate TypeScript type definitions from facet type metadata.
2//!
3//! This crate uses facet's reflection capabilities to generate TypeScript
4//! interfaces and types from any type that implements `Facet`.
5//!
6//! # Example
7//!
8//! ```
9//! use facet::Facet;
10//! use facet_typescript::to_typescript;
11//!
12//! #[derive(Facet)]
13//! struct User {
14//!     name: String,
15//!     age: u32,
16//!     email: Option<String>,
17//! }
18//!
19//! let ts = to_typescript::<User>();
20//! assert!(ts.contains("export interface User"));
21//! ```
22
23extern crate alloc;
24
25use alloc::collections::BTreeSet;
26use alloc::string::String;
27use alloc::vec::Vec;
28use core::fmt::Write;
29
30use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType};
31
32/// Generate TypeScript definitions for a single type.
33///
34/// Returns a string containing the TypeScript interface or type declaration.
35pub fn to_typescript<T: Facet<'static>>() -> String {
36    let mut generator = TypeScriptGenerator::new();
37    generator.add_shape(T::SHAPE);
38    generator.finish()
39}
40
41/// Generator for TypeScript type definitions.
42///
43/// Use this when you need to generate multiple related types.
44pub struct TypeScriptGenerator {
45    output: String,
46    /// Types already generated (by type identifier)
47    generated: BTreeSet<&'static str>,
48    /// Types queued for generation
49    queue: Vec<&'static Shape>,
50    /// Indentation level
51    indent: usize,
52}
53
54impl Default for TypeScriptGenerator {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl TypeScriptGenerator {
61    /// Create a new TypeScript generator.
62    pub fn new() -> Self {
63        Self {
64            output: String::new(),
65            generated: BTreeSet::new(),
66            queue: Vec::new(),
67            indent: 0,
68        }
69    }
70
71    /// Add a type to generate.
72    pub fn add_type<T: Facet<'static>>(&mut self) {
73        self.add_shape(T::SHAPE);
74    }
75
76    /// Add a shape to generate.
77    pub fn add_shape(&mut self, shape: &'static Shape) {
78        if !self.generated.contains(shape.type_identifier) {
79            self.queue.push(shape);
80        }
81    }
82
83    /// Finish generation and return the TypeScript code.
84    pub fn finish(mut self) -> String {
85        // Process queue until empty
86        while let Some(shape) = self.queue.pop() {
87            if self.generated.contains(shape.type_identifier) {
88                continue;
89            }
90            self.generated.insert(shape.type_identifier);
91            self.generate_shape(shape);
92        }
93        self.output
94    }
95
96    fn write_indent(&mut self) {
97        for _ in 0..self.indent {
98            self.output.push_str("  ");
99        }
100    }
101
102    fn generate_shape(&mut self, shape: &'static Shape) {
103        // Handle transparent wrappers - generate the inner type instead
104        if let Some(inner) = shape.inner {
105            self.add_shape(inner);
106            // Generate a type alias
107            let inner_type = self.type_for_shape(inner);
108            writeln!(
109                self.output,
110                "export type {} = {};",
111                shape.type_identifier, inner_type
112            )
113            .unwrap();
114            self.output.push('\n');
115            return;
116        }
117
118        // Generate doc comment if present
119        if !shape.doc.is_empty() {
120            self.output.push_str("/**\n");
121            for line in shape.doc {
122                self.output.push_str(" *");
123                self.output.push_str(line);
124                self.output.push('\n');
125            }
126            self.output.push_str(" */\n");
127        }
128
129        match &shape.ty {
130            Type::User(UserType::Struct(st)) => {
131                self.generate_struct(shape, st.fields, st.kind);
132            }
133            Type::User(UserType::Enum(en)) => {
134                self.generate_enum(shape, en);
135            }
136            _ => {
137                // For other types, generate a type alias
138                let type_str = self.type_for_shape(shape);
139                writeln!(
140                    self.output,
141                    "export type {} = {};",
142                    shape.type_identifier, type_str
143                )
144                .unwrap();
145                self.output.push('\n');
146            }
147        }
148    }
149
150    fn generate_struct(
151        &mut self,
152        shape: &'static Shape,
153        fields: &'static [Field],
154        kind: StructKind,
155    ) {
156        match kind {
157            StructKind::Unit => {
158                // Unit struct as null
159                writeln!(self.output, "export type {} = null;", shape.type_identifier).unwrap();
160            }
161            StructKind::TupleStruct if fields.len() == 1 => {
162                // Newtype - type alias to inner
163                let inner_type = self.type_for_shape(fields[0].shape.get());
164                writeln!(
165                    self.output,
166                    "export type {} = {};",
167                    shape.type_identifier, inner_type
168                )
169                .unwrap();
170            }
171            StructKind::TupleStruct | StructKind::Tuple => {
172                // Tuple as array type
173                let types: Vec<String> = fields
174                    .iter()
175                    .map(|f| self.type_for_shape(f.shape.get()))
176                    .collect();
177                writeln!(
178                    self.output,
179                    "export type {} = [{}];",
180                    shape.type_identifier,
181                    types.join(", ")
182                )
183                .unwrap();
184            }
185            StructKind::Struct => {
186                writeln!(self.output, "export interface {} {{", shape.type_identifier).unwrap();
187                self.indent += 1;
188
189                for field in fields {
190                    // Skip fields marked with skip
191                    if field.flags.contains(facet_core::FieldFlags::SKIP) {
192                        continue;
193                    }
194
195                    // Generate doc comment for field
196                    if !field.doc.is_empty() {
197                        self.write_indent();
198                        self.output.push_str("/**\n");
199                        for line in field.doc {
200                            self.write_indent();
201                            self.output.push_str(" *");
202                            self.output.push_str(line);
203                            self.output.push('\n');
204                        }
205                        self.write_indent();
206                        self.output.push_str(" */\n");
207                    }
208
209                    let field_name = field.rename.unwrap_or(field.name);
210                    let is_option = matches!(field.shape.get().def, Def::Option(_));
211
212                    self.write_indent();
213
214                    // Use optional marker for Option fields
215                    if is_option {
216                        // Unwrap the Option to get the inner type
217                        if let Def::Option(opt) = &field.shape.get().def {
218                            let inner_type = self.type_for_shape(opt.t);
219                            writeln!(self.output, "{}?: {};", field_name, inner_type).unwrap();
220                        }
221                    } else {
222                        let field_type = self.type_for_shape(field.shape.get());
223                        writeln!(self.output, "{}: {};", field_name, field_type).unwrap();
224                    }
225                }
226
227                self.indent -= 1;
228                self.output.push_str("}\n");
229            }
230        }
231        self.output.push('\n');
232    }
233
234    fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
235        // Check if all variants are unit variants (simple string union)
236        let all_unit = enum_type
237            .variants
238            .iter()
239            .all(|v| matches!(v.data.kind, StructKind::Unit));
240
241        if all_unit {
242            // Simple string literal union
243            let variants: Vec<String> = enum_type
244                .variants
245                .iter()
246                .map(|v| format!("\"{}\"", v.name))
247                .collect();
248            writeln!(
249                self.output,
250                "export type {} = {};",
251                shape.type_identifier,
252                variants.join(" | ")
253            )
254            .unwrap();
255        } else {
256            // Discriminated union
257            // Generate each variant as a separate interface, then union them
258            let mut variant_types = Vec::new();
259
260            for variant in enum_type.variants {
261                match variant.data.kind {
262                    StructKind::Unit => {
263                        // Unit variant as object with type discriminator
264                        variant_types.push(format!("{{ {}: \"{}\" }}", variant.name, variant.name));
265                    }
266                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
267                        // Newtype variant: { VariantName: InnerType }
268                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
269                        variant_types.push(format!("{{ {}: {} }}", variant.name, inner));
270                    }
271                    _ => {
272                        // Struct variant: { VariantName: { ...fields } }
273                        let mut field_types = Vec::new();
274                        for field in variant.data.fields {
275                            let field_name = field.rename.unwrap_or(field.name);
276                            let field_type = self.type_for_shape(field.shape.get());
277                            field_types.push(format!("{}: {}", field_name, field_type));
278                        }
279                        variant_types.push(format!(
280                            "{{ {}: {{ {} }} }}",
281                            variant.name,
282                            field_types.join("; ")
283                        ));
284                    }
285                }
286            }
287
288            writeln!(
289                self.output,
290                "export type {} =\n  | {};",
291                shape.type_identifier,
292                variant_types.join("\n  | ")
293            )
294            .unwrap();
295        }
296        self.output.push('\n');
297    }
298
299    fn type_for_shape(&mut self, shape: &'static Shape) -> String {
300        // Check Def first - these take precedence over transparent wrappers
301        match &shape.def {
302            Def::Scalar => self.scalar_type(shape),
303            Def::Option(opt) => {
304                format!("{} | null", self.type_for_shape(opt.t))
305            }
306            Def::List(list) => {
307                format!("{}[]", self.type_for_shape(list.t))
308            }
309            Def::Array(arr) => {
310                format!("{}[]", self.type_for_shape(arr.t))
311            }
312            Def::Set(set) => {
313                format!("{}[]", self.type_for_shape(set.t))
314            }
315            Def::Map(map) => {
316                format!("Record<string, {}>", self.type_for_shape(map.v))
317            }
318            Def::Pointer(ptr) => {
319                // Smart pointers are transparent
320                if let Some(pointee) = ptr.pointee {
321                    self.type_for_shape(pointee)
322                } else {
323                    "unknown".to_string()
324                }
325            }
326            Def::Undefined => {
327                // User-defined types - queue for generation and return name
328                match &shape.ty {
329                    Type::User(UserType::Struct(_) | UserType::Enum(_)) => {
330                        self.add_shape(shape);
331                        shape.type_identifier.to_string()
332                    }
333                    _ => {
334                        // For other undefined types, check if it's a transparent wrapper
335                        if let Some(inner) = shape.inner {
336                            self.type_for_shape(inner)
337                        } else {
338                            "unknown".to_string()
339                        }
340                    }
341                }
342            }
343            _ => {
344                // For other defs, check if it's a transparent wrapper
345                if let Some(inner) = shape.inner {
346                    self.type_for_shape(inner)
347                } else {
348                    "unknown".to_string()
349                }
350            }
351        }
352    }
353
354    fn scalar_type(&self, shape: &'static Shape) -> String {
355        match shape.type_identifier {
356            // Strings
357            "String" | "str" | "&str" | "Cow" => "string".to_string(),
358
359            // Booleans
360            "bool" => "boolean".to_string(),
361
362            // Numbers (all become number in TypeScript)
363            "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
364            | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
365
366            // Char as string
367            "char" => "string".to_string(),
368
369            // Unknown scalar
370            _ => "unknown".to_string(),
371        }
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use facet::Facet;
379
380    #[test]
381    fn test_simple_struct() {
382        #[derive(Facet)]
383        struct User {
384            name: String,
385            age: u32,
386        }
387
388        let ts = to_typescript::<User>();
389        insta::assert_snapshot!(ts);
390    }
391
392    #[test]
393    fn test_optional_field() {
394        #[derive(Facet)]
395        struct Config {
396            required: String,
397            optional: Option<String>,
398        }
399
400        let ts = to_typescript::<Config>();
401        insta::assert_snapshot!(ts);
402    }
403
404    #[test]
405    fn test_simple_enum() {
406        #[derive(Facet)]
407        #[repr(u8)]
408        enum Status {
409            Active,
410            Inactive,
411            Pending,
412        }
413
414        let ts = to_typescript::<Status>();
415        insta::assert_snapshot!(ts);
416    }
417
418    #[test]
419    fn test_vec() {
420        #[derive(Facet)]
421        struct Data {
422            items: Vec<String>,
423        }
424
425        let ts = to_typescript::<Data>();
426        insta::assert_snapshot!(ts);
427    }
428
429    #[test]
430    fn test_nested_types() {
431        #[derive(Facet)]
432        struct Inner {
433            value: i32,
434        }
435
436        #[derive(Facet)]
437        struct Outer {
438            inner: Inner,
439            name: String,
440        }
441
442        let ts = to_typescript::<Outer>();
443        insta::assert_snapshot!(ts);
444    }
445}