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 | StructKind::Tuple => {
413                // Tuple as array type
414                let types: Vec<String> = fields
415                    .iter()
416                    .map(|f| self.type_for_shape(f.shape.get()))
417                    .collect();
418                writeln!(
419                    self.output,
420                    "export type {} = [{}];",
421                    exported_shape.type_identifier,
422                    types.join(", ")
423                )
424                .unwrap();
425            }
426            StructKind::Struct => {
427                // Empty structs should use `object` type to prevent accepting primitives
428                if !Self::has_serializable_fields(field_owner_shape, fields) {
429                    writeln!(
430                        self.output,
431                        "export type {} = object;",
432                        exported_shape.type_identifier
433                    )
434                    .unwrap();
435                } else {
436                    writeln!(
437                        self.output,
438                        "export interface {} {{",
439                        exported_shape.type_identifier
440                    )
441                    .unwrap();
442                    self.indent += 1;
443
444                    self.write_struct_fields_for_shape(field_owner_shape, fields);
445
446                    self.indent -= 1;
447                    self.output.push_str("}\n");
448                }
449            }
450        }
451        self.output.push('\n');
452    }
453
454    fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
455        // Check if all variants are unit variants (simple string union)
456        let all_unit = enum_type
457            .variants
458            .iter()
459            .all(|v| matches!(v.data.kind, StructKind::Unit));
460
461        // Check if the enum is untagged
462        let is_untagged = shape.is_untagged();
463
464        if is_untagged {
465            // Untagged enum: simple union of variant types
466            let mut variant_types = Vec::new();
467
468            for variant in enum_type.variants {
469                match variant.data.kind {
470                    StructKind::Unit => {
471                        // Unit variant in untagged enum - serializes as variant name string
472                        let variant_name = variant.effective_name();
473                        variant_types.push(format!("\"{}\"", variant_name));
474                    }
475                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
476                        // Newtype variant: just the inner type
477                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
478                        variant_types.push(inner);
479                    }
480                    StructKind::TupleStruct => {
481                        // Multi-element tuple variant: [T1, T2, ...]
482                        let types: Vec<String> = variant
483                            .data
484                            .fields
485                            .iter()
486                            .map(|f| self.type_for_shape(f.shape.get()))
487                            .collect();
488                        variant_types.push(format!("[{}]", types.join(", ")));
489                    }
490                    _ => {
491                        // Struct variant: inline object type
492                        let field_types = self.collect_inline_fields(variant.data.fields, false);
493                        variant_types.push(format!("{{ {} }}", field_types.join("; ")));
494                    }
495                }
496            }
497
498            writeln!(
499                self.output,
500                "export type {} = {};",
501                shape.type_identifier,
502                variant_types.join(" | ")
503            )
504            .unwrap();
505        } else if all_unit {
506            // Simple string literal union
507            let variants: Vec<String> = enum_type
508                .variants
509                .iter()
510                .map(|v| format!("\"{}\"", v.effective_name()))
511                .collect();
512            writeln!(
513                self.output,
514                "export type {} = {};",
515                shape.type_identifier,
516                variants.join(" | ")
517            )
518            .unwrap();
519        } else {
520            // Discriminated union
521            // Generate each variant as a separate interface, then union them
522            let mut variant_types = Vec::new();
523
524            for variant in enum_type.variants {
525                let variant_name = variant.effective_name();
526                match variant.data.kind {
527                    StructKind::Unit => {
528                        // Unit variant serializes as bare string, even in tagged enums.
529                        variant_types.push(format!("\"{}\"", variant_name));
530                    }
531                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
532                        // Newtype variant: { VariantName: InnerType }
533                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
534                        variant_types.push(format!("{{ {}: {} }}", variant_name, inner));
535                    }
536                    StructKind::TupleStruct => {
537                        // Multi-element tuple variant: { VariantName: [T1, T2, ...] }
538                        let types: Vec<String> = variant
539                            .data
540                            .fields
541                            .iter()
542                            .map(|f| self.type_for_shape(f.shape.get()))
543                            .collect();
544                        variant_types.push(format!(
545                            "{{ {}: [{}] }}",
546                            variant_name,
547                            types.join(", ")
548                        ));
549                    }
550                    _ => {
551                        // Struct variant: { VariantName: { ...fields } }
552                        let field_types = self.collect_inline_fields(variant.data.fields, false);
553                        variant_types.push(format!(
554                            "{{ {}: {{ {} }} }}",
555                            variant_name,
556                            field_types.join("; ")
557                        ));
558                    }
559                }
560            }
561
562            writeln!(
563                self.output,
564                "export type {} =\n  | {};",
565                shape.type_identifier,
566                variant_types.join("\n  | ")
567            )
568            .unwrap();
569        }
570        self.output.push('\n');
571    }
572
573    fn type_for_shape(&mut self, shape: &'static Shape) -> String {
574        // Check Def first - these take precedence over transparent wrappers
575        match &shape.def {
576            Def::Scalar => self.scalar_type(shape),
577            Def::Option(opt) => {
578                format!("{} | null", self.type_for_shape(opt.t))
579            }
580            Def::List(list) => {
581                format!("{}[]", self.type_for_shape(list.t))
582            }
583            Def::Array(arr) => {
584                format!("{}[]", self.type_for_shape(arr.t))
585            }
586            Def::Set(set) => {
587                format!("{}[]", self.type_for_shape(set.t))
588            }
589            Def::Map(map) => {
590                format!("Record<string, {}>", self.type_for_shape(map.v))
591            }
592            Def::Pointer(ptr) => {
593                // Smart pointers are transparent
594                if let Some(pointee) = ptr.pointee {
595                    self.type_for_shape(pointee)
596                } else {
597                    "unknown".to_string()
598                }
599            }
600            Def::Undefined => {
601                // User-defined types - queue for generation and return name
602                match &shape.ty {
603                    Type::User(UserType::Struct(st)) => {
604                        // Handle tuples specially - inline them as [T1, T2, ...] since their
605                        // type_identifier "(…)" is not a valid TypeScript identifier
606                        if st.kind == StructKind::Tuple {
607                            let types: Vec<String> = st
608                                .fields
609                                .iter()
610                                .map(|f| self.type_for_shape(f.shape.get()))
611                                .collect();
612                            format!("[{}]", types.join(", "))
613                        } else {
614                            self.add_shape(shape);
615                            shape.type_identifier.to_string()
616                        }
617                    }
618                    Type::User(UserType::Enum(_)) => {
619                        self.add_shape(shape);
620                        shape.type_identifier.to_string()
621                    }
622                    _ => {
623                        // For other undefined types, check if it's a transparent wrapper
624                        if let Some(inner) = shape.inner {
625                            self.type_for_shape(inner)
626                        } else {
627                            "unknown".to_string()
628                        }
629                    }
630                }
631            }
632            _ => {
633                // For other defs, check if it's a transparent wrapper
634                if let Some(inner) = shape.inner {
635                    self.type_for_shape(inner)
636                } else {
637                    "unknown".to_string()
638                }
639            }
640        }
641    }
642
643    fn scalar_type(&self, shape: &'static Shape) -> String {
644        match shape.type_identifier {
645            // Strings
646            "String" | "str" | "&str" | "Cow" => "string".to_string(),
647
648            // Booleans
649            "bool" => "boolean".to_string(),
650
651            // Numbers (all become number in TypeScript)
652            "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
653            | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
654
655            // Char as string
656            "char" => "string".to_string(),
657
658            // chrono types
659            "NaiveDate"
660            | "NaiveDateTime"
661            | "NaiveTime"
662            | "DateTime<Utc>"
663            | "DateTime<FixedOffset>"
664            | "DateTime<Local>"
665                if shape.module_path == Some("chrono") =>
666            {
667                "string".to_string()
668            }
669
670            // Unknown scalar
671            _ => "unknown".to_string(),
672        }
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use alloc::collections::BTreeMap;
680    use facet::Facet;
681
682    #[test]
683    fn test_simple_struct() {
684        #[derive(Facet)]
685        struct User {
686            name: String,
687            age: u32,
688        }
689
690        let ts = to_typescript::<User>();
691        insta::assert_snapshot!(ts);
692    }
693
694    #[test]
695    fn test_optional_field() {
696        #[derive(Facet)]
697        struct Config {
698            required: String,
699            optional: Option<String>,
700        }
701
702        let ts = to_typescript::<Config>();
703        insta::assert_snapshot!(ts);
704    }
705
706    #[test]
707    fn test_simple_enum() {
708        #[derive(Facet)]
709        #[repr(u8)]
710        enum Status {
711            Active,
712            Inactive,
713            Pending,
714        }
715
716        let ts = to_typescript::<Status>();
717        insta::assert_snapshot!(ts);
718    }
719
720    #[test]
721    fn test_vec() {
722        #[derive(Facet)]
723        struct Data {
724            items: Vec<String>,
725        }
726
727        let ts = to_typescript::<Data>();
728        insta::assert_snapshot!(ts);
729    }
730
731    #[test]
732    fn test_nested_types() {
733        #[derive(Facet)]
734        struct Inner {
735            value: i32,
736        }
737
738        #[derive(Facet)]
739        struct Outer {
740            inner: Inner,
741            name: String,
742        }
743
744        let ts = to_typescript::<Outer>();
745        insta::assert_snapshot!(ts);
746    }
747
748    #[test]
749    fn test_enum_rename_all_snake_case() {
750        #[derive(Facet)]
751        #[facet(rename_all = "snake_case")]
752        #[repr(u8)]
753        enum ValidationErrorCode {
754            CircularDependency,
755            InvalidNaming,
756            UnknownRequirement,
757        }
758
759        let ts = to_typescript::<ValidationErrorCode>();
760        insta::assert_snapshot!(ts);
761    }
762
763    #[test]
764    fn test_enum_rename_individual() {
765        #[derive(Facet)]
766        #[repr(u8)]
767        enum GitStatus {
768            #[facet(rename = "dirty")]
769            Dirty,
770            #[facet(rename = "staged")]
771            Staged,
772            #[facet(rename = "clean")]
773            Clean,
774        }
775
776        let ts = to_typescript::<GitStatus>();
777        insta::assert_snapshot!(ts);
778    }
779
780    #[test]
781    fn test_struct_rename_all_camel_case() {
782        #[derive(Facet)]
783        #[facet(rename_all = "camelCase")]
784        struct ApiResponse {
785            user_name: String,
786            created_at: String,
787            is_active: bool,
788        }
789
790        let ts = to_typescript::<ApiResponse>();
791        insta::assert_snapshot!(ts);
792    }
793
794    #[test]
795    fn test_struct_rename_individual() {
796        #[derive(Facet)]
797        struct UserProfile {
798            #[facet(rename = "userName")]
799            user_name: String,
800            #[facet(rename = "emailAddress")]
801            email: String,
802        }
803
804        let ts = to_typescript::<UserProfile>();
805        insta::assert_snapshot!(ts);
806    }
807
808    #[test]
809    fn test_enum_with_data_rename_all() {
810        #[derive(Facet)]
811        #[facet(rename_all = "snake_case")]
812        #[repr(C)]
813        #[allow(dead_code)]
814        enum Message {
815            TextMessage { content: String },
816            ImageUpload { url: String, width: u32 },
817        }
818
819        let ts = to_typescript::<Message>();
820        insta::assert_snapshot!(ts);
821    }
822
823    #[test]
824    fn test_tagged_enum_unit_and_data_variants() {
825        #[derive(Facet)]
826        #[facet(rename_all = "snake_case")]
827        #[repr(u8)]
828        #[allow(dead_code)]
829        enum ResponseStatus {
830            Pending,
831            Ok(String),
832            Error { message: String },
833            Cancelled,
834        }
835
836        let ts = to_typescript::<ResponseStatus>();
837        insta::assert_snapshot!("tagged_enum_unit_and_data_variants", ts);
838    }
839
840    #[test]
841    fn test_struct_with_tuple_field() {
842        #[derive(Facet)]
843        struct Container {
844            coordinates: (i32, i32),
845        }
846
847        let ts = to_typescript::<Container>();
848        insta::assert_snapshot!(ts);
849    }
850
851    #[test]
852    fn test_struct_with_single_element_tuple() {
853        #[derive(Facet)]
854        struct Wrapper {
855            value: (String,),
856        }
857
858        let ts = to_typescript::<Wrapper>();
859        insta::assert_snapshot!(ts);
860    }
861
862    #[test]
863    fn test_enum_with_tuple_variant() {
864        #[derive(Facet)]
865        #[repr(C)]
866        #[allow(dead_code)]
867        enum Event {
868            Click { x: i32, y: i32 },
869            Move((i32, i32)),
870            Resize { dimensions: (u32, u32) },
871        }
872
873        let ts = to_typescript::<Event>();
874        insta::assert_snapshot!(ts);
875    }
876
877    #[test]
878    fn test_untagged_enum() {
879        #[derive(Facet)]
880        #[facet(untagged)]
881        #[repr(C)]
882        #[allow(dead_code)]
883        pub enum Value {
884            Text(String),
885            Number(f64),
886        }
887
888        let ts = to_typescript::<Value>();
889        insta::assert_snapshot!(ts);
890    }
891
892    #[test]
893    fn test_untagged_enum_unit_and_struct_variants() {
894        #[derive(Facet)]
895        #[facet(untagged)]
896        #[repr(C)]
897        #[allow(dead_code)]
898        pub enum Event {
899            None,
900            Data { x: i32, y: i32 },
901        }
902
903        let ts = to_typescript::<Event>();
904        insta::assert_snapshot!(ts);
905    }
906
907    #[test]
908    fn test_enum_with_tuple_struct_variant() {
909        #[derive(Facet)]
910        #[allow(dead_code)]
911        pub struct Point {
912            x: f64,
913            y: f64,
914        }
915
916        #[derive(Facet)]
917        #[repr(u8)]
918        #[allow(dead_code)]
919        pub enum Shape {
920            Line(Point, Point),
921        }
922
923        let ts = to_typescript::<Shape>();
924        insta::assert_snapshot!(ts);
925    }
926
927    #[test]
928    fn test_enum_with_proxy_struct() {
929        #[derive(Facet)]
930        #[facet(proxy = PointProxy)]
931        #[allow(dead_code)]
932        pub struct Point {
933            xxx: f64,
934            yyy: f64,
935        }
936
937        #[derive(Facet)]
938        #[allow(dead_code)]
939        pub struct PointProxy {
940            x: f64,
941            y: f64,
942        }
943
944        impl From<PointProxy> for Point {
945            fn from(p: PointProxy) -> Self {
946                Self { xxx: p.x, yyy: p.y }
947            }
948        }
949
950        impl From<&Point> for PointProxy {
951            fn from(p: &Point) -> Self {
952                Self { x: p.xxx, y: p.yyy }
953            }
954        }
955
956        #[derive(Facet)]
957        #[repr(u8)]
958        #[facet(untagged)]
959        #[allow(dead_code)]
960        pub enum Shape {
961            Circle { center: Point, radius: f64 },
962            Line(Point, Point),
963        }
964
965        let ts = to_typescript::<Shape>();
966        insta::assert_snapshot!(ts);
967    }
968
969    #[test]
970    fn test_enum_with_proxy_enum() {
971        #[derive(Facet)]
972        #[repr(u8)]
973        #[facet(proxy = StatusProxy)]
974        pub enum Status {
975            Unknown,
976        }
977
978        #[derive(Facet)]
979        #[repr(u8)]
980        pub enum StatusProxy {
981            Active,
982            Inactive,
983        }
984
985        impl From<StatusProxy> for Status {
986            fn from(_: StatusProxy) -> Self {
987                Self::Unknown
988            }
989        }
990
991        impl From<&Status> for StatusProxy {
992            fn from(_: &Status) -> Self {
993                Self::Active
994            }
995        }
996
997        let ts = to_typescript::<Status>();
998        insta::assert_snapshot!(ts);
999    }
1000
1001    #[test]
1002    fn test_proxy_to_scalar() {
1003        /// A user ID that serializes as a string
1004        #[derive(Facet)]
1005        #[facet(proxy = String)]
1006        #[allow(dead_code)]
1007        pub struct UserId(u64);
1008
1009        impl From<String> for UserId {
1010            fn from(s: String) -> Self {
1011                Self(s.parse().unwrap_or(0))
1012            }
1013        }
1014
1015        impl From<&UserId> for String {
1016            fn from(id: &UserId) -> Self {
1017                id.0.to_string()
1018            }
1019        }
1020
1021        let ts = to_typescript::<UserId>();
1022        insta::assert_snapshot!(ts);
1023    }
1024
1025    #[test]
1026    fn test_proxy_preserves_doc_comments() {
1027        /// This is a point in 2D space.
1028        /// It has x and y coordinates.
1029        #[derive(Facet)]
1030        #[facet(proxy = PointProxy)]
1031        #[allow(dead_code)]
1032        pub struct Point {
1033            internal_x: f64,
1034            internal_y: f64,
1035        }
1036
1037        #[derive(Facet)]
1038        #[allow(dead_code)]
1039        pub struct PointProxy {
1040            x: f64,
1041            y: f64,
1042        }
1043
1044        impl From<PointProxy> for Point {
1045            fn from(p: PointProxy) -> Self {
1046                Self {
1047                    internal_x: p.x,
1048                    internal_y: p.y,
1049                }
1050            }
1051        }
1052
1053        impl From<&Point> for PointProxy {
1054            fn from(p: &Point) -> Self {
1055                Self {
1056                    x: p.internal_x,
1057                    y: p.internal_y,
1058                }
1059            }
1060        }
1061
1062        let ts = to_typescript::<Point>();
1063        insta::assert_snapshot!(ts);
1064    }
1065
1066    #[test]
1067    fn test_untagged_enum_optional_fields() {
1068        #[derive(Facet)]
1069        #[facet(untagged)]
1070        #[repr(C)]
1071        #[allow(dead_code)]
1072        pub enum Config {
1073            Simple {
1074                name: String,
1075            },
1076            Full {
1077                name: String,
1078                description: Option<String>,
1079                count: Option<u32>,
1080            },
1081        }
1082
1083        let ts = to_typescript::<Config>();
1084        insta::assert_snapshot!(ts);
1085    }
1086
1087    #[test]
1088    fn test_flatten_variants() {
1089        use std::sync::Arc;
1090
1091        // Inner struct with a skipped field to test skip handling
1092        #[derive(Facet)]
1093        pub struct Coords {
1094            pub x: i32,
1095            pub y: i32,
1096            #[facet(skip)]
1097            pub internal: u8,
1098        }
1099
1100        // Direct flatten
1101        #[derive(Facet)]
1102        pub struct FlattenDirect {
1103            pub name: String,
1104            #[facet(flatten)]
1105            pub coords: Coords,
1106        }
1107
1108        // Flatten through Arc<T>
1109        #[derive(Facet)]
1110        pub struct FlattenArc {
1111            pub name: String,
1112            #[facet(flatten)]
1113            pub coords: Arc<Coords>,
1114        }
1115
1116        // Flatten through Box<T>
1117        #[derive(Facet)]
1118        pub struct FlattenBox {
1119            pub name: String,
1120            #[facet(flatten)]
1121            pub coords: Box<Coords>,
1122        }
1123
1124        // Flatten Option<T> makes inner fields optional
1125        #[derive(Facet)]
1126        pub struct FlattenOption {
1127            pub name: String,
1128            #[facet(flatten)]
1129            pub coords: Option<Coords>,
1130        }
1131
1132        // Nested Option<Arc<T>> tests multi-layer unwrapping
1133        #[derive(Facet)]
1134        pub struct FlattenOptionArc {
1135            pub name: String,
1136            #[facet(flatten)]
1137            pub coords: Option<Arc<Coords>>,
1138        }
1139
1140        // Non-struct flatten (BTreeMap) falls through to normal field output
1141        #[derive(Facet)]
1142        pub struct FlattenMap {
1143            pub name: String,
1144            #[facet(flatten)]
1145            pub extra: BTreeMap<String, String>,
1146        }
1147
1148        let ts_direct = to_typescript::<FlattenDirect>();
1149        let ts_arc = to_typescript::<FlattenArc>();
1150        let ts_box = to_typescript::<FlattenBox>();
1151        let ts_option = to_typescript::<FlattenOption>();
1152        let ts_option_arc = to_typescript::<FlattenOptionArc>();
1153        let ts_map = to_typescript::<FlattenMap>();
1154
1155        insta::assert_snapshot!("flatten_direct", ts_direct);
1156        insta::assert_snapshot!("flatten_arc", ts_arc);
1157        insta::assert_snapshot!("flatten_box", ts_box);
1158        insta::assert_snapshot!("flatten_option", ts_option);
1159        insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
1160        insta::assert_snapshot!("flatten_map", ts_map);
1161    }
1162
1163    #[test]
1164    fn test_tagged_enum_optional_fields() {
1165        #[derive(Facet)]
1166        #[repr(u8)]
1167        #[allow(dead_code)]
1168        enum Message {
1169            Simple {
1170                text: String,
1171            },
1172            Full {
1173                text: String,
1174                metadata: Option<String>,
1175                count: Option<u32>,
1176            },
1177        }
1178
1179        let ts = to_typescript::<Message>();
1180        insta::assert_snapshot!(ts);
1181    }
1182
1183    #[test]
1184    fn test_flatten_proxy_struct() {
1185        #[derive(Facet)]
1186        #[facet(proxy = CoordsProxy)]
1187        #[allow(dead_code)]
1188        struct Coords {
1189            internal_x: f64,
1190            internal_y: f64,
1191        }
1192
1193        #[derive(Facet)]
1194        #[allow(dead_code)]
1195        struct CoordsProxy {
1196            x: f64,
1197            y: f64,
1198        }
1199
1200        impl From<CoordsProxy> for Coords {
1201            fn from(p: CoordsProxy) -> Self {
1202                Self {
1203                    internal_x: p.x,
1204                    internal_y: p.y,
1205                }
1206            }
1207        }
1208
1209        impl From<&Coords> for CoordsProxy {
1210            fn from(c: &Coords) -> Self {
1211                Self {
1212                    x: c.internal_x,
1213                    y: c.internal_y,
1214                }
1215            }
1216        }
1217
1218        #[derive(Facet)]
1219        #[allow(dead_code)]
1220        struct Shape {
1221            name: String,
1222            #[facet(flatten)]
1223            coords: Coords,
1224        }
1225
1226        let ts = to_typescript::<Shape>();
1227        insta::assert_snapshot!(ts);
1228    }
1229
1230    #[test]
1231    fn test_enum_variant_skipped_field() {
1232        #[derive(Facet)]
1233        #[repr(u8)]
1234        #[allow(dead_code)]
1235        enum Event {
1236            Data {
1237                visible: String,
1238                #[facet(skip)]
1239                internal: u64,
1240            },
1241        }
1242
1243        let ts = to_typescript::<Event>();
1244        insta::assert_snapshot!(ts);
1245    }
1246
1247    #[test]
1248    fn test_enum_variant_flatten() {
1249        // BUG: Enum struct variants should inline flattened fields
1250        #[derive(Facet)]
1251        #[allow(dead_code)]
1252        struct Metadata {
1253            author: String,
1254            version: u32,
1255        }
1256
1257        #[derive(Facet)]
1258        #[repr(u8)]
1259        #[allow(dead_code)]
1260        enum Document {
1261            Article {
1262                title: String,
1263                #[facet(flatten)]
1264                meta: Metadata,
1265            },
1266        }
1267
1268        let ts = to_typescript::<Document>();
1269        insta::assert_snapshot!(ts);
1270    }
1271
1272    #[test]
1273    fn test_nested_flatten_struct() {
1274        #[derive(Facet)]
1275        #[allow(dead_code)]
1276        struct Inner {
1277            x: i32,
1278            y: i32,
1279        }
1280
1281        #[derive(Facet)]
1282        #[allow(dead_code)]
1283        struct Middle {
1284            #[facet(flatten)]
1285            inner: Inner,
1286            z: i32,
1287        }
1288
1289        #[derive(Facet)]
1290        #[allow(dead_code)]
1291        struct Outer {
1292            name: String,
1293            #[facet(flatten)]
1294            middle: Middle,
1295        }
1296
1297        let ts = to_typescript::<Outer>();
1298        insta::assert_snapshot!(ts);
1299    }
1300
1301    #[test]
1302    fn test_flatten_recursive_option_box() {
1303        #[derive(Facet)]
1304        struct Node {
1305            value: u32,
1306            #[facet(flatten)]
1307            next: Option<Box<Node>>,
1308        }
1309
1310        let ts = to_typescript::<Node>();
1311        insta::assert_snapshot!("flatten_recursive_option_box", ts);
1312    }
1313
1314    #[test]
1315    fn test_skip_serializing_struct_field() {
1316        #[derive(Facet)]
1317        struct Data {
1318            visible: String,
1319            #[facet(skip_serializing)]
1320            internal: u64,
1321        }
1322
1323        let ts = to_typescript::<Data>();
1324        insta::assert_snapshot!("skip_serializing_struct_field", ts);
1325    }
1326
1327    #[test]
1328    fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
1329        #[derive(Facet)]
1330        struct Node {
1331            value: u32,
1332            #[facet(flatten)]
1333            next: Option<Box<Node>>,
1334        }
1335
1336        #[derive(Facet)]
1337        #[repr(u8)]
1338        enum Wrapper {
1339            Item {
1340                #[facet(flatten)]
1341                node: Node,
1342            },
1343            Data {
1344                visible: String,
1345                #[facet(skip_serializing)]
1346                internal: u64,
1347            },
1348        }
1349
1350        let item = Wrapper::Item {
1351            node: Node {
1352                value: 1,
1353                next: None,
1354            },
1355        };
1356        match item {
1357            Wrapper::Item { node } => assert_eq!(node.value, 1),
1358            Wrapper::Data { .. } => unreachable!(),
1359        }
1360
1361        let data = Wrapper::Data {
1362            visible: String::new(),
1363            internal: 0,
1364        };
1365        match data {
1366            Wrapper::Data { visible, internal } => {
1367                assert!(visible.is_empty());
1368                assert_eq!(internal, 0);
1369            }
1370            Wrapper::Item { .. } => unreachable!(),
1371        }
1372
1373        let ts = to_typescript::<Wrapper>();
1374        insta::assert_snapshot!(
1375            "skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
1376            ts
1377        );
1378    }
1379
1380    #[test]
1381    fn test_empty_struct() {
1382        #[derive(Facet)]
1383        struct Data {
1384            empty: Empty,
1385        }
1386
1387        #[derive(Facet)]
1388        struct Empty {}
1389
1390        let e = to_typescript::<Empty>();
1391        let d = to_typescript::<Data>();
1392        insta::assert_snapshot!("test_empty_struct", e);
1393        insta::assert_snapshot!("test_empty_struct_wrap", d);
1394    }
1395
1396    #[test]
1397    fn test_empty_struct_with_skipped_fields() {
1398        #[derive(Facet)]
1399        struct EmptyAfterSkip {
1400            #[facet(skip_serializing)]
1401            internal: String,
1402        }
1403
1404        let ts = to_typescript::<EmptyAfterSkip>();
1405        insta::assert_snapshot!("test_empty_struct_with_skipped_fields", ts);
1406    }
1407
1408    #[test]
1409    fn test_empty_struct_multiple_references() {
1410        #[derive(Facet)]
1411        struct Container {
1412            first: Empty,
1413            second: Empty,
1414            third: Option<Empty>,
1415        }
1416
1417        #[derive(Facet)]
1418        struct Empty {}
1419
1420        let ts = to_typescript::<Container>();
1421        insta::assert_snapshot!("test_empty_struct_multiple_references", ts);
1422    }
1423
1424    #[test]
1425    fn test_flatten_empty_struct() {
1426        #[derive(Facet)]
1427        struct Empty {}
1428
1429        #[derive(Facet)]
1430        struct Wrapper {
1431            #[facet(flatten)]
1432            empty: Empty,
1433        }
1434
1435        let ts = to_typescript::<Wrapper>();
1436        insta::assert_snapshot!("test_flatten_empty_struct", ts);
1437    }
1438
1439    #[test]
1440    fn test_default_not_required() {
1441        #[derive(Facet, Default)]
1442        struct Def {
1443            pub a: i32,
1444            pub b: i32,
1445        }
1446
1447        #[derive(Facet)]
1448        struct Wrapper {
1449            pub a: String,
1450            #[facet(default)]
1451            pub d: Def,
1452        }
1453
1454        let ts = to_typescript::<Wrapper>();
1455        insta::assert_snapshot!("test_default_not_required", ts);
1456    }
1457
1458    #[test]
1459    fn test_default_mixed_fields() {
1460        #[derive(Facet)]
1461        struct MixedDefaults {
1462            pub required: String,
1463            pub optional: Option<String>,
1464            #[facet(default)]
1465            pub with_default: i32,
1466            #[facet(default = 100)]
1467            pub with_default_expr: i32,
1468            #[facet(default)]
1469            pub option_with_default: Option<String>,
1470        }
1471
1472        let ts = to_typescript::<MixedDefaults>();
1473        insta::assert_snapshot!("test_default_mixed_fields", ts);
1474    }
1475
1476    #[test]
1477    fn test_default_in_flattened_struct() {
1478        #[derive(Facet)]
1479        struct FlattenedInner {
1480            pub foo: String,
1481            #[facet(default)]
1482            pub bar: u32,
1483        }
1484
1485        #[derive(Facet)]
1486        struct WithFlatten {
1487            pub outer_field: String,
1488            #[facet(flatten)]
1489            pub inner: FlattenedInner,
1490        }
1491
1492        let ts = to_typescript::<WithFlatten>();
1493        insta::assert_snapshot!("test_default_in_flattened_struct", ts);
1494    }
1495
1496    #[test]
1497    fn test_default_in_enum_variant() {
1498        #[derive(Facet)]
1499        #[allow(dead_code)]
1500        #[repr(C)]
1501        enum Message {
1502            Text {
1503                content: String,
1504            },
1505            Data {
1506                required: String,
1507                #[facet(default)]
1508                optional: i32,
1509            },
1510        }
1511
1512        let ts = to_typescript::<Message>();
1513        insta::assert_snapshot!("test_default_in_enum_variant", ts);
1514    }
1515
1516    #[test]
1517    fn test_untagged_enum_unit_and_newtype_variants() {
1518        #[derive(Facet, Clone, PartialEq, PartialOrd)]
1519        #[repr(C)]
1520        #[allow(dead_code)]
1521        #[facet(untagged)]
1522        pub enum Enum {
1523            Daily,
1524            Weekly,
1525            Custom(f64),
1526        }
1527
1528        let ts = to_typescript::<Enum>();
1529        insta::assert_snapshot!("test_untagged_enum_unit_and_newtype_variants", ts);
1530    }
1531
1532    #[test]
1533    fn test_untagged_enum_with_tuple_variant() {
1534        #[derive(Facet)]
1535        #[repr(C)]
1536        #[allow(dead_code)]
1537        #[facet(untagged)]
1538        pub enum Message {
1539            Text(String),
1540            Pair(String, i32),
1541            Struct { x: i32, y: i32 },
1542        }
1543
1544        let ts = to_typescript::<Message>();
1545        insta::assert_snapshot!("test_untagged_enum_with_tuple_variant", ts);
1546    }
1547    #[test]
1548    fn test_chrono_naive_date() {
1549        use chrono::NaiveDate;
1550
1551        #[derive(Facet)]
1552        struct WithChronoDate {
1553            birthday: NaiveDate,
1554        }
1555
1556        let ts = to_typescript::<WithChronoDate>();
1557        insta::assert_snapshot!(ts);
1558    }
1559
1560    #[test]
1561    fn test_non_transparent_newtype_is_not_scalar_alias() {
1562        #[derive(Facet)]
1563        struct Envelope {
1564            id: BacktraceId,
1565        }
1566
1567        #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1568        struct BacktraceId(u64);
1569
1570        let mut ts_gen = TypeScriptGenerator::new();
1571        ts_gen.add_type::<Envelope>();
1572        let out = ts_gen.finish();
1573
1574        assert!(
1575            !out.contains("export type BacktraceId = number;"),
1576            "bug: non-transparent tuple newtype generated scalar alias:\n{out}"
1577        );
1578        insta::assert_snapshot!("non_transparent_newtype", out);
1579    }
1580
1581    #[test]
1582    fn test_transparent_newtype_is_scalar_alias() {
1583        #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1584        #[facet(transparent)]
1585        struct TransparentId(u64);
1586
1587        let ts = to_typescript::<TransparentId>();
1588        assert!(
1589            ts.contains("export type TransparentId = number;"),
1590            "bug: transparent tuple newtype did not generate scalar alias:\n{ts}"
1591        );
1592        insta::assert_snapshot!("transparent_newtype", ts);
1593    }
1594}