Skip to main content

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 const 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    #[inline]
103    fn shape_key(shape: &'static Shape) -> &'static str {
104        shape.type_identifier
105    }
106
107    /// Unwrap through options, pointers, transparent wrappers, and proxies to get the effective shape.
108    ///
109    /// Returns the unwrapped shape along with a flag indicating whether an `Option` was encountered.
110    fn unwrap_to_inner_shape(shape: &'static Shape) -> (&'static Shape, bool) {
111        // Unwrap Option<T> first so we can mark fields as optional.
112        if let Def::Option(opt) = &shape.def {
113            let (inner, _) = Self::unwrap_to_inner_shape(opt.t);
114            return (inner, true);
115        }
116        // Unwrap pointers (Arc, Box, etc.)
117        if let Def::Pointer(ptr) = &shape.def
118            && let Some(pointee) = ptr.pointee
119        {
120            return Self::unwrap_to_inner_shape(pointee);
121        }
122        // Unwrap transparent wrappers
123        if let Some(inner) = shape.inner {
124            let (inner_shape, is_optional) = Self::unwrap_to_inner_shape(inner);
125            return (inner_shape, is_optional);
126        }
127        // Handle proxy types - use the proxy's shape for serialization
128        if let Some(proxy_def) = shape.proxy {
129            return Self::unwrap_to_inner_shape(proxy_def.shape);
130        }
131        (shape, false)
132    }
133
134    /// Format a field for inline object types (e.g., in enum variants).
135    /// Returns a string like `"fieldName: Type"` or `"fieldName?: Type"` for Option fields or fields with defaults.
136    fn format_inline_field(&mut self, field: &Field, force_optional: bool) -> String {
137        let field_name = field.effective_name();
138        let field_shape = field.shape.get();
139        let has_default = field.default.is_some();
140
141        if let Def::Option(opt) = &field_shape.def {
142            let inner_type = self.type_for_shape(opt.t);
143            format!("{}?: {}", field_name, inner_type)
144        } else if force_optional || has_default {
145            let field_type = self.type_for_shape(field_shape);
146            format!("{}?: {}", field_name, field_type)
147        } else {
148            let field_type = self.type_for_shape(field_shape);
149            format!("{}: {}", field_name, field_type)
150        }
151    }
152
153    /// Collect inline field strings for a struct's fields, handling skip and flatten.
154    fn collect_inline_fields(
155        &mut self,
156        fields: &'static [Field],
157        force_optional: bool,
158    ) -> Vec<String> {
159        let mut flatten_stack: Vec<&'static str> = Vec::new();
160        self.collect_inline_fields_guarded(fields, force_optional, &mut flatten_stack)
161    }
162
163    fn collect_inline_fields_guarded(
164        &mut self,
165        fields: &'static [Field],
166        force_optional: bool,
167        flatten_stack: &mut Vec<&'static str>,
168    ) -> Vec<String> {
169        let mut result = Vec::new();
170        for field in fields {
171            if field.should_skip_serializing_unconditional() {
172                continue;
173            }
174            if field.is_flattened() {
175                let (inner_shape, parent_is_optional) =
176                    Self::unwrap_to_inner_shape(field.shape.get());
177                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
178                    let inner_key = Self::shape_key(inner_shape);
179                    if flatten_stack.contains(&inner_key) {
180                        continue;
181                    }
182                    flatten_stack.push(inner_key);
183                    result.extend(self.collect_inline_fields_guarded(
184                        st.fields,
185                        force_optional || parent_is_optional,
186                        flatten_stack,
187                    ));
188                    flatten_stack.pop();
189                    continue;
190                }
191            }
192            result.push(self.format_inline_field(field, force_optional));
193        }
194        result
195    }
196
197    /// Check if a struct has any fields that will be serialized.
198    /// This accounts for skipped fields and flattened structs.
199    fn has_serializable_fields(
200        field_owner_shape: &'static Shape,
201        fields: &'static [Field],
202    ) -> bool {
203        let mut flatten_stack: Vec<&'static str> = Vec::new();
204        flatten_stack.push(Self::shape_key(field_owner_shape));
205        Self::has_serializable_fields_guarded(fields, &mut flatten_stack)
206    }
207
208    fn has_serializable_fields_guarded(
209        fields: &'static [Field],
210        flatten_stack: &mut Vec<&'static str>,
211    ) -> bool {
212        for field in fields {
213            if field.should_skip_serializing_unconditional() {
214                continue;
215            }
216            if field.is_flattened() {
217                let (inner_shape, _) = Self::unwrap_to_inner_shape(field.shape.get());
218                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
219                    let inner_key = Self::shape_key(inner_shape);
220                    if flatten_stack.contains(&inner_key) {
221                        continue;
222                    }
223                    flatten_stack.push(inner_key);
224                    let has_fields =
225                        Self::has_serializable_fields_guarded(st.fields, flatten_stack);
226                    flatten_stack.pop();
227                    if has_fields {
228                        return true;
229                    }
230                    continue;
231                }
232            }
233            // Found a field that will be serialized
234            return true;
235        }
236        false
237    }
238
239    /// Write struct fields to output, handling skip and flatten recursively.
240    fn write_struct_fields_for_shape(
241        &mut self,
242        field_owner_shape: &'static Shape,
243        fields: &'static [Field],
244    ) {
245        let mut flatten_stack: Vec<&'static str> = Vec::new();
246        flatten_stack.push(Self::shape_key(field_owner_shape));
247        self.write_struct_fields_guarded(fields, false, &mut flatten_stack);
248    }
249
250    fn write_struct_fields_guarded(
251        &mut self,
252        fields: &'static [Field],
253        force_optional: bool,
254        flatten_stack: &mut Vec<&'static str>,
255    ) {
256        for field in fields {
257            if field.should_skip_serializing_unconditional() {
258                continue;
259            }
260            if field.is_flattened() {
261                let (inner_shape, parent_is_optional) =
262                    Self::unwrap_to_inner_shape(field.shape.get());
263                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
264                    let inner_key = Self::shape_key(inner_shape);
265                    if flatten_stack.contains(&inner_key) {
266                        continue;
267                    }
268                    flatten_stack.push(inner_key);
269                    self.write_struct_fields_guarded(
270                        st.fields,
271                        force_optional || parent_is_optional,
272                        flatten_stack,
273                    );
274                    flatten_stack.pop();
275                    continue;
276                }
277            }
278            self.write_field(field, force_optional);
279        }
280    }
281
282    /// Write a single field to the output.
283    fn write_field(&mut self, field: &Field, force_optional: bool) {
284        // Generate doc comment for field
285        if !field.doc.is_empty() {
286            self.write_indent();
287            self.output.push_str("/**\n");
288            for line in field.doc {
289                self.write_indent();
290                self.output.push_str(" *");
291                self.output.push_str(line);
292                self.output.push('\n');
293            }
294            self.write_indent();
295            self.output.push_str(" */\n");
296        }
297
298        let field_name = field.effective_name();
299        let field_shape = field.shape.get();
300
301        self.write_indent();
302
303        // Use optional marker for Option fields, fields with defaults, or when explicitly forced (flattened Option parents).
304        let has_default = field.default.is_some();
305
306        if let Def::Option(opt) = &field_shape.def {
307            let inner_type = self.type_for_shape(opt.t);
308            writeln!(self.output, "{}?: {};", field_name, inner_type).unwrap();
309        } else if force_optional || has_default {
310            let field_type = self.type_for_shape(field_shape);
311            writeln!(self.output, "{}?: {};", field_name, field_type).unwrap();
312        } else {
313            let field_type = self.type_for_shape(field_shape);
314            writeln!(self.output, "{}: {};", field_name, field_type).unwrap();
315        }
316    }
317
318    fn generate_shape(&mut self, shape: &'static Shape) {
319        // Handle transparent wrappers - generate the inner type instead
320        if let Some(inner) = shape.inner {
321            self.add_shape(inner);
322            // Generate a type alias
323            let inner_type = self.type_for_shape(inner);
324            writeln!(
325                self.output,
326                "export type {} = {};",
327                shape.type_identifier, inner_type
328            )
329            .unwrap();
330            self.output.push('\n');
331            return;
332        }
333
334        // Generate doc comment if present (before proxy handling so proxied types keep their docs)
335        if !shape.doc.is_empty() {
336            self.output.push_str("/**\n");
337            for line in shape.doc {
338                self.output.push_str(" *");
339                self.output.push_str(line);
340                self.output.push('\n');
341            }
342            self.output.push_str(" */\n");
343        }
344
345        // Handle proxy types - use the proxy's shape for generation
346        // but keep the original type name
347        if let Some(proxy_def) = shape.proxy {
348            let proxy_shape = proxy_def.shape;
349            match &proxy_shape.ty {
350                Type::User(UserType::Struct(st)) => {
351                    self.generate_struct(shape, proxy_shape, st.fields, st.kind);
352                    return;
353                }
354                Type::User(UserType::Enum(en)) => {
355                    self.generate_enum(shape, en);
356                    return;
357                }
358                _ => {
359                    // For non-struct/enum proxies (scalars, tuples, collections, etc.),
360                    // generate a type alias to the proxy's type
361                    let proxy_type = self.type_for_shape(proxy_shape);
362                    writeln!(
363                        self.output,
364                        "export type {} = {};",
365                        shape.type_identifier, proxy_type
366                    )
367                    .unwrap();
368                    self.output.push('\n');
369                    return;
370                }
371            }
372        }
373
374        match &shape.ty {
375            Type::User(UserType::Struct(st)) => {
376                self.generate_struct(shape, shape, st.fields, st.kind);
377            }
378            Type::User(UserType::Enum(en)) => {
379                self.generate_enum(shape, en);
380            }
381            _ => {
382                // For other types, generate a type alias
383                let type_str = self.type_for_shape(shape);
384                writeln!(
385                    self.output,
386                    "export type {} = {};",
387                    shape.type_identifier, type_str
388                )
389                .unwrap();
390                self.output.push('\n');
391            }
392        }
393    }
394
395    fn generate_struct(
396        &mut self,
397        exported_shape: &'static Shape,
398        field_owner_shape: &'static Shape,
399        fields: &'static [Field],
400        kind: StructKind,
401    ) {
402        match kind {
403            StructKind::Unit => {
404                // Unit struct as null
405                writeln!(
406                    self.output,
407                    "export type {} = null;",
408                    exported_shape.type_identifier
409                )
410                .unwrap();
411            }
412            StructKind::TupleStruct if fields.len() == 1 => {
413                // Newtype - type alias to inner
414                let inner_type = self.type_for_shape(fields[0].shape.get());
415                writeln!(
416                    self.output,
417                    "export type {} = {};",
418                    exported_shape.type_identifier, inner_type
419                )
420                .unwrap();
421            }
422            StructKind::TupleStruct | StructKind::Tuple => {
423                // Tuple as array type
424                let types: Vec<String> = fields
425                    .iter()
426                    .map(|f| self.type_for_shape(f.shape.get()))
427                    .collect();
428                writeln!(
429                    self.output,
430                    "export type {} = [{}];",
431                    exported_shape.type_identifier,
432                    types.join(", ")
433                )
434                .unwrap();
435            }
436            StructKind::Struct => {
437                // Empty structs should use `object` type to prevent accepting primitives
438                if !Self::has_serializable_fields(field_owner_shape, fields) {
439                    writeln!(
440                        self.output,
441                        "export type {} = object;",
442                        exported_shape.type_identifier
443                    )
444                    .unwrap();
445                } else {
446                    writeln!(
447                        self.output,
448                        "export interface {} {{",
449                        exported_shape.type_identifier
450                    )
451                    .unwrap();
452                    self.indent += 1;
453
454                    self.write_struct_fields_for_shape(field_owner_shape, fields);
455
456                    self.indent -= 1;
457                    self.output.push_str("}\n");
458                }
459            }
460        }
461        self.output.push('\n');
462    }
463
464    fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
465        // Check if all variants are unit variants (simple string union)
466        let all_unit = enum_type
467            .variants
468            .iter()
469            .all(|v| matches!(v.data.kind, StructKind::Unit));
470
471        // Check if the enum is untagged
472        let is_untagged = shape.is_untagged();
473
474        if is_untagged {
475            // Untagged enum: simple union of variant types
476            let mut variant_types = Vec::new();
477
478            for variant in enum_type.variants {
479                match variant.data.kind {
480                    StructKind::Unit => {
481                        // Unit variant in untagged enum - this is unusual but we represent as null
482                        variant_types.push("null".to_string());
483                    }
484                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
485                        // Newtype variant: just the inner type
486                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
487                        variant_types.push(inner);
488                    }
489                    StructKind::TupleStruct => {
490                        // Multi-element tuple variant: [T1, T2, ...]
491                        let types: Vec<String> = variant
492                            .data
493                            .fields
494                            .iter()
495                            .map(|f| self.type_for_shape(f.shape.get()))
496                            .collect();
497                        variant_types.push(format!("[{}]", types.join(", ")));
498                    }
499                    _ => {
500                        // Struct variant: inline object type
501                        let field_types = self.collect_inline_fields(variant.data.fields, false);
502                        variant_types.push(format!("{{ {} }}", field_types.join("; ")));
503                    }
504                }
505            }
506
507            writeln!(
508                self.output,
509                "export type {} = {};",
510                shape.type_identifier,
511                variant_types.join(" | ")
512            )
513            .unwrap();
514        } else if all_unit {
515            // Simple string literal union
516            let variants: Vec<String> = enum_type
517                .variants
518                .iter()
519                .map(|v| format!("\"{}\"", v.effective_name()))
520                .collect();
521            writeln!(
522                self.output,
523                "export type {} = {};",
524                shape.type_identifier,
525                variants.join(" | ")
526            )
527            .unwrap();
528        } else {
529            // Discriminated union
530            // Generate each variant as a separate interface, then union them
531            let mut variant_types = Vec::new();
532
533            for variant in enum_type.variants {
534                let variant_name = variant.effective_name();
535                match variant.data.kind {
536                    StructKind::Unit => {
537                        // Unit variant as object with type discriminator
538                        variant_types.push(format!("{{ {}: \"{}\" }}", variant_name, variant_name));
539                    }
540                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
541                        // Newtype variant: { VariantName: InnerType }
542                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
543                        variant_types.push(format!("{{ {}: {} }}", variant_name, inner));
544                    }
545                    StructKind::TupleStruct => {
546                        // Multi-element tuple variant: { VariantName: [T1, T2, ...] }
547                        let types: Vec<String> = variant
548                            .data
549                            .fields
550                            .iter()
551                            .map(|f| self.type_for_shape(f.shape.get()))
552                            .collect();
553                        variant_types.push(format!(
554                            "{{ {}: [{}] }}",
555                            variant_name,
556                            types.join(", ")
557                        ));
558                    }
559                    _ => {
560                        // Struct variant: { VariantName: { ...fields } }
561                        let field_types = self.collect_inline_fields(variant.data.fields, false);
562                        variant_types.push(format!(
563                            "{{ {}: {{ {} }} }}",
564                            variant_name,
565                            field_types.join("; ")
566                        ));
567                    }
568                }
569            }
570
571            writeln!(
572                self.output,
573                "export type {} =\n  | {};",
574                shape.type_identifier,
575                variant_types.join("\n  | ")
576            )
577            .unwrap();
578        }
579        self.output.push('\n');
580    }
581
582    fn type_for_shape(&mut self, shape: &'static Shape) -> String {
583        // Check Def first - these take precedence over transparent wrappers
584        match &shape.def {
585            Def::Scalar => self.scalar_type(shape),
586            Def::Option(opt) => {
587                format!("{} | null", self.type_for_shape(opt.t))
588            }
589            Def::List(list) => {
590                format!("{}[]", self.type_for_shape(list.t))
591            }
592            Def::Array(arr) => {
593                format!("{}[]", self.type_for_shape(arr.t))
594            }
595            Def::Set(set) => {
596                format!("{}[]", self.type_for_shape(set.t))
597            }
598            Def::Map(map) => {
599                format!("Record<string, {}>", self.type_for_shape(map.v))
600            }
601            Def::Pointer(ptr) => {
602                // Smart pointers are transparent
603                if let Some(pointee) = ptr.pointee {
604                    self.type_for_shape(pointee)
605                } else {
606                    "unknown".to_string()
607                }
608            }
609            Def::Undefined => {
610                // User-defined types - queue for generation and return name
611                match &shape.ty {
612                    Type::User(UserType::Struct(st)) => {
613                        // Handle tuples specially - inline them as [T1, T2, ...] since their
614                        // type_identifier "(…)" is not a valid TypeScript identifier
615                        if st.kind == StructKind::Tuple {
616                            let types: Vec<String> = st
617                                .fields
618                                .iter()
619                                .map(|f| self.type_for_shape(f.shape.get()))
620                                .collect();
621                            format!("[{}]", types.join(", "))
622                        } else {
623                            self.add_shape(shape);
624                            shape.type_identifier.to_string()
625                        }
626                    }
627                    Type::User(UserType::Enum(_)) => {
628                        self.add_shape(shape);
629                        shape.type_identifier.to_string()
630                    }
631                    _ => {
632                        // For other undefined types, check if it's a transparent wrapper
633                        if let Some(inner) = shape.inner {
634                            self.type_for_shape(inner)
635                        } else {
636                            "unknown".to_string()
637                        }
638                    }
639                }
640            }
641            _ => {
642                // For other defs, check if it's a transparent wrapper
643                if let Some(inner) = shape.inner {
644                    self.type_for_shape(inner)
645                } else {
646                    "unknown".to_string()
647                }
648            }
649        }
650    }
651
652    fn scalar_type(&self, shape: &'static Shape) -> String {
653        match shape.type_identifier {
654            // Strings
655            "String" | "str" | "&str" | "Cow" => "string".to_string(),
656
657            // Booleans
658            "bool" => "boolean".to_string(),
659
660            // Numbers (all become number in TypeScript)
661            "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
662            | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
663
664            // Char as string
665            "char" => "string".to_string(),
666
667            // Unknown scalar
668            _ => "unknown".to_string(),
669        }
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use alloc::collections::BTreeMap;
677    use facet::Facet;
678
679    #[test]
680    fn test_simple_struct() {
681        #[derive(Facet)]
682        struct User {
683            name: String,
684            age: u32,
685        }
686
687        let ts = to_typescript::<User>();
688        insta::assert_snapshot!(ts);
689    }
690
691    #[test]
692    fn test_optional_field() {
693        #[derive(Facet)]
694        struct Config {
695            required: String,
696            optional: Option<String>,
697        }
698
699        let ts = to_typescript::<Config>();
700        insta::assert_snapshot!(ts);
701    }
702
703    #[test]
704    fn test_simple_enum() {
705        #[derive(Facet)]
706        #[repr(u8)]
707        enum Status {
708            Active,
709            Inactive,
710            Pending,
711        }
712
713        let ts = to_typescript::<Status>();
714        insta::assert_snapshot!(ts);
715    }
716
717    #[test]
718    fn test_vec() {
719        #[derive(Facet)]
720        struct Data {
721            items: Vec<String>,
722        }
723
724        let ts = to_typescript::<Data>();
725        insta::assert_snapshot!(ts);
726    }
727
728    #[test]
729    fn test_nested_types() {
730        #[derive(Facet)]
731        struct Inner {
732            value: i32,
733        }
734
735        #[derive(Facet)]
736        struct Outer {
737            inner: Inner,
738            name: String,
739        }
740
741        let ts = to_typescript::<Outer>();
742        insta::assert_snapshot!(ts);
743    }
744
745    #[test]
746    fn test_enum_rename_all_snake_case() {
747        #[derive(Facet)]
748        #[facet(rename_all = "snake_case")]
749        #[repr(u8)]
750        enum ValidationErrorCode {
751            CircularDependency,
752            InvalidNaming,
753            UnknownRequirement,
754        }
755
756        let ts = to_typescript::<ValidationErrorCode>();
757        insta::assert_snapshot!(ts);
758    }
759
760    #[test]
761    fn test_enum_rename_individual() {
762        #[derive(Facet)]
763        #[repr(u8)]
764        enum GitStatus {
765            #[facet(rename = "dirty")]
766            Dirty,
767            #[facet(rename = "staged")]
768            Staged,
769            #[facet(rename = "clean")]
770            Clean,
771        }
772
773        let ts = to_typescript::<GitStatus>();
774        insta::assert_snapshot!(ts);
775    }
776
777    #[test]
778    fn test_struct_rename_all_camel_case() {
779        #[derive(Facet)]
780        #[facet(rename_all = "camelCase")]
781        struct ApiResponse {
782            user_name: String,
783            created_at: String,
784            is_active: bool,
785        }
786
787        let ts = to_typescript::<ApiResponse>();
788        insta::assert_snapshot!(ts);
789    }
790
791    #[test]
792    fn test_struct_rename_individual() {
793        #[derive(Facet)]
794        struct UserProfile {
795            #[facet(rename = "userName")]
796            user_name: String,
797            #[facet(rename = "emailAddress")]
798            email: String,
799        }
800
801        let ts = to_typescript::<UserProfile>();
802        insta::assert_snapshot!(ts);
803    }
804
805    #[test]
806    fn test_enum_with_data_rename_all() {
807        #[derive(Facet)]
808        #[facet(rename_all = "snake_case")]
809        #[repr(C)]
810        #[allow(dead_code)]
811        enum Message {
812            TextMessage { content: String },
813            ImageUpload { url: String, width: u32 },
814        }
815
816        let ts = to_typescript::<Message>();
817        insta::assert_snapshot!(ts);
818    }
819
820    #[test]
821    fn test_struct_with_tuple_field() {
822        #[derive(Facet)]
823        struct Container {
824            coordinates: (i32, i32),
825        }
826
827        let ts = to_typescript::<Container>();
828        insta::assert_snapshot!(ts);
829    }
830
831    #[test]
832    fn test_struct_with_single_element_tuple() {
833        #[derive(Facet)]
834        struct Wrapper {
835            value: (String,),
836        }
837
838        let ts = to_typescript::<Wrapper>();
839        insta::assert_snapshot!(ts);
840    }
841
842    #[test]
843    fn test_enum_with_tuple_variant() {
844        #[derive(Facet)]
845        #[repr(C)]
846        #[allow(dead_code)]
847        enum Event {
848            Click { x: i32, y: i32 },
849            Move((i32, i32)),
850            Resize { dimensions: (u32, u32) },
851        }
852
853        let ts = to_typescript::<Event>();
854        insta::assert_snapshot!(ts);
855    }
856
857    #[test]
858    fn test_untagged_enum() {
859        #[derive(Facet)]
860        #[facet(untagged)]
861        #[repr(C)]
862        #[allow(dead_code)]
863        pub enum Value {
864            Text(String),
865            Number(f64),
866        }
867
868        let ts = to_typescript::<Value>();
869        insta::assert_snapshot!(ts);
870    }
871
872    #[test]
873    fn test_untagged_enum_unit_and_struct_variants() {
874        #[derive(Facet)]
875        #[facet(untagged)]
876        #[repr(C)]
877        #[allow(dead_code)]
878        pub enum Event {
879            None,
880            Data { x: i32, y: i32 },
881        }
882
883        let ts = to_typescript::<Event>();
884        insta::assert_snapshot!(ts);
885    }
886
887    #[test]
888    fn test_enum_with_tuple_struct_variant() {
889        #[derive(Facet)]
890        #[allow(dead_code)]
891        pub struct Point {
892            x: f64,
893            y: f64,
894        }
895
896        #[derive(Facet)]
897        #[repr(u8)]
898        #[allow(dead_code)]
899        pub enum Shape {
900            Line(Point, Point),
901        }
902
903        let ts = to_typescript::<Shape>();
904        insta::assert_snapshot!(ts);
905    }
906
907    #[test]
908    fn test_enum_with_proxy_struct() {
909        #[derive(Facet)]
910        #[facet(proxy = PointProxy)]
911        #[allow(dead_code)]
912        pub struct Point {
913            xxx: f64,
914            yyy: f64,
915        }
916
917        #[derive(Facet)]
918        #[allow(dead_code)]
919        pub struct PointProxy {
920            x: f64,
921            y: f64,
922        }
923
924        impl From<PointProxy> for Point {
925            fn from(p: PointProxy) -> Self {
926                Self { xxx: p.x, yyy: p.y }
927            }
928        }
929
930        impl From<&Point> for PointProxy {
931            fn from(p: &Point) -> Self {
932                Self { x: p.xxx, y: p.yyy }
933            }
934        }
935
936        #[derive(Facet)]
937        #[repr(u8)]
938        #[facet(untagged)]
939        #[allow(dead_code)]
940        pub enum Shape {
941            Circle { center: Point, radius: f64 },
942            Line(Point, Point),
943        }
944
945        let ts = to_typescript::<Shape>();
946        insta::assert_snapshot!(ts);
947    }
948
949    #[test]
950    fn test_enum_with_proxy_enum() {
951        #[derive(Facet)]
952        #[repr(u8)]
953        #[facet(proxy = StatusProxy)]
954        pub enum Status {
955            Unknown,
956        }
957
958        #[derive(Facet)]
959        #[repr(u8)]
960        pub enum StatusProxy {
961            Active,
962            Inactive,
963        }
964
965        impl From<StatusProxy> for Status {
966            fn from(_: StatusProxy) -> Self {
967                Self::Unknown
968            }
969        }
970
971        impl From<&Status> for StatusProxy {
972            fn from(_: &Status) -> Self {
973                Self::Active
974            }
975        }
976
977        let ts = to_typescript::<Status>();
978        insta::assert_snapshot!(ts);
979    }
980
981    #[test]
982    fn test_proxy_to_scalar() {
983        /// A user ID that serializes as a string
984        #[derive(Facet)]
985        #[facet(proxy = String)]
986        #[allow(dead_code)]
987        pub struct UserId(u64);
988
989        impl From<String> for UserId {
990            fn from(s: String) -> Self {
991                Self(s.parse().unwrap_or(0))
992            }
993        }
994
995        impl From<&UserId> for String {
996            fn from(id: &UserId) -> Self {
997                id.0.to_string()
998            }
999        }
1000
1001        let ts = to_typescript::<UserId>();
1002        insta::assert_snapshot!(ts);
1003    }
1004
1005    #[test]
1006    fn test_proxy_preserves_doc_comments() {
1007        /// This is a point in 2D space.
1008        /// It has x and y coordinates.
1009        #[derive(Facet)]
1010        #[facet(proxy = PointProxy)]
1011        #[allow(dead_code)]
1012        pub struct Point {
1013            internal_x: f64,
1014            internal_y: f64,
1015        }
1016
1017        #[derive(Facet)]
1018        #[allow(dead_code)]
1019        pub struct PointProxy {
1020            x: f64,
1021            y: f64,
1022        }
1023
1024        impl From<PointProxy> for Point {
1025            fn from(p: PointProxy) -> Self {
1026                Self {
1027                    internal_x: p.x,
1028                    internal_y: p.y,
1029                }
1030            }
1031        }
1032
1033        impl From<&Point> for PointProxy {
1034            fn from(p: &Point) -> Self {
1035                Self {
1036                    x: p.internal_x,
1037                    y: p.internal_y,
1038                }
1039            }
1040        }
1041
1042        let ts = to_typescript::<Point>();
1043        insta::assert_snapshot!(ts);
1044    }
1045
1046    #[test]
1047    fn test_untagged_enum_optional_fields() {
1048        #[derive(Facet)]
1049        #[facet(untagged)]
1050        #[repr(C)]
1051        #[allow(dead_code)]
1052        pub enum Config {
1053            Simple {
1054                name: String,
1055            },
1056            Full {
1057                name: String,
1058                description: Option<String>,
1059                count: Option<u32>,
1060            },
1061        }
1062
1063        let ts = to_typescript::<Config>();
1064        insta::assert_snapshot!(ts);
1065    }
1066
1067    #[test]
1068    fn test_flatten_variants() {
1069        use std::sync::Arc;
1070
1071        // Inner struct with a skipped field to test skip handling
1072        #[derive(Facet)]
1073        pub struct Coords {
1074            pub x: i32,
1075            pub y: i32,
1076            #[facet(skip)]
1077            pub internal: u8,
1078        }
1079
1080        // Direct flatten
1081        #[derive(Facet)]
1082        pub struct FlattenDirect {
1083            pub name: String,
1084            #[facet(flatten)]
1085            pub coords: Coords,
1086        }
1087
1088        // Flatten through Arc<T>
1089        #[derive(Facet)]
1090        pub struct FlattenArc {
1091            pub name: String,
1092            #[facet(flatten)]
1093            pub coords: Arc<Coords>,
1094        }
1095
1096        // Flatten through Box<T>
1097        #[derive(Facet)]
1098        pub struct FlattenBox {
1099            pub name: String,
1100            #[facet(flatten)]
1101            pub coords: Box<Coords>,
1102        }
1103
1104        // Flatten Option<T> makes inner fields optional
1105        #[derive(Facet)]
1106        pub struct FlattenOption {
1107            pub name: String,
1108            #[facet(flatten)]
1109            pub coords: Option<Coords>,
1110        }
1111
1112        // Nested Option<Arc<T>> tests multi-layer unwrapping
1113        #[derive(Facet)]
1114        pub struct FlattenOptionArc {
1115            pub name: String,
1116            #[facet(flatten)]
1117            pub coords: Option<Arc<Coords>>,
1118        }
1119
1120        // Non-struct flatten (BTreeMap) falls through to normal field output
1121        #[derive(Facet)]
1122        pub struct FlattenMap {
1123            pub name: String,
1124            #[facet(flatten)]
1125            pub extra: BTreeMap<String, String>,
1126        }
1127
1128        let ts_direct = to_typescript::<FlattenDirect>();
1129        let ts_arc = to_typescript::<FlattenArc>();
1130        let ts_box = to_typescript::<FlattenBox>();
1131        let ts_option = to_typescript::<FlattenOption>();
1132        let ts_option_arc = to_typescript::<FlattenOptionArc>();
1133        let ts_map = to_typescript::<FlattenMap>();
1134
1135        insta::assert_snapshot!("flatten_direct", ts_direct);
1136        insta::assert_snapshot!("flatten_arc", ts_arc);
1137        insta::assert_snapshot!("flatten_box", ts_box);
1138        insta::assert_snapshot!("flatten_option", ts_option);
1139        insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
1140        insta::assert_snapshot!("flatten_map", ts_map);
1141    }
1142
1143    #[test]
1144    fn test_tagged_enum_optional_fields() {
1145        #[derive(Facet)]
1146        #[repr(u8)]
1147        #[allow(dead_code)]
1148        enum Message {
1149            Simple {
1150                text: String,
1151            },
1152            Full {
1153                text: String,
1154                metadata: Option<String>,
1155                count: Option<u32>,
1156            },
1157        }
1158
1159        let ts = to_typescript::<Message>();
1160        insta::assert_snapshot!(ts);
1161    }
1162
1163    #[test]
1164    fn test_flatten_proxy_struct() {
1165        #[derive(Facet)]
1166        #[facet(proxy = CoordsProxy)]
1167        #[allow(dead_code)]
1168        struct Coords {
1169            internal_x: f64,
1170            internal_y: f64,
1171        }
1172
1173        #[derive(Facet)]
1174        #[allow(dead_code)]
1175        struct CoordsProxy {
1176            x: f64,
1177            y: f64,
1178        }
1179
1180        impl From<CoordsProxy> for Coords {
1181            fn from(p: CoordsProxy) -> Self {
1182                Self {
1183                    internal_x: p.x,
1184                    internal_y: p.y,
1185                }
1186            }
1187        }
1188
1189        impl From<&Coords> for CoordsProxy {
1190            fn from(c: &Coords) -> Self {
1191                Self {
1192                    x: c.internal_x,
1193                    y: c.internal_y,
1194                }
1195            }
1196        }
1197
1198        #[derive(Facet)]
1199        #[allow(dead_code)]
1200        struct Shape {
1201            name: String,
1202            #[facet(flatten)]
1203            coords: Coords,
1204        }
1205
1206        let ts = to_typescript::<Shape>();
1207        insta::assert_snapshot!(ts);
1208    }
1209
1210    #[test]
1211    fn test_enum_variant_skipped_field() {
1212        #[derive(Facet)]
1213        #[repr(u8)]
1214        #[allow(dead_code)]
1215        enum Event {
1216            Data {
1217                visible: String,
1218                #[facet(skip)]
1219                internal: u64,
1220            },
1221        }
1222
1223        let ts = to_typescript::<Event>();
1224        insta::assert_snapshot!(ts);
1225    }
1226
1227    #[test]
1228    fn test_enum_variant_flatten() {
1229        // BUG: Enum struct variants should inline flattened fields
1230        #[derive(Facet)]
1231        #[allow(dead_code)]
1232        struct Metadata {
1233            author: String,
1234            version: u32,
1235        }
1236
1237        #[derive(Facet)]
1238        #[repr(u8)]
1239        #[allow(dead_code)]
1240        enum Document {
1241            Article {
1242                title: String,
1243                #[facet(flatten)]
1244                meta: Metadata,
1245            },
1246        }
1247
1248        let ts = to_typescript::<Document>();
1249        insta::assert_snapshot!(ts);
1250    }
1251
1252    #[test]
1253    fn test_nested_flatten_struct() {
1254        #[derive(Facet)]
1255        #[allow(dead_code)]
1256        struct Inner {
1257            x: i32,
1258            y: i32,
1259        }
1260
1261        #[derive(Facet)]
1262        #[allow(dead_code)]
1263        struct Middle {
1264            #[facet(flatten)]
1265            inner: Inner,
1266            z: i32,
1267        }
1268
1269        #[derive(Facet)]
1270        #[allow(dead_code)]
1271        struct Outer {
1272            name: String,
1273            #[facet(flatten)]
1274            middle: Middle,
1275        }
1276
1277        let ts = to_typescript::<Outer>();
1278        insta::assert_snapshot!(ts);
1279    }
1280
1281    #[test]
1282    fn test_flatten_recursive_option_box() {
1283        #[derive(Facet)]
1284        struct Node {
1285            value: u32,
1286            #[facet(flatten)]
1287            next: Option<Box<Node>>,
1288        }
1289
1290        let ts = to_typescript::<Node>();
1291        insta::assert_snapshot!("flatten_recursive_option_box", ts);
1292    }
1293
1294    #[test]
1295    fn test_skip_serializing_struct_field() {
1296        #[derive(Facet)]
1297        struct Data {
1298            visible: String,
1299            #[facet(skip_serializing)]
1300            internal: u64,
1301        }
1302
1303        let ts = to_typescript::<Data>();
1304        insta::assert_snapshot!("skip_serializing_struct_field", ts);
1305    }
1306
1307    #[test]
1308    fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
1309        #[derive(Facet)]
1310        struct Node {
1311            value: u32,
1312            #[facet(flatten)]
1313            next: Option<Box<Node>>,
1314        }
1315
1316        #[derive(Facet)]
1317        #[repr(u8)]
1318        enum Wrapper {
1319            Item {
1320                #[facet(flatten)]
1321                node: Node,
1322            },
1323            Data {
1324                visible: String,
1325                #[facet(skip_serializing)]
1326                internal: u64,
1327            },
1328        }
1329
1330        let item = Wrapper::Item {
1331            node: Node {
1332                value: 1,
1333                next: None,
1334            },
1335        };
1336        match item {
1337            Wrapper::Item { node } => assert_eq!(node.value, 1),
1338            Wrapper::Data { .. } => unreachable!(),
1339        }
1340
1341        let data = Wrapper::Data {
1342            visible: String::new(),
1343            internal: 0,
1344        };
1345        match data {
1346            Wrapper::Data { visible, internal } => {
1347                assert!(visible.is_empty());
1348                assert_eq!(internal, 0);
1349            }
1350            Wrapper::Item { .. } => unreachable!(),
1351        }
1352
1353        let ts = to_typescript::<Wrapper>();
1354        insta::assert_snapshot!(
1355            "skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
1356            ts
1357        );
1358    }
1359
1360    #[test]
1361    fn test_empty_struct() {
1362        #[derive(Facet)]
1363        struct Data {
1364            empty: Empty,
1365        }
1366
1367        #[derive(Facet)]
1368        struct Empty {}
1369
1370        let e = to_typescript::<Empty>();
1371        let d = to_typescript::<Data>();
1372        insta::assert_snapshot!("test_empty_struct", e);
1373        insta::assert_snapshot!("test_empty_struct_wrap", d);
1374    }
1375
1376    #[test]
1377    fn test_empty_struct_with_skipped_fields() {
1378        #[derive(Facet)]
1379        struct EmptyAfterSkip {
1380            #[facet(skip_serializing)]
1381            internal: String,
1382        }
1383
1384        let ts = to_typescript::<EmptyAfterSkip>();
1385        insta::assert_snapshot!("test_empty_struct_with_skipped_fields", ts);
1386    }
1387
1388    #[test]
1389    fn test_empty_struct_multiple_references() {
1390        #[derive(Facet)]
1391        struct Container {
1392            first: Empty,
1393            second: Empty,
1394            third: Option<Empty>,
1395        }
1396
1397        #[derive(Facet)]
1398        struct Empty {}
1399
1400        let ts = to_typescript::<Container>();
1401        insta::assert_snapshot!("test_empty_struct_multiple_references", ts);
1402    }
1403
1404    #[test]
1405    fn test_flatten_empty_struct() {
1406        #[derive(Facet)]
1407        struct Empty {}
1408
1409        #[derive(Facet)]
1410        struct Wrapper {
1411            #[facet(flatten)]
1412            empty: Empty,
1413        }
1414
1415        let ts = to_typescript::<Wrapper>();
1416        insta::assert_snapshot!("test_flatten_empty_struct", ts);
1417    }
1418
1419    #[test]
1420    fn test_default_not_required() {
1421        #[derive(Facet, Default)]
1422        struct Def {
1423            pub a: i32,
1424            pub b: i32,
1425        }
1426
1427        #[derive(Facet)]
1428        struct Wrapper {
1429            pub a: String,
1430            #[facet(default)]
1431            pub d: Def,
1432        }
1433
1434        let ts = to_typescript::<Wrapper>();
1435        insta::assert_snapshot!("test_default_not_required", ts);
1436    }
1437
1438    #[test]
1439    fn test_default_mixed_fields() {
1440        #[derive(Facet)]
1441        struct MixedDefaults {
1442            pub required: String,
1443            pub optional: Option<String>,
1444            #[facet(default)]
1445            pub with_default: i32,
1446            #[facet(default = 100)]
1447            pub with_default_expr: i32,
1448            #[facet(default)]
1449            pub option_with_default: Option<String>,
1450        }
1451
1452        let ts = to_typescript::<MixedDefaults>();
1453        insta::assert_snapshot!("test_default_mixed_fields", ts);
1454    }
1455
1456    #[test]
1457    fn test_default_in_flattened_struct() {
1458        #[derive(Facet)]
1459        struct FlattenedInner {
1460            pub foo: String,
1461            #[facet(default)]
1462            pub bar: u32,
1463        }
1464
1465        #[derive(Facet)]
1466        struct WithFlatten {
1467            pub outer_field: String,
1468            #[facet(flatten)]
1469            pub inner: FlattenedInner,
1470        }
1471
1472        let ts = to_typescript::<WithFlatten>();
1473        insta::assert_snapshot!("test_default_in_flattened_struct", ts);
1474    }
1475
1476    #[test]
1477    fn test_default_in_enum_variant() {
1478        #[derive(Facet)]
1479        #[allow(dead_code)]
1480        #[repr(C)]
1481        enum Message {
1482            Text {
1483                content: String,
1484            },
1485            Data {
1486                required: String,
1487                #[facet(default)]
1488                optional: i32,
1489            },
1490        }
1491
1492        let ts = to_typescript::<Message>();
1493        insta::assert_snapshot!("test_default_in_enum_variant", ts);
1494    }
1495}