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