Skip to main content

facet_python/
lib.rs

1//! Generate Python type definitions from facet type metadata.
2//!
3//! This crate uses facet's reflection capabilities to generate Python
4//! type hints and TypedDicts from any type that implements `Facet`.
5//!
6//! # Example
7//!
8//! ```
9//! use facet::Facet;
10//! use facet_python::to_python;
11//!
12//! #[derive(Facet)]
13//! struct User {
14//!     name: String,
15//!     age: u32,
16//!     email: Option<String>,
17//! }
18//!
19//! let py = to_python::<User>(false);
20//! assert!(py.contains("class User(TypedDict"));
21//! ```
22
23extern crate alloc;
24
25use alloc::collections::{BTreeMap, 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/// Check if a field name is a Python reserved keyword using binary search
33fn is_python_keyword(name: &str) -> bool {
34    // Python reserved keywords - MUST be sorted alphabetically for binary search
35    const KEYWORDS: &[&str] = &[
36        "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
37        "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
38        "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
39        "try", "while", "with", "yield",
40    ];
41    KEYWORDS.binary_search(&name).is_ok()
42}
43
44/// A field in a TypedDict, used for shared generation logic.
45struct TypedDictField<'a> {
46    name: &'a str,
47    type_string: String,
48    required: bool,
49    doc: &'a [&'a str],
50}
51
52impl<'a> TypedDictField<'a> {
53    fn new(name: &'a str, type_string: String, required: bool, doc: &'a [&'a str]) -> Self {
54        Self {
55            name,
56            type_string,
57            required,
58            doc,
59        }
60    }
61
62    /// Get the full type string with Required[] wrapper if needed
63    fn full_type_string(&self) -> String {
64        if self.required {
65            format!("Required[{}]", self.type_string)
66        } else {
67            self.type_string.clone()
68        }
69    }
70}
71
72/// Check if any field has a name that is a Python reserved keyword
73fn has_reserved_keyword_field(fields: &[TypedDictField]) -> bool {
74    fields.iter().any(|f| is_python_keyword(f.name))
75}
76
77/// Generate TypedDict using functional syntax: `Name = TypedDict("Name", {...}, total=False)`
78fn write_typed_dict_functional(output: &mut String, class_name: &str, fields: &[TypedDictField]) {
79    writeln!(output, "{} = TypedDict(", class_name).unwrap();
80    writeln!(output, "    \"{}\",", class_name).unwrap();
81    output.push_str("    {");
82
83    let mut first = true;
84    for field in fields {
85        if !first {
86            output.push_str(", ");
87        }
88        first = false;
89
90        write!(output, "\"{}\": {}", field.name, field.full_type_string()).unwrap();
91    }
92
93    output.push_str("},\n");
94    output.push_str("    total=False,\n");
95    output.push(')');
96}
97
98/// Generate TypedDict using class syntax: `class Name(TypedDict, total=False): ...`
99fn write_typed_dict_class(output: &mut String, class_name: &str, fields: &[TypedDictField]) {
100    writeln!(output, "class {}(TypedDict, total=False):", class_name).unwrap();
101
102    if fields.is_empty() {
103        output.push_str("    pass");
104        return;
105    }
106
107    for field in fields {
108        // Generate doc comment for field
109        for line in field.doc {
110            output.push_str("    #");
111            output.push_str(line);
112            output.push('\n');
113        }
114
115        writeln!(output, "    {}: {}", field.name, field.full_type_string()).unwrap();
116    }
117}
118
119/// Generate a TypedDict, choosing between class and functional syntax.
120fn write_typed_dict(output: &mut String, class_name: &str, fields: &[TypedDictField]) {
121    if has_reserved_keyword_field(fields) {
122        write_typed_dict_functional(output, class_name, fields);
123    } else {
124        write_typed_dict_class(output, class_name, fields);
125    }
126}
127
128/// Generate Python definitions for a single type.
129pub fn to_python<T: Facet<'static>>(write_imports: bool) -> String {
130    let mut generator = PythonGenerator::new();
131    generator.add_shape(T::SHAPE);
132    generator.finish(write_imports)
133}
134
135/// Generator for Python type definitions.
136pub struct PythonGenerator {
137    /// Generated type definitions, keyed by type name for sorting
138    generated: BTreeMap<String, String>,
139    /// Types queued for generation
140    queue: Vec<&'static Shape>,
141    /// Typing imports used (Any, Literal, Required, TypedDict)
142    imports: BTreeSet<&'static str>,
143}
144
145impl Default for PythonGenerator {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151impl PythonGenerator {
152    /// Create a new Python generator.
153    pub const fn new() -> Self {
154        Self {
155            generated: BTreeMap::new(),
156            queue: Vec::new(),
157            imports: BTreeSet::new(),
158        }
159    }
160
161    /// Add a type to generate.
162    pub fn add_type<T: Facet<'static>>(&mut self) {
163        self.add_shape(T::SHAPE);
164    }
165
166    /// Add a shape to generate.
167    pub fn add_shape(&mut self, shape: &'static Shape) {
168        if !self.generated.contains_key(shape.type_identifier) {
169            self.queue.push(shape);
170        }
171    }
172
173    /// Finish generation and return the Python code.
174    pub fn finish(mut self, write_imports: bool) -> String {
175        // Process queue until empty
176        while let Some(shape) = self.queue.pop() {
177            if self.generated.contains_key(shape.type_identifier) {
178                continue;
179            }
180            // Insert a placeholder to mark as "being generated"
181            self.generated
182                .insert(shape.type_identifier.to_string(), String::new());
183            self.generate_shape(shape);
184        }
185
186        // Collect all generated code in sorted order (BTreeMap iterates in key order)
187        // Invariant: we must generate in lexia order to ensure that forward references are quoted correctly
188        let mut output = String::new();
189
190        // Write imports if requested
191        if write_imports {
192            // Always emit __future__ annotations for postponed evaluation
193            // This allows forward references and | syntax without runtime issues
194            writeln!(output, "from __future__ import annotations").unwrap();
195
196            if !self.imports.is_empty() {
197                let imports: Vec<&str> = self.imports.iter().copied().collect();
198                writeln!(output, "from typing import {}", imports.join(", ")).unwrap();
199            }
200            output.push('\n');
201        }
202
203        for code in self.generated.values() {
204            output.push_str(code);
205        }
206        output
207    }
208
209    fn generate_shape(&mut self, shape: &'static Shape) {
210        let mut output = String::new();
211
212        // Handle transparent wrappers — generate a type alias to the inner type.
213        if let Some(inner) = shape.inner {
214            self.add_shape(inner);
215            let inner_type = self.type_for_shape(inner, None);
216            write_doc_comment(&mut output, shape.doc);
217            writeln!(output, "type {} = {}", shape.type_identifier, inner_type).unwrap();
218            output.push('\n');
219            self.generated
220                .insert(shape.type_identifier.to_string(), output);
221            return;
222        }
223
224        // Handle proxy types — use the proxy's wire format but keep the original
225        // type's name and doc comment. Mirrors facet-typescript's approach.
226        if let Some(proxy_def) = shape.proxy {
227            let proxy_shape = proxy_def.shape;
228            match &proxy_shape.ty {
229                Type::User(UserType::Struct(st)) => {
230                    // Proxy is a struct: generate TypedDict using the proxy's fields.
231                    // Doc comment is written by generate_struct → generate_typed_dict.
232                    self.generate_struct(&mut output, shape, st.fields, st.kind);
233                }
234                Type::User(UserType::Enum(en)) => {
235                    // Proxy is an enum: use proxy_shape's tag/untagged attributes so
236                    // the generated union matches the actual wire format.
237                    // Write doc comment here — the enum sub-generators don't.
238                    write_doc_comment(&mut output, shape.doc);
239                    let all_unit = en
240                        .variants
241                        .iter()
242                        .all(|v| matches!(v.data.kind, StructKind::Unit));
243                    if let Some(tag_key) = proxy_shape.tag {
244                        self.generate_enum_internally_tagged(&mut output, shape, en, tag_key);
245                    } else if proxy_shape.is_untagged() {
246                        self.generate_enum_untagged(&mut output, shape, en);
247                    } else if all_unit {
248                        self.generate_enum_unit_variants(&mut output, shape, en);
249                    } else {
250                        self.generate_enum_with_data(&mut output, shape, en);
251                    }
252                    output.push('\n');
253                }
254                _ => {
255                    // Scalar or other proxy type: generate a type alias.
256                    // Write doc comment here — the type alias path doesn't.
257                    write_doc_comment(&mut output, shape.doc);
258                    let proxy_type = self.type_for_shape(proxy_shape, None);
259                    writeln!(output, "type {} = {}", shape.type_identifier, proxy_type).unwrap();
260                    output.push('\n');
261                }
262            }
263            self.generated
264                .insert(shape.type_identifier.to_string(), output);
265            return;
266        }
267
268        match &shape.ty {
269            Type::User(UserType::Struct(st)) => {
270                self.generate_struct(&mut output, shape, st.fields, st.kind);
271            }
272            Type::User(UserType::Enum(en)) => {
273                self.generate_enum(&mut output, shape, en);
274            }
275            _ => {
276                // For other types, generate a type alias
277                let type_str = self.type_for_shape(shape, None);
278                write_doc_comment(&mut output, shape.doc);
279                writeln!(output, "type {} = {}", shape.type_identifier, type_str).unwrap();
280                output.push('\n');
281            }
282        }
283
284        self.generated
285            .insert(shape.type_identifier.to_string(), output);
286    }
287
288    fn generate_struct(
289        &mut self,
290        output: &mut String,
291        shape: &'static Shape,
292        fields: &'static [Field],
293        kind: StructKind,
294    ) {
295        match kind {
296            StructKind::Unit => {
297                write_doc_comment(output, shape.doc);
298                writeln!(output, "{} = None", shape.type_identifier).unwrap();
299            }
300            StructKind::TupleStruct | StructKind::Tuple if fields.is_empty() => {
301                // Empty tuple struct like `struct Empty();` - treat like unit struct
302                write_doc_comment(output, shape.doc);
303                writeln!(output, "{} = None", shape.type_identifier).unwrap();
304            }
305            StructKind::TupleStruct if fields.len() == 1 => {
306                let inner_type = self.type_for_shape(fields[0].shape.get(), None);
307                write_doc_comment(output, shape.doc);
308                writeln!(output, "{} = {}", shape.type_identifier, inner_type).unwrap();
309            }
310            StructKind::TupleStruct | StructKind::Tuple => {
311                let types: Vec<String> = fields
312                    .iter()
313                    .map(|f| self.type_for_shape(f.shape.get(), None))
314                    .collect();
315                write_doc_comment(output, shape.doc);
316                writeln!(
317                    output,
318                    "{} = tuple[{}]",
319                    shape.type_identifier,
320                    types.join(", ")
321                )
322                .unwrap();
323            }
324            StructKind::Struct => {
325                self.generate_typed_dict(output, shape, fields);
326            }
327        }
328        output.push('\n');
329    }
330
331    /// Generate a TypedDict for a struct, choosing between class and functional syntax.
332    fn generate_typed_dict(
333        &mut self,
334        output: &mut String,
335        shape: &'static Shape,
336        fields: &'static [Field],
337    ) {
338        self.imports.insert("TypedDict");
339
340        // Collect fields, recursively inlining any #[facet(flatten)] fields.
341        let all_fields = self.collect_flat_fields(fields);
342
343        // Partition: separate flattened tagged-enum fields from everything else.
344        // A flattened tagged-enum field cannot be expressed as a single TypedDict
345        // key — it causes the entire parent to expand into a per-variant union.
346        let mut base_fields: Vec<(&'static Field, bool)> = Vec::new();
347        let mut tag_enum_flattens: Vec<(&'static facet_core::EnumType, &'static str)> = Vec::new();
348
349        for &(field, force_optional) in &all_fields {
350            if field.is_flattened() {
351                let (inner, _) = Self::unwrap_to_inner_shape(field.shape.get());
352                if let (Type::User(UserType::Enum(en)), Some(tag)) = (&inner.ty, inner.tag) {
353                    tag_enum_flattens.push((en, tag));
354                    continue;
355                }
356            }
357            base_fields.push((field, force_optional));
358        }
359
360        if !tag_enum_flattens.is_empty() {
361            self.generate_struct_as_tagged_union(output, shape, &base_fields, &tag_enum_flattens);
362            return;
363        }
364
365        // Normal path: emit a single TypedDict class.
366        let needs_functional = all_fields
367            .iter()
368            .any(|(f, _)| is_python_keyword(f.effective_name()));
369        let quote_after: Option<&str> = if needs_functional {
370            Some(shape.type_identifier)
371        } else {
372            None
373        };
374
375        // Convert to TypedDictField for shared generation logic
376        let typed_dict_fields: Vec<_> = all_fields
377            .iter()
378            .map(|(f, force_optional)| {
379                let (type_string, required) = self.field_type_info(f, quote_after);
380                let required = required && !force_optional;
381                TypedDictField::new(f.effective_name(), type_string, required, f.doc)
382            })
383            .collect();
384
385        // Track Required import if any field needs it
386        if typed_dict_fields.iter().any(|f| f.required) {
387            self.imports.insert("Required");
388        }
389
390        write_doc_comment(output, shape.doc);
391        write_typed_dict(output, shape.type_identifier, &typed_dict_fields);
392    }
393
394    /// Generate a struct that flattens a `#[facet(tag = "...")]` enum.
395    ///
396    /// Because the tag field and variant fields are merged directly into the parent
397    /// JSON object, the parent struct becomes a union of per-variant TypedDicts.
398    /// Each variant TypedDict is named `ParentNameVariantName` and contains:
399    ///   - all base fields of the parent (those not involved in the tagged flatten)
400    ///   - a `Required[Literal["VariantName"]]` tag field
401    ///   - the variant's own fields
402    fn generate_struct_as_tagged_union(
403        &mut self,
404        output: &mut String,
405        shape: &'static Shape,
406        base_fields: &[(&'static Field, bool)],
407        tag_enum_flattens: &[(&'static facet_core::EnumType, &'static str)],
408    ) {
409        self.imports.insert("TypedDict");
410        self.imports.insert("Required");
411        self.imports.insert("Literal");
412
413        let parent_name = shape.type_identifier;
414        let mut variant_class_names: Vec<String> = Vec::new();
415
416        for &(enum_type, tag_key) in tag_enum_flattens {
417            for variant in enum_type.variants {
418                let variant_name = variant.effective_name();
419                let class_name = format!("{}{}", parent_name, to_pascal_case(variant_name));
420
421                let mut fields: Vec<TypedDictField> = Vec::new();
422
423                // Parent's own base fields come first.
424                for &(f, force_optional) in base_fields {
425                    let (type_string, required) = self.field_type_info(f, None);
426                    let required = required && !force_optional;
427                    fields.push(TypedDictField::new(
428                        f.effective_name(),
429                        type_string,
430                        required,
431                        f.doc,
432                    ));
433                }
434
435                // Tag discriminator field.
436                let tag_type = format!("Literal[\"{}\"]", variant_name);
437                fields.push(TypedDictField::new(tag_key, tag_type, true, &[]));
438
439                // Variant-specific fields.
440                match variant.data.kind {
441                    StructKind::Unit => {
442                        // Only the tag field — no additional data.
443                    }
444                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
445                        let inner_shape = variant.data.fields[0].shape.get();
446                        let (resolved, _) = Self::unwrap_to_inner_shape(inner_shape);
447                        if let Type::User(UserType::Struct(st)) = &resolved.ty {
448                            // Inner type is a struct: inline its fields directly.
449                            self.add_shape(resolved);
450                            for f in st.fields {
451                                if f.should_skip_serializing_unconditional() {
452                                    continue;
453                                }
454                                let (type_string, required) = self.field_type_info(f, None);
455                                fields.push(TypedDictField::new(
456                                    f.effective_name(),
457                                    type_string,
458                                    required,
459                                    f.doc,
460                                ));
461                            }
462                        } else {
463                            // Primitive or collection: wrap in a "value" key.
464                            let inner_type = self.type_for_shape(inner_shape, None);
465                            fields.push(TypedDictField::new("value", inner_type, true, &[]));
466                        }
467                    }
468                    StructKind::TupleStruct => {
469                        let types: Vec<String> = variant
470                            .data
471                            .fields
472                            .iter()
473                            .map(|f| self.type_for_shape(f.shape.get(), None))
474                            .collect();
475                        let inner_type = format!("tuple[{}]", types.join(", "));
476                        fields.push(TypedDictField::new("value", inner_type, true, &[]));
477                    }
478                    _ => {
479                        // Struct variant: inline fields alongside the tag.
480                        for f in variant.data.fields {
481                            if f.should_skip_serializing_unconditional() {
482                                continue;
483                            }
484                            let (type_string, required) = self.field_type_info(f, None);
485                            fields.push(TypedDictField::new(
486                                f.effective_name(),
487                                type_string,
488                                required,
489                                f.doc,
490                            ));
491                        }
492                    }
493                }
494
495                let mut class_output = String::new();
496                write_typed_dict(&mut class_output, &class_name, &fields);
497                class_output.push('\n');
498                self.generated.insert(class_name.clone(), class_output);
499                variant_class_names.push(class_name);
500            }
501        }
502
503        write_doc_comment(output, shape.doc);
504        writeln!(
505            output,
506            "type {} = {}",
507            parent_name,
508            variant_class_names.join(" | ")
509        )
510        .unwrap();
511    }
512
513    /// Unwrap through `Option<T>`, pointers (`Arc<T>`, `Box<T>`), and transparent
514    /// wrappers to reach the effective inner shape for flatten purposes.
515    ///
516    /// Returns `(inner_shape, was_optional)` where `was_optional` is `true` if an
517    /// `Option` layer was encountered.
518    fn unwrap_to_inner_shape(shape: &'static Shape) -> (&'static Shape, bool) {
519        // Option<T> — mark optional and recurse on T.
520        if let Def::Option(opt) = &shape.def {
521            let (inner, _) = Self::unwrap_to_inner_shape(opt.t);
522            return (inner, true);
523        }
524        // Arc<T>, Box<T>, etc. — unwrap the pointee.
525        if let Def::Pointer(ptr) = &shape.def
526            && let Some(pointee) = ptr.pointee
527        {
528            return Self::unwrap_to_inner_shape(pointee);
529        }
530        // Transparent wrappers (#[facet(transparent)]).
531        if let Some(inner) = shape.inner {
532            let (inner_shape, is_optional) = Self::unwrap_to_inner_shape(inner);
533            return (inner_shape, is_optional);
534        }
535        // Proxy types — follow the proxy chain.
536        if let Some(proxy_def) = shape.proxy {
537            return Self::unwrap_to_inner_shape(proxy_def.shape);
538        }
539        (shape, false)
540    }
541
542    /// Collect visible fields, inlining `#[facet(flatten)]` ones.
543    ///
544    /// Each entry is `(field, force_optional)`. `force_optional` is `true` when the
545    /// field was inlined from an `Option<Struct>` flatten, meaning the child field
546    /// must be treated as optional in the parent regardless of its own shape.
547    fn collect_flat_fields(&mut self, fields: &'static [Field]) -> Vec<(&'static Field, bool)> {
548        let mut flatten_stack: Vec<&'static str> = Vec::new();
549        self.collect_flat_fields_guarded(fields, false, &mut flatten_stack)
550    }
551
552    fn collect_flat_fields_guarded(
553        &mut self,
554        fields: &'static [Field],
555        force_optional: bool,
556        flatten_stack: &mut Vec<&'static str>,
557    ) -> Vec<(&'static Field, bool)> {
558        let mut result = Vec::new();
559        for field in fields {
560            // Covers both #[facet(skip)] and #[facet(skip_serializing)].
561            if field.should_skip_serializing_unconditional() {
562                continue;
563            }
564
565            if field.is_flattened() {
566                // Unwrap Option/pointer/transparent layers to reach the struct shape.
567                let (inner_shape, parent_is_optional) =
568                    Self::unwrap_to_inner_shape(field.shape.get());
569
570                // Queue the struct itself (not any Option/pointer wrapper) so it
571                // gets its own TypedDict if referenced elsewhere.
572                self.add_shape(inner_shape);
573
574                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
575                    // Cycle guard: skip self-referential shapes.
576                    let key = inner_shape.type_identifier;
577                    if flatten_stack.contains(&key) {
578                        continue;
579                    }
580                    flatten_stack.push(key);
581                    let inner = self.collect_flat_fields_guarded(
582                        st.fields,
583                        force_optional || parent_is_optional,
584                        flatten_stack,
585                    );
586                    result.extend(inner);
587                    flatten_stack.pop();
588                } else {
589                    // Non-struct flatten (e.g. a map) — emit as a regular field.
590                    result.push((field, force_optional));
591                }
592            } else {
593                result.push((field, force_optional));
594            }
595        }
596        result
597    }
598
599    /// Get the Python type string and required status for a field.
600    fn field_type_info(&mut self, field: &Field, quote_after: Option<&str>) -> (String, bool) {
601        if let Def::Option(opt) = &field.shape.get().def {
602            (self.type_for_shape(opt.t, quote_after), false)
603        } else {
604            // Fields with a default value are optional in JSON — facet fills in
605            // the default when the key is absent. Matches facet-typescript behaviour.
606            let required = field.default.is_none();
607            (
608                self.type_for_shape(field.shape.get(), quote_after),
609                required,
610            )
611        }
612    }
613
614    fn generate_enum(
615        &mut self,
616        output: &mut String,
617        shape: &'static Shape,
618        enum_type: &facet_core::EnumType,
619    ) {
620        let all_unit = enum_type
621            .variants
622            .iter()
623            .all(|v| matches!(v.data.kind, StructKind::Unit));
624
625        write_doc_comment(output, shape.doc);
626
627        if let Some(tag_key) = shape.tag {
628            self.generate_enum_internally_tagged(output, shape, enum_type, tag_key);
629        } else if shape.is_untagged() {
630            self.generate_enum_untagged(output, shape, enum_type);
631        } else if all_unit {
632            self.generate_enum_unit_variants(output, shape, enum_type);
633        } else {
634            self.generate_enum_with_data(output, shape, enum_type);
635        }
636        output.push('\n');
637    }
638
639    /// Generate an internally-tagged enum (`#[facet(tag = "key")]`).
640    ///
641    /// Each variant becomes a named TypedDict `EnumNameVariantName` containing
642    /// a `Required[Literal["VariantName"]]` tag field plus the variant's own fields.
643    fn generate_enum_internally_tagged(
644        &mut self,
645        output: &mut String,
646        shape: &'static Shape,
647        enum_type: &facet_core::EnumType,
648        tag_key: &'static str,
649    ) {
650        self.imports.insert("TypedDict");
651        self.imports.insert("Required");
652        self.imports.insert("Literal");
653
654        let enum_name = shape.type_identifier;
655        let mut variant_class_names: Vec<String> = Vec::new();
656
657        for variant in enum_type.variants {
658            let variant_name = variant.effective_name();
659            let class_name = format!("{}{}", enum_name, to_pascal_case(variant_name));
660            let tag_type = format!("Literal[\"{}\"]", variant_name);
661
662            let mut fields: Vec<TypedDictField> = Vec::new();
663            // Tag field always comes first.
664            fields.push(TypedDictField::new(tag_key, tag_type, true, &[]));
665
666            match variant.data.kind {
667                StructKind::Unit => {
668                    // Only the tag field — no additional data.
669                }
670                StructKind::TupleStruct if variant.data.fields.len() == 1 => {
671                    let inner_shape = variant.data.fields[0].shape.get();
672                    let (resolved, _) = Self::unwrap_to_inner_shape(inner_shape);
673                    if let Type::User(UserType::Struct(st)) = &resolved.ty {
674                        // Inner type is a struct: inline its fields directly, matching
675                        // facet-json which merges struct fields at the same level as the tag.
676                        self.add_shape(resolved);
677                        for f in st.fields {
678                            if f.should_skip_serializing_unconditional() {
679                                continue;
680                            }
681                            let (type_string, required) = self.field_type_info(f, None);
682                            fields.push(TypedDictField::new(
683                                f.effective_name(),
684                                type_string,
685                                required,
686                                f.doc,
687                            ));
688                        }
689                    } else {
690                        // Primitive or collection: wrap in a "value" key.
691                        let inner_type = self.type_for_shape(inner_shape, None);
692                        fields.push(TypedDictField::new("value", inner_type, true, &[]));
693                    }
694                }
695                StructKind::TupleStruct => {
696                    // Multi-field tuple: tag + tuple payload under a "value" key.
697                    let types: Vec<String> = variant
698                        .data
699                        .fields
700                        .iter()
701                        .map(|f| self.type_for_shape(f.shape.get(), None))
702                        .collect();
703                    let inner_type = format!("tuple[{}]", types.join(", "));
704                    fields.push(TypedDictField::new("value", inner_type, true, &[]));
705                }
706                _ => {
707                    // Struct variant: inline all fields alongside the tag.
708                    for field in variant.data.fields {
709                        if field.should_skip_serializing_unconditional() {
710                            continue;
711                        }
712                        let (type_string, required) = self.field_type_info(field, None);
713                        fields.push(TypedDictField::new(
714                            field.effective_name(),
715                            type_string,
716                            required,
717                            field.doc,
718                        ));
719                    }
720                }
721            }
722
723            let mut class_output = String::new();
724            write_typed_dict(&mut class_output, &class_name, &fields);
725            class_output.push('\n');
726            self.generated.insert(class_name.clone(), class_output);
727            variant_class_names.push(class_name);
728        }
729
730        writeln!(
731            output,
732            "type {} = {}",
733            enum_name,
734            variant_class_names.join(" | ")
735        )
736        .unwrap();
737    }
738
739    /// Generate an untagged enum (`#[facet(untagged)]`).
740    ///
741    /// Each variant serializes purely by content with no discriminator:
742    ///   - Unit variant    => `Literal["VariantName"]`
743    ///   - Newtype variant => the inner Python type directly
744    ///   - Struct variant  => a named TypedDict `EnumNameVariantName`
745    fn generate_enum_untagged(
746        &mut self,
747        output: &mut String,
748        shape: &'static Shape,
749        enum_type: &facet_core::EnumType,
750    ) {
751        self.imports.insert("Literal");
752
753        let enum_name = shape.type_identifier;
754        let mut variant_types: Vec<String> = Vec::new();
755
756        for variant in enum_type.variants {
757            let variant_name = variant.effective_name();
758
759            match variant.data.kind {
760                StructKind::Unit => {
761                    variant_types.push(format!("Literal[\"{}\"]", variant_name));
762                }
763                StructKind::TupleStruct if variant.data.fields.len() == 1 => {
764                    // Newtype: the inner type directly, no wrapper.
765                    let inner = self.type_for_shape(variant.data.fields[0].shape.get(), None);
766                    variant_types.push(inner);
767                }
768                StructKind::TupleStruct => {
769                    // Multi-field tuple: tuple[T1, T2, ...]
770                    let types: Vec<String> = variant
771                        .data
772                        .fields
773                        .iter()
774                        .map(|f| self.type_for_shape(f.shape.get(), None))
775                        .collect();
776                    variant_types.push(format!("tuple[{}]", types.join(", ")));
777                }
778                _ => {
779                    // Struct variant: generate a named TypedDict.
780                    self.imports.insert("TypedDict");
781                    self.imports.insert("Required");
782                    let class_name = format!("{}{}", enum_name, to_pascal_case(variant_name));
783                    let typed_dict_fields: Vec<TypedDictField> = variant
784                        .data
785                        .fields
786                        .iter()
787                        .filter(|f| !f.should_skip_serializing_unconditional())
788                        .map(|f| {
789                            let (type_string, required) = self.field_type_info(f, None);
790                            TypedDictField::new(f.effective_name(), type_string, required, f.doc)
791                        })
792                        .collect();
793                    let mut class_output = String::new();
794                    write_typed_dict(&mut class_output, &class_name, &typed_dict_fields);
795                    class_output.push('\n');
796                    self.generated.insert(class_name.clone(), class_output);
797                    variant_types.push(class_name);
798                }
799            }
800        }
801
802        writeln!(output, "type {} = {}", enum_name, variant_types.join(" | ")).unwrap();
803    }
804
805    /// Generate a simple enum where all variants are unit variants.
806    fn generate_enum_unit_variants(
807        &mut self,
808        output: &mut String,
809        shape: &'static Shape,
810        enum_type: &facet_core::EnumType,
811    ) {
812        self.imports.insert("Literal");
813
814        let variants: Vec<String> = enum_type
815            .variants
816            .iter()
817            .map(|v| format!("Literal[\"{}\"]", v.effective_name()))
818            .collect();
819
820        writeln!(
821            output,
822            "type {} = {}",
823            shape.type_identifier,
824            variants.join(" | ")
825        )
826        .unwrap();
827    }
828
829    /// Generate an enum with data variants (discriminated union).
830    fn generate_enum_with_data(
831        &mut self,
832        output: &mut String,
833        shape: &'static Shape,
834        enum_type: &facet_core::EnumType,
835    ) {
836        let mut variant_class_names = Vec::new();
837
838        for variant in enum_type.variants {
839            let variant_type_name = self.generate_enum_variant(variant);
840            variant_class_names.push(variant_type_name);
841        }
842
843        writeln!(
844            output,
845            "type {} = {}",
846            shape.type_identifier,
847            variant_class_names.join(" | ")
848        )
849        .unwrap();
850    }
851
852    /// Generate a single enum variant and return its type reference.
853    fn generate_enum_variant(&mut self, variant: &facet_core::Variant) -> String {
854        let variant_name = variant.effective_name();
855        let pascal_variant_name = to_pascal_case(variant_name);
856
857        match variant.data.kind {
858            StructKind::Unit => {
859                self.imports.insert("Literal");
860                format!("Literal[\"{}\"]", variant_name)
861            }
862            StructKind::TupleStruct if variant.data.fields.len() == 1 => {
863                self.generate_newtype_variant(variant_name, &pascal_variant_name, variant);
864                pascal_variant_name.to_string()
865            }
866            StructKind::TupleStruct => {
867                self.generate_tuple_variant(variant_name, &pascal_variant_name, variant);
868                pascal_variant_name.to_string()
869            }
870            _ => {
871                self.generate_struct_variant(variant_name, &pascal_variant_name, variant);
872                pascal_variant_name.to_string()
873            }
874        }
875    }
876
877    /// Generate a newtype variant (single-field tuple variant).
878    fn generate_newtype_variant(
879        &mut self,
880        variant_name: &str,
881        pascal_variant_name: &str,
882        variant: &facet_core::Variant,
883    ) {
884        self.imports.insert("TypedDict");
885        self.imports.insert("Required");
886
887        // Functional form uses runtime expressions — quote forward references.
888        let quote_after: Option<&str> = if is_python_keyword(variant_name) {
889            Some(pascal_variant_name)
890        } else {
891            None
892        };
893
894        let inner_type = self.type_for_shape(variant.data.fields[0].shape.get(), quote_after);
895
896        let fields = [TypedDictField::new(variant_name, inner_type, true, &[])];
897
898        let mut output = String::new();
899        write_typed_dict(&mut output, pascal_variant_name, &fields);
900        output.push('\n');
901
902        self.generated
903            .insert(pascal_variant_name.to_string(), output);
904    }
905
906    /// Generate a tuple variant (multiple fields).
907    fn generate_tuple_variant(
908        &mut self,
909        variant_name: &str,
910        pascal_variant_name: &str,
911        variant: &facet_core::Variant,
912    ) {
913        self.imports.insert("TypedDict");
914        self.imports.insert("Required");
915
916        // Functional form uses runtime expressions — quote forward references.
917        let quote_after: Option<&str> = if is_python_keyword(variant_name) {
918            Some(pascal_variant_name)
919        } else {
920            None
921        };
922
923        let types: Vec<String> = variant
924            .data
925            .fields
926            .iter()
927            .map(|f| self.type_for_shape(f.shape.get(), quote_after))
928            .collect();
929
930        // Note: types should never be empty here because:
931        // - Single-field tuple structs are handled by generate_newtype_variant
932        // - Zero-field tuple variants (e.g., A()) fail to compile in the derive macro
933        let inner_type = format!("tuple[{}]", types.join(", "));
934
935        let fields = [TypedDictField::new(variant_name, inner_type, true, &[])];
936
937        let mut output = String::new();
938        write_typed_dict(&mut output, pascal_variant_name, &fields);
939        output.push('\n');
940
941        self.generated
942            .insert(pascal_variant_name.to_string(), output);
943    }
944
945    /// Generate a struct variant (multiple fields or named fields).
946    fn generate_struct_variant(
947        &mut self,
948        variant_name: &str,
949        pascal_variant_name: &str,
950        variant: &facet_core::Variant,
951    ) {
952        self.imports.insert("TypedDict");
953        self.imports.insert("Required");
954
955        let data_class_name = format!("{}Data", pascal_variant_name);
956
957        // Functional form uses runtime expressions — quote forward references.
958        let needs_functional = variant
959            .data
960            .fields
961            .iter()
962            .any(|f| is_python_keyword(f.effective_name()));
963        let quote_after: Option<&str> = if needs_functional {
964            Some(&data_class_name)
965        } else {
966            None
967        };
968
969        // Generate the data class fields
970        let data_fields: Vec<_> = variant
971            .data
972            .fields
973            .iter()
974            .map(|field| {
975                let field_type = self.type_for_shape(field.shape.get(), quote_after);
976                TypedDictField::new(field.effective_name(), field_type, true, &[])
977            })
978            .collect();
979
980        let mut data_output = String::new();
981        write_typed_dict(&mut data_output, &data_class_name, &data_fields);
982        data_output.push('\n');
983        self.generated.insert(data_class_name.clone(), data_output);
984
985        // Quote data_class_name if wrapper will use functional form (forward ref).
986        let wrapper_type_str =
987            if is_python_keyword(variant_name) && data_class_name.as_str() > pascal_variant_name {
988                format!("\"{}\"", data_class_name)
989            } else {
990                data_class_name.clone()
991            };
992        let wrapper_fields = [TypedDictField::new(
993            variant_name,
994            wrapper_type_str,
995            true,
996            &[],
997        )];
998
999        let mut wrapper_output = String::new();
1000        write_typed_dict(&mut wrapper_output, pascal_variant_name, &wrapper_fields);
1001        wrapper_output.push('\n');
1002
1003        self.generated
1004            .insert(pascal_variant_name.to_string(), wrapper_output);
1005    }
1006
1007    /// Get the Python type string for a shape.
1008    /// `quote_after` quotes user-defined names sorting after it (forward refs).
1009    fn type_for_shape(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
1010        // Check Def first - these take precedence over transparent wrappers
1011        match &shape.def {
1012            Def::Scalar => self.scalar_type(shape),
1013            Def::Option(opt) => {
1014                format!("{} | None", self.type_for_shape(opt.t, quote_after))
1015            }
1016            Def::List(list) => {
1017                format!("list[{}]", self.type_for_shape(list.t, quote_after))
1018            }
1019            Def::Array(arr) => {
1020                format!("list[{}]", self.type_for_shape(arr.t, quote_after))
1021            }
1022            Def::Set(set) => {
1023                format!("list[{}]", self.type_for_shape(set.t, quote_after))
1024            }
1025            Def::Map(map) => {
1026                format!(
1027                    "dict[{}, {}]",
1028                    self.type_for_shape(map.k, quote_after),
1029                    self.type_for_shape(map.v, quote_after)
1030                )
1031            }
1032            Def::Pointer(ptr) => match ptr.pointee {
1033                Some(pointee) => self.type_for_shape(pointee, quote_after),
1034                None => {
1035                    self.imports.insert("Any");
1036                    "Any".to_string()
1037                }
1038            },
1039            Def::Undefined => {
1040                // User-defined types - queue for generation and return name
1041                match &shape.ty {
1042                    Type::User(UserType::Struct(st)) => {
1043                        // Handle tuples specially - inline them as tuple[...] since their
1044                        // type_identifier "(…)" is not a valid Python identifier
1045                        if st.kind == StructKind::Tuple {
1046                            let types: Vec<String> = st
1047                                .fields
1048                                .iter()
1049                                .map(|f| self.type_for_shape(f.shape.get(), quote_after))
1050                                .collect();
1051                            format!("tuple[{}]", types.join(", "))
1052                        } else {
1053                            self.add_shape(shape);
1054                            self.maybe_quote(shape.type_identifier, quote_after)
1055                        }
1056                    }
1057                    Type::User(UserType::Enum(_)) => {
1058                        self.add_shape(shape);
1059                        self.maybe_quote(shape.type_identifier, quote_after)
1060                    }
1061                    _ => self.inner_type_or_any(shape, quote_after),
1062                }
1063            }
1064            _ => self.inner_type_or_any(shape, quote_after),
1065        }
1066    }
1067
1068    /// Wrap a type name in quotes if it is a forward reference (sorts after `quote_after`).
1069    fn maybe_quote(&self, name: &str, quote_after: Option<&str>) -> String {
1070        if let Some(after) = quote_after
1071            && name > after
1072        {
1073            return format!("\"{}\"", name);
1074        }
1075        name.to_string()
1076    }
1077
1078    /// Get the inner type for transparent wrappers, or "Any" as fallback.
1079    fn inner_type_or_any(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
1080        match shape.inner {
1081            Some(inner) => self.type_for_shape(inner, quote_after),
1082            None => {
1083                self.imports.insert("Any");
1084                "Any".to_string()
1085            }
1086        }
1087    }
1088
1089    /// Get the Python type for a scalar shape.
1090    fn scalar_type(&mut self, shape: &'static Shape) -> String {
1091        match shape.type_identifier {
1092            // Strings
1093            "String" | "str" | "&str" | "Cow" => "str".to_string(),
1094
1095            // Booleans
1096            "bool" => "bool".to_string(),
1097
1098            // Integers
1099            "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
1100            | "i128" | "isize" => "int".to_string(),
1101
1102            // Floats
1103            "f32" | "f64" => "float".to_string(),
1104
1105            // Char as string
1106            "char" => "str".to_string(),
1107
1108            // chrono date/time types — all serialise as ISO 8601 strings
1109            "NaiveDate"
1110            | "NaiveDateTime"
1111            | "NaiveTime"
1112            | "DateTime<Utc>"
1113            | "DateTime<FixedOffset>"
1114            | "DateTime<Local>"
1115                if shape.module_path == Some("chrono") =>
1116            {
1117                "str".to_string()
1118            }
1119
1120            // Unknown scalar
1121            _ => {
1122                self.imports.insert("Any");
1123                "Any".to_string()
1124            }
1125        }
1126    }
1127}
1128
1129/// Write a doc comment to the output.
1130fn write_doc_comment(output: &mut String, doc: &[&str]) {
1131    for line in doc {
1132        output.push('#');
1133        output.push_str(line);
1134        output.push('\n');
1135    }
1136}
1137
1138/// Convert a snake_case or other string to PascalCase.
1139fn to_pascal_case(s: &str) -> String {
1140    let mut result = String::new();
1141    let mut capitalize_next = true;
1142
1143    for c in s.chars() {
1144        if c == '_' || c == '-' {
1145            capitalize_next = true;
1146        } else if capitalize_next {
1147            result.push(c.to_ascii_uppercase());
1148            capitalize_next = false;
1149        } else {
1150            result.push(c);
1151        }
1152    }
1153
1154    result
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160    use facet::Facet;
1161
1162    #[test]
1163    fn test_simple_struct() {
1164        #[derive(Facet)]
1165        struct User {
1166            name: String,
1167            age: u32,
1168        }
1169
1170        let py = to_python::<User>(false);
1171        insta::assert_snapshot!(py);
1172    }
1173
1174    #[test]
1175    fn test_optional_field() {
1176        #[derive(Facet)]
1177        struct Config {
1178            required: String,
1179            optional: Option<String>,
1180        }
1181
1182        let py = to_python::<Config>(false);
1183        insta::assert_snapshot!(py);
1184    }
1185
1186    #[test]
1187    fn test_simple_enum() {
1188        #[derive(Facet)]
1189        #[repr(u8)]
1190        enum Status {
1191            Active,
1192            Inactive,
1193            Pending,
1194        }
1195
1196        let py = to_python::<Status>(false);
1197        insta::assert_snapshot!(py);
1198    }
1199
1200    #[test]
1201    fn test_vec() {
1202        #[derive(Facet)]
1203        struct Data {
1204            items: Vec<String>,
1205        }
1206
1207        let py = to_python::<Data>(false);
1208        insta::assert_snapshot!(py);
1209    }
1210
1211    #[test]
1212    fn test_nested_types() {
1213        #[derive(Facet)]
1214        struct Inner {
1215            value: i32,
1216        }
1217
1218        #[derive(Facet)]
1219        struct Outer {
1220            inner: Inner,
1221            name: String,
1222        }
1223
1224        let py = to_python::<Outer>(false);
1225        insta::assert_snapshot!(py);
1226    }
1227
1228    #[test]
1229    fn test_enum_rename_all_snake_case() {
1230        #[derive(Facet)]
1231        #[facet(rename_all = "snake_case")]
1232        #[repr(u8)]
1233        enum ValidationErrorCode {
1234            CircularDependency,
1235            InvalidNaming,
1236            UnknownRequirement,
1237        }
1238
1239        let py = to_python::<ValidationErrorCode>(false);
1240        insta::assert_snapshot!(py);
1241    }
1242
1243    #[test]
1244    fn test_enum_rename_individual() {
1245        #[derive(Facet)]
1246        #[repr(u8)]
1247        enum GitStatus {
1248            #[facet(rename = "dirty")]
1249            Dirty,
1250            #[facet(rename = "staged")]
1251            Staged,
1252            #[facet(rename = "clean")]
1253            Clean,
1254        }
1255
1256        let py = to_python::<GitStatus>(false);
1257        insta::assert_snapshot!(py);
1258    }
1259
1260    #[test]
1261    fn test_struct_rename_all_camel_case() {
1262        #[derive(Facet)]
1263        #[facet(rename_all = "camelCase")]
1264        struct ApiResponse {
1265            user_name: String,
1266            created_at: String,
1267            is_active: bool,
1268        }
1269
1270        let py = to_python::<ApiResponse>(false);
1271        insta::assert_snapshot!(py);
1272    }
1273
1274    #[test]
1275    fn test_struct_rename_individual() {
1276        #[derive(Facet)]
1277        struct UserProfile {
1278            #[facet(rename = "userName")]
1279            user_name: String,
1280            #[facet(rename = "emailAddress")]
1281            email: String,
1282        }
1283
1284        let py = to_python::<UserProfile>(false);
1285        insta::assert_snapshot!(py);
1286    }
1287
1288    #[test]
1289    fn test_enum_with_data_rename_all() {
1290        #[derive(Facet)]
1291        #[facet(rename_all = "snake_case")]
1292        #[repr(C)]
1293        #[allow(dead_code)]
1294        enum Message {
1295            TextMessage { content: String },
1296            ImageUpload { url: String, width: u32 },
1297        }
1298
1299        let py = to_python::<Message>(false);
1300        insta::assert_snapshot!(py);
1301    }
1302
1303    #[test]
1304    fn test_unit_struct() {
1305        #[derive(Facet)]
1306        struct Empty;
1307
1308        let py = to_python::<Empty>(false);
1309        insta::assert_snapshot!(py);
1310    }
1311
1312    #[test]
1313    fn test_tuple_struct() {
1314        #[derive(Facet)]
1315        struct Point(f32, f64);
1316
1317        let py = to_python::<Point>(false);
1318        insta::assert_snapshot!(py);
1319    }
1320
1321    #[test]
1322    fn test_newtype_struct() {
1323        #[derive(Facet)]
1324        struct UserId(u64);
1325
1326        let py = to_python::<UserId>(false);
1327        insta::assert_snapshot!(py);
1328    }
1329
1330    #[test]
1331    fn test_hashmap() {
1332        use std::collections::HashMap;
1333
1334        #[derive(Facet)]
1335        struct Registry {
1336            entries: HashMap<String, i32>,
1337        }
1338
1339        let py = to_python::<Registry>(false);
1340        insta::assert_snapshot!(py);
1341    }
1342
1343    #[test]
1344    fn test_mixed_enum_variants() {
1345        #[derive(Facet)]
1346        #[repr(C)]
1347        #[allow(dead_code)]
1348        enum Event {
1349            /// Unit variant
1350            Empty,
1351            /// Newtype variant
1352            Id(u64),
1353            /// Struct variant
1354            Data { name: String, value: f64 },
1355        }
1356
1357        let py = to_python::<Event>(false);
1358        insta::assert_snapshot!(py);
1359    }
1360
1361    #[test]
1362    fn test_with_imports() {
1363        #[derive(Facet)]
1364        struct User {
1365            name: String,
1366            age: u32,
1367        }
1368
1369        let py = to_python::<User>(true);
1370        insta::assert_snapshot!(py);
1371    }
1372
1373    #[test]
1374    fn test_enum_with_imports() {
1375        #[derive(Facet)]
1376        #[repr(u8)]
1377        enum Status {
1378            Active,
1379            Inactive,
1380        }
1381
1382        let py = to_python::<Status>(true);
1383        insta::assert_snapshot!(py);
1384    }
1385
1386    #[test]
1387    fn test_transparent_wrapper() {
1388        #[derive(Facet)]
1389        #[facet(transparent)]
1390        struct UserId(String);
1391
1392        let py = to_python::<UserId>(false);
1393        // This should generate "type UserId = str" not "UserId = str"
1394        insta::assert_snapshot!(py);
1395    }
1396
1397    #[test]
1398    fn test_transparent_wrapper_with_inner_type() {
1399        #[derive(Facet)]
1400        struct Inner {
1401            value: i32,
1402        }
1403
1404        #[derive(Facet)]
1405        #[facet(transparent)]
1406        struct Wrapper(Inner);
1407
1408        let py = to_python::<Wrapper>(false);
1409        // This should generate "type Wrapper = Inner" not "Wrapper = Inner"
1410        insta::assert_snapshot!(py);
1411    }
1412
1413    #[test]
1414    fn test_struct_with_tuple_field() {
1415        #[derive(Facet)]
1416        struct Container {
1417            /// A tuple field containing coordinates
1418            coordinates: (i32, i32),
1419        }
1420
1421        let py = to_python::<Container>(false);
1422        // This should NOT generate "(…)" as a type - it should properly expand the tuple
1423        insta::assert_snapshot!(py);
1424    }
1425
1426    #[test]
1427    fn test_struct_with_reserved_keyword_field() {
1428        #[derive(Facet)]
1429        struct TradeOrder {
1430            from: f64,
1431            to: f64,
1432            quantity: f64,
1433        }
1434
1435        let py = to_python::<TradeOrder>(false);
1436        // This should use functional TypedDict syntax since "from" is a Python keyword
1437        insta::assert_snapshot!(py);
1438    }
1439
1440    #[test]
1441    fn test_struct_with_multiple_reserved_keywords() {
1442        #[derive(Facet)]
1443        struct ControlFlow {
1444            r#if: bool,
1445            r#else: String,
1446            r#return: i32,
1447        }
1448
1449        let py = to_python::<ControlFlow>(false);
1450        // Multiple Python keywords - should use functional syntax
1451        insta::assert_snapshot!(py);
1452    }
1453
1454    #[test]
1455    fn test_enum_variant_name_is_reserved_keyword() {
1456        #[derive(Facet)]
1457        #[repr(C)]
1458        #[facet(rename_all = "snake_case")]
1459        #[allow(dead_code)]
1460        enum ImportSource {
1461            /// Import from a file
1462            From(String),
1463            /// Import from a URL
1464            Url(String),
1465        }
1466
1467        let py = to_python::<ImportSource>(false);
1468        // The variant "From" becomes field name "from" which is a Python keyword
1469        // Should use functional TypedDict syntax for the wrapper class
1470        insta::assert_snapshot!(py);
1471    }
1472
1473    #[test]
1474    fn test_enum_data_variant_with_reserved_keyword_field() {
1475        #[derive(Facet)]
1476        #[repr(C)]
1477        #[allow(dead_code)]
1478        enum Transfer {
1479            /// A transfer between accounts
1480            Move {
1481                from: String,
1482                to: String,
1483                amount: f64,
1484            },
1485            /// Cancel the transfer
1486            Cancel,
1487        }
1488
1489        let py = to_python::<Transfer>(false);
1490        // The data variant "Move" has fields "from" and "to" which are Python keywords
1491        // Should use functional TypedDict syntax for the data class
1492        insta::assert_snapshot!(py);
1493    }
1494
1495    #[test]
1496    fn test_hashmap_with_integer_keys() {
1497        use std::collections::HashMap;
1498
1499        #[derive(Facet)]
1500        struct IntKeyedMap {
1501            /// Map with integer keys
1502            counts: HashMap<i32, String>,
1503        }
1504
1505        let py = to_python::<IntKeyedMap>(false);
1506        insta::assert_snapshot!(py);
1507    }
1508
1509    #[test]
1510    fn test_empty_tuple_struct() {
1511        #[derive(Facet)]
1512        struct EmptyTuple();
1513
1514        let py = to_python::<EmptyTuple>(false);
1515        insta::assert_snapshot!(py);
1516    }
1517
1518    #[test]
1519    fn test_hashmap_with_enum_keys() {
1520        use std::collections::HashMap;
1521
1522        #[derive(Facet, Hash, PartialEq, Eq)]
1523        #[repr(u8)]
1524        enum Priority {
1525            Low,
1526            Medium,
1527            High,
1528        }
1529
1530        #[derive(Facet)]
1531        struct TaskMap {
1532            tasks: HashMap<Priority, String>,
1533        }
1534
1535        let py = to_python::<TaskMap>(false);
1536        insta::assert_snapshot!(py);
1537    }
1538
1539    #[test]
1540    fn test_enum_tuple_variant() {
1541        #[derive(Facet)]
1542        #[repr(C)]
1543        #[allow(dead_code)]
1544        enum TupleVariant {
1545            Point(i32, i32),
1546        }
1547        let py = to_python::<TupleVariant>(false);
1548        insta::assert_snapshot!(py);
1549    }
1550
1551    #[test]
1552    fn test_enum_struct_variant_forward_reference() {
1553        // This test verifies that struct variant data classes are quoted
1554        // to handle forward references correctly in Python.
1555        // Without quoting, Python would fail with "NameError: name 'DataData' is not defined"
1556        // because DataData is defined after Data in alphabetical order.
1557        #[derive(Facet)]
1558        #[repr(C)]
1559        #[allow(dead_code)]
1560        enum Message {
1561            // Struct variant with inline fields - generates MessageData class
1562            Data { name: String, value: f64 },
1563        }
1564        let py = to_python::<Message>(false);
1565        insta::assert_snapshot!(py);
1566    }
1567
1568    #[test]
1569    fn test_functional_typed_dict_no_type_keyword() {
1570        // Regression test for https://github.com/facet-rs/facet/issues/2131
1571        #[derive(Facet)]
1572        struct Bug {
1573            from: Option<String>,
1574        }
1575
1576        let py = to_python::<Bug>(false);
1577        assert!(
1578            !py.starts_with("type "),
1579            "functional TypedDict should NOT start with `type` keyword, got:\n{py}"
1580        );
1581        insta::assert_snapshot!(py);
1582    }
1583
1584    #[test]
1585    fn test_functional_typed_dict_forward_ref_quoted() {
1586        // Regression test for https://github.com/facet-rs/facet/issues/2131
1587        #[derive(Facet)]
1588        #[allow(dead_code)]
1589        struct Recipient {
1590            name: String,
1591        }
1592
1593        #[derive(Facet)]
1594        #[allow(dead_code)]
1595        struct Addr {
1596            from: String,
1597            to: Recipient,
1598        }
1599
1600        let py = to_python::<Addr>(false);
1601        assert!(
1602            py.contains("Required[\"Recipient\"]"),
1603            "forward reference in functional TypedDict should be quoted, got:\n{py}"
1604        );
1605        insta::assert_snapshot!(py);
1606    }
1607
1608    #[test]
1609    fn test_flatten() {
1610        #[derive(Facet)]
1611        struct Inner {
1612            x: f64,
1613            y: f64,
1614        }
1615
1616        #[derive(Facet)]
1617        struct Outer {
1618            #[facet(flatten)]
1619            inner: Inner,
1620            z: String,
1621        }
1622
1623        let py = to_python::<Outer>(false);
1624
1625        // Inner should still be generated as its own TypedDict
1626        assert!(
1627            py.contains("class Inner(TypedDict, total=False):"),
1628            "#[facet(flatten)] — Inner should still be generated as its own TypedDict, got:\n{py}"
1629        );
1630
1631        // The flattened field 'inner' must NOT appear as a key in Outer
1632        assert!(
1633            !py.contains("inner: Required[Inner]"),
1634            "#[facet(flatten)] — 'inner' should be inlined, not a nested field, got:\n{py}"
1635        );
1636
1637        // x and y must be inlined directly into Outer
1638        assert!(
1639            py.contains("    x: Required[float]"),
1640            "#[facet(flatten)] — 'x' should be inlined from Inner into Outer, got:\n{py}"
1641        );
1642        assert!(
1643            py.contains("    y: Required[float]"),
1644            "#[facet(flatten)] — 'y' should be inlined from Inner into Outer, got:\n{py}"
1645        );
1646
1647        insta::assert_snapshot!(py);
1648    }
1649
1650    #[test]
1651    fn test_flatten_option() {
1652        // #[facet(flatten)] on Option<Struct> — the inlined fields should be
1653        // optional in the parent TypedDict because their JSON presence is
1654        // conditional on the Option being Some.
1655        #[derive(Facet)]
1656        struct Coords {
1657            x: f64,
1658            y: f64,
1659        }
1660
1661        #[derive(Facet)]
1662        struct Entity {
1663            name: String,
1664            #[facet(flatten)]
1665            coords: Option<Coords>,
1666        }
1667
1668        let py = to_python::<Entity>(false);
1669
1670        // Coords must still be generated as its own TypedDict
1671        assert!(
1672            py.contains("class Coords(TypedDict, total=False):"),
1673            "flatten Option — Coords should still be generated, got:\n{py}"
1674        );
1675        // The raw 'coords' field must NOT appear as a nested key
1676        assert!(
1677            !py.contains("coords:"),
1678            "flatten Option — 'coords' key should not appear in Entity, got:\n{py}"
1679        );
1680        // x and y must be inlined as optional (bare type, no Required[]).
1681        // Coords' own definition still uses Required[float], so check with
1682        // indentation to match Entity's lines only.
1683        assert!(
1684            py.contains("    x: float"),
1685            "flatten Option — 'x' should be inlined as optional float in Entity, got:\n{py}"
1686        );
1687        assert!(
1688            py.contains("    y: float"),
1689            "flatten Option — 'y' should be inlined as optional float in Entity, got:\n{py}"
1690        );
1691
1692        insta::assert_snapshot!(py);
1693    }
1694
1695    #[test]
1696    fn test_flatten_with_rename_all() {
1697        // Flattened struct with rename_all — inlined fields should use the
1698        // renamed effective names, not the original Rust field names.
1699        #[derive(Facet)]
1700        #[facet(rename_all = "camelCase")]
1701        struct Coords {
1702            pos_x: f64,
1703            pos_y: f64,
1704        }
1705
1706        #[derive(Facet)]
1707        struct Entity {
1708            #[facet(flatten)]
1709            coords: Coords,
1710            label: String,
1711        }
1712
1713        let py = to_python::<Entity>(false);
1714
1715        // Renamed fields must appear, not the Rust names
1716        assert!(
1717            py.contains("posX: Required[float]"),
1718            "flatten + rename_all — 'posX' should be inlined into Entity, got:\n{py}"
1719        );
1720        assert!(
1721            py.contains("posY: Required[float]"),
1722            "flatten + rename_all — 'posY' should be inlined into Entity, got:\n{py}"
1723        );
1724        // Raw Rust names must NOT appear in the output
1725        assert!(
1726            !py.contains("pos_x"),
1727            "flatten + rename_all — raw 'pos_x' should not appear, got:\n{py}"
1728        );
1729
1730        insta::assert_snapshot!(py);
1731    }
1732
1733    #[test]
1734    fn test_flatten_with_optional_fields() {
1735        // Optional fields inside a flattened struct must remain optional
1736        // (i.e. not wrapped in Required[]) in the parent TypedDict.
1737        #[derive(Facet)]
1738        struct Meta {
1739            description: Option<String>,
1740            version: u32,
1741        }
1742
1743        #[derive(Facet)]
1744        struct Package {
1745            name: String,
1746            #[facet(flatten)]
1747            meta: Meta,
1748        }
1749
1750        let py = to_python::<Package>(false);
1751
1752        // Optional field must stay optional — in this generator optional fields
1753        // use a bare type (no Required[]) because the TypedDict is total=False.
1754        assert!(
1755            py.contains("description: str"),
1756            "flatten + optional — 'description' should be optional (bare type) in Package, got:\n{py}"
1757        );
1758        assert!(
1759            !py.contains("description: Required[str]"),
1760            "flatten + optional — 'description' must NOT be wrapped in Required[], got:\n{py}"
1761        );
1762        // Required field must stay required
1763        assert!(
1764            py.contains("version: Required[int]"),
1765            "flatten + optional — 'version' should be required in Package, got:\n{py}"
1766        );
1767
1768        insta::assert_snapshot!(py);
1769    }
1770
1771    #[test]
1772    fn test_flatten_multilevel() {
1773        // A flattened struct that itself contains a flattened struct —
1774        // all fields should end up in the outermost TypedDict.
1775        #[derive(Facet)]
1776        struct Point {
1777            x: f64,
1778            y: f64,
1779        }
1780
1781        #[derive(Facet)]
1782        struct ColoredPoint {
1783            #[facet(flatten)]
1784            point: Point,
1785            color: String,
1786        }
1787
1788        #[derive(Facet)]
1789        struct Scene {
1790            #[facet(flatten)]
1791            colored_point: ColoredPoint,
1792            name: String,
1793        }
1794
1795        let py = to_python::<Scene>(false);
1796
1797        // x and y must be inlined all the way into Scene
1798        assert!(
1799            py.contains("    x: Required[float]"),
1800            "multi-level flatten — 'x' should reach Scene, got:\n{py}"
1801        );
1802        assert!(
1803            py.contains("    y: Required[float]"),
1804            "multi-level flatten — 'y' should reach Scene, got:\n{py}"
1805        );
1806        assert!(
1807            py.contains("    color: Required[str]"),
1808            "multi-level flatten — 'color' should reach Scene, got:\n{py}"
1809        );
1810        // Neither intermediate field name should appear as a key
1811        assert!(
1812            !py.contains("colored_point:"),
1813            "multi-level flatten — 'colored_point' key should not appear in Scene, got:\n{py}"
1814        );
1815        assert!(
1816            !py.contains("point:"),
1817            "multi-level flatten — 'point' key should not appear in Scene, got:\n{py}"
1818        );
1819
1820        insta::assert_snapshot!(py);
1821    }
1822
1823    #[test]
1824    fn test_flatten_preserves_field_docs() {
1825        // Doc comments on fields inside a flattened struct should be
1826        // preserved when those fields are inlined into the parent TypedDict.
1827        #[derive(Facet)]
1828        struct Dims {
1829            /// Width in pixels
1830            width: u32,
1831            /// Height in pixels
1832            height: u32,
1833        }
1834
1835        #[derive(Facet)]
1836        struct Image {
1837            #[facet(flatten)]
1838            dims: Dims,
1839            path: String,
1840        }
1841
1842        let py = to_python::<Image>(false);
1843
1844        assert!(
1845            py.contains("width: Required[int]"),
1846            "flatten docs — 'width' should be inlined into Image, got:\n{py}"
1847        );
1848        assert!(
1849            py.contains("height: Required[int]"),
1850            "flatten docs — 'height' should be inlined into Image, got:\n{py}"
1851        );
1852        assert!(
1853            !py.contains("dims: Required[Dims]"),
1854            "flatten docs — 'dims' key should not appear in Image, got:\n{py}"
1855        );
1856
1857        insta::assert_snapshot!(py);
1858    }
1859
1860    #[test]
1861    fn test_flatten_arc() {
1862        // #[facet(flatten)] on Arc<Struct> — should inline the same as a plain
1863        // struct flatten, since Arc is just a pointer wrapper.
1864        use std::sync::Arc;
1865
1866        #[derive(Facet)]
1867        struct Coords {
1868            x: f64,
1869            y: f64,
1870        }
1871
1872        #[derive(Facet)]
1873        struct Entity {
1874            name: String,
1875            #[facet(flatten)]
1876            coords: Arc<Coords>,
1877        }
1878
1879        let py = to_python::<Entity>(false);
1880
1881        assert!(
1882            py.contains("class Coords(TypedDict, total=False):"),
1883            "flatten Arc — Coords should still be generated, got:\n{py}"
1884        );
1885        assert!(
1886            !py.contains("coords:"),
1887            "flatten Arc — 'coords' key should not appear in Entity, got:\n{py}"
1888        );
1889        assert!(
1890            py.contains("    x: Required[float]"),
1891            "flatten Arc — 'x' should be inlined as required float in Entity, got:\n{py}"
1892        );
1893        assert!(
1894            py.contains("    y: Required[float]"),
1895            "flatten Arc — 'y' should be inlined as required float in Entity, got:\n{py}"
1896        );
1897
1898        insta::assert_snapshot!(py);
1899    }
1900
1901    #[test]
1902    fn test_flatten_option_arc() {
1903        // #[facet(flatten)] on Option<Arc<Struct>> — multi-layer unwrap.
1904        // Fields should be optional (from the Option) despite the Arc wrapper.
1905        use std::sync::Arc;
1906
1907        #[derive(Facet)]
1908        struct Coords {
1909            x: f64,
1910            y: f64,
1911        }
1912
1913        #[derive(Facet)]
1914        struct Entity {
1915            name: String,
1916            #[facet(flatten)]
1917            coords: Option<Arc<Coords>>,
1918        }
1919
1920        let py = to_python::<Entity>(false);
1921
1922        assert!(
1923            py.contains("class Coords(TypedDict, total=False):"),
1924            "flatten Option<Arc> — Coords should still be generated, got:\n{py}"
1925        );
1926        assert!(
1927            !py.contains("coords:"),
1928            "flatten Option<Arc> — 'coords' key should not appear in Entity, got:\n{py}"
1929        );
1930        // Fields must be optional (bare type) because of the Option wrapper
1931        assert!(
1932            py.contains("    x: float"),
1933            "flatten Option<Arc> — 'x' should be inlined as optional float in Entity, got:\n{py}"
1934        );
1935        assert!(
1936            py.contains("    y: float"),
1937            "flatten Option<Arc> — 'y' should be inlined as optional float in Entity, got:\n{py}"
1938        );
1939
1940        insta::assert_snapshot!(py);
1941    }
1942
1943    #[test]
1944    fn test_flatten_skip_serializing_field() {
1945        // A #[facet(skip_serializing)] field inside a flattened struct must
1946        // NOT appear in the parent TypedDict — it is excluded from the wire
1947        // format so it must not be part of the Python type either.
1948        #[derive(Facet)]
1949        struct Coords {
1950            x: f64,
1951            y: f64,
1952            #[facet(skip_serializing)]
1953            internal: u8,
1954        }
1955
1956        #[derive(Facet)]
1957        struct Entity {
1958            name: String,
1959            #[facet(flatten)]
1960            coords: Coords,
1961        }
1962
1963        let py = to_python::<Entity>(false);
1964
1965        assert!(
1966            py.contains("    x: Required[float]"),
1967            "flatten skip_serializing — 'x' should be inlined, got:\n{py}"
1968        );
1969        assert!(
1970            py.contains("    y: Required[float]"),
1971            "flatten skip_serializing — 'y' should be inlined, got:\n{py}"
1972        );
1973        assert!(
1974            !py.contains("internal"),
1975            "flatten skip_serializing — 'internal' must not appear anywhere, got:\n{py}"
1976        );
1977
1978        insta::assert_snapshot!(py);
1979    }
1980
1981    #[test]
1982    fn test_default_field_not_required() {
1983        // Fields with #[facet(default)] are optional in JSON — facet fills in
1984        // the default when the key is absent. They must not be Required[T].
1985        #[derive(Facet)]
1986        struct Config {
1987            name: String,
1988            #[facet(default)]
1989            retries: u32,
1990            #[facet(default = 30)]
1991            timeout: u32,
1992            required_value: i32,
1993        }
1994
1995        let py = to_python::<Config>(false);
1996
1997        // Non-default fields must still be Required
1998        assert!(
1999            py.contains("name: Required[str]"),
2000            "default — 'name' has no default so must be Required, got:\n{py}"
2001        );
2002        assert!(
2003            py.contains("required_value: Required[int]"),
2004            "default — 'required_value' has no default so must be Required, got:\n{py}"
2005        );
2006        // Fields with defaults must NOT be Required
2007        assert!(
2008            !py.contains("retries: Required[int]"),
2009            "default — 'retries' has a default so must NOT be Required, got:\n{py}"
2010        );
2011        assert!(
2012            py.contains("retries: int"),
2013            "default — 'retries' should be bare int (optional), got:\n{py}"
2014        );
2015        assert!(
2016            !py.contains("timeout: Required[int]"),
2017            "default — 'timeout' has a default so must NOT be Required, got:\n{py}"
2018        );
2019        assert!(
2020            py.contains("timeout: int"),
2021            "default — 'timeout' should be bare int (optional), got:\n{py}"
2022        );
2023
2024        insta::assert_snapshot!(py);
2025    }
2026
2027    #[test]
2028    fn test_internally_tagged_enum() {
2029        // #[facet(tag = "type")] enums are internally tagged: facet-json serializes
2030        // each variant as {"type":"VariantName", ...fields}. The Python TypedDict
2031        // must include the tag field, NOT an outer wrapper key.
2032        #[derive(Facet)]
2033        #[facet(tag = "type")]
2034        #[repr(C)]
2035        #[allow(dead_code)]
2036        enum Shape {
2037            Mat,
2038            Sp { first_roll: u32, long_last: bool },
2039        }
2040
2041        let py = to_python::<Shape>(false);
2042
2043        // Must NOT use external wrapping ({"Sp": {...}} does not exist at runtime)
2044        assert!(
2045            !py.contains("Sp: Required"),
2046            "internally tagged — 'Sp' must not appear as an outer key, got:\n{py}"
2047        );
2048        // Tag field must be present for the struct variant
2049        assert!(
2050            py.contains("type: Required[Literal[\"Sp\"]]"),
2051            "internally tagged — tag field missing for Sp variant, got:\n{py}"
2052        );
2053        // Struct fields must be inlined at the same level as the tag
2054        assert!(
2055            py.contains("first_roll: Required[int]"),
2056            "internally tagged — 'first_roll' should be inlined, got:\n{py}"
2057        );
2058        // Unit variant must also get a TypedDict with just the tag field
2059        assert!(
2060            py.contains("type: Required[Literal[\"Mat\"]]"),
2061            "internally tagged — tag field missing for Mat variant, got:\n{py}"
2062        );
2063
2064        insta::assert_snapshot!(py);
2065    }
2066
2067    #[test]
2068    fn test_untagged_enum() {
2069        // #[facet(untagged)] enums serialize each variant purely by content —
2070        // no discriminator key at all. The Python union must reflect this:
2071        //   unit    => Literal["VariantName"]
2072        //   newtype => the inner type directly (no wrapper TypedDict)
2073        //   struct  => a named TypedDict with inlined fields
2074        #[derive(Facet)]
2075        #[facet(untagged)]
2076        #[repr(C)]
2077        #[allow(dead_code)]
2078        enum Value {
2079            None,
2080            Number(f64),
2081            Point { x: f64, y: f64 },
2082        }
2083
2084        let py = to_python::<Value>(false);
2085
2086        // Newtype must NOT be wrapped in {"Number": ...} — the float appears directly
2087        assert!(
2088            !py.contains("Number: Required"),
2089            "untagged — 'Number' must not appear as an outer key, got:\n{py}"
2090        );
2091        // The union must include float directly for the newtype variant
2092        assert!(
2093            py.contains("float"),
2094            "untagged — float should appear directly in the union, got:\n{py}"
2095        );
2096        // Struct variant must NOT be wrapped in {"Point": ...}
2097        assert!(
2098            !py.contains("Point: Required"),
2099            "untagged — 'Point' must not appear as an outer key, got:\n{py}"
2100        );
2101        // Struct variant fields must be in a named TypedDict
2102        assert!(
2103            py.contains("x: Required[float]"),
2104            "untagged — 'x' field should appear in a TypedDict, got:\n{py}"
2105        );
2106        assert!(
2107            py.contains("y: Required[float]"),
2108            "untagged — 'y' field should appear in a TypedDict, got:\n{py}"
2109        );
2110
2111        insta::assert_snapshot!(py);
2112    }
2113
2114    #[test]
2115    fn test_flatten_tagged_enum() {
2116        // When a struct flattens a #[facet(tag = "...")] enum, facet-json merges
2117        // the tag field and variant fields directly into the parent object — no
2118        // wrapper key appears. The Python output must reflect this by turning the
2119        // parent struct into a union of per-variant TypedDicts, each containing
2120        // the parent's own fields plus the tag discriminator plus the variant fields.
2121        #[derive(Facet)]
2122        #[facet(tag = "type")]
2123        #[repr(C)]
2124        #[allow(dead_code)]
2125        enum Product {
2126            Irs { pay_rate: f64, receive_rate: f64 },
2127            Fx { ccy: String, amount: f64 },
2128        }
2129
2130        #[derive(Facet)]
2131        struct Deal {
2132            id: String,
2133            #[facet(flatten)]
2134            product: Product,
2135        }
2136
2137        let py = to_python::<Deal>(false);
2138
2139        // The "product" key must NOT appear — it does not exist in the JSON
2140        assert!(
2141            !py.contains("product:"),
2142            "flatten tagged enum — 'product' key must not appear, got:\n{py}"
2143        );
2144        // Deal must be a union, not a single class
2145        assert!(
2146            !py.contains("class Deal(TypedDict"),
2147            "flatten tagged enum — Deal must not be a single TypedDict class, got:\n{py}"
2148        );
2149        assert!(
2150            py.contains("type Deal = "),
2151            "flatten tagged enum — Deal should be a union type alias, got:\n{py}"
2152        );
2153        // Per-variant TypedDicts must exist
2154        assert!(
2155            py.contains("class DealIrs(TypedDict, total=False):"),
2156            "flatten tagged enum — DealIrs class missing, got:\n{py}"
2157        );
2158        assert!(
2159            py.contains("class DealFx(TypedDict, total=False):"),
2160            "flatten tagged enum — DealFx class missing, got:\n{py}"
2161        );
2162        // Tag discriminator fields
2163        assert!(
2164            py.contains("type: Required[Literal[\"Irs\"]]"),
2165            "flatten tagged enum — tag field missing for Irs variant, got:\n{py}"
2166        );
2167        assert!(
2168            py.contains("type: Required[Literal[\"Fx\"]]"),
2169            "flatten tagged enum — tag field missing for Fx variant, got:\n{py}"
2170        );
2171        // Base field from parent must appear in both variants
2172        assert!(
2173            py.contains("id: Required[str]"),
2174            "flatten tagged enum — base field 'id' must be inlined into variants, got:\n{py}"
2175        );
2176        // Variant-specific fields
2177        assert!(
2178            py.contains("pay_rate: Required[float]"),
2179            "flatten tagged enum — 'pay_rate' field missing from DealIrs, got:\n{py}"
2180        );
2181        assert!(
2182            py.contains("ccy: Required[str]"),
2183            "flatten tagged enum — 'ccy' field missing from DealFx, got:\n{py}"
2184        );
2185
2186        insta::assert_snapshot!(py);
2187    }
2188
2189    #[test]
2190    fn test_internally_tagged_newtype_struct_variant() {
2191        // When a tagged enum has a newtype variant whose inner type is a struct,
2192        // facet-json inlines the struct's fields at the same level as the tag —
2193        // there is no "value" wrapper key. The Python TypedDict must match.
2194        #[derive(Facet)]
2195        struct IrsData {
2196            pay_rate: f64,
2197            receive_rate: f64,
2198        }
2199
2200        #[derive(Facet)]
2201        #[facet(tag = "type")]
2202        #[repr(C)]
2203        #[allow(dead_code)]
2204        enum Product {
2205            Irs(IrsData),
2206            Fixed { rate: f64 },
2207        }
2208
2209        let py = to_python::<Product>(false);
2210
2211        // Must NOT have a "value" key wrapping IrsData
2212        assert!(
2213            !py.contains("value: Required[IrsData]"),
2214            "tagged newtype struct — IrsData fields must be inlined, not wrapped in 'value', got:\n{py}"
2215        );
2216        // Tag discriminator must be present
2217        assert!(
2218            py.contains("type: Required[Literal[\"Irs\"]]"),
2219            "tagged newtype struct — tag field missing for Irs variant, got:\n{py}"
2220        );
2221        // Struct fields must be inlined alongside the tag
2222        assert!(
2223            py.contains("pay_rate: Required[float]"),
2224            "tagged newtype struct — 'pay_rate' must be inlined from IrsData, got:\n{py}"
2225        );
2226        assert!(
2227            py.contains("receive_rate: Required[float]"),
2228            "tagged newtype struct — 'receive_rate' must be inlined from IrsData, got:\n{py}"
2229        );
2230
2231        insta::assert_snapshot!(py);
2232    }
2233
2234    #[test]
2235    fn test_flatten_tagged_enum_newtype_struct() {
2236        // Same as above but the tagged enum is flattened into a parent struct.
2237        // The per-variant TypedDicts must combine the parent's base fields with
2238        // the inlined inner-struct fields — no "value" wrapper key.
2239        #[derive(Facet)]
2240        struct IrsData {
2241            pay_rate: f64,
2242            receive_rate: f64,
2243        }
2244
2245        #[derive(Facet)]
2246        struct FxData {
2247            ccy: String,
2248            amount: f64,
2249        }
2250
2251        #[derive(Facet)]
2252        #[facet(tag = "type")]
2253        #[repr(C)]
2254        #[allow(dead_code)]
2255        enum Product {
2256            Irs(IrsData),
2257            Fx(FxData),
2258        }
2259
2260        #[derive(Facet)]
2261        struct Deal {
2262            id: String,
2263            #[facet(flatten)]
2264            product: Product,
2265        }
2266
2267        let py = to_python::<Deal>(false);
2268
2269        // Must NOT have "value" wrappers
2270        assert!(
2271            !py.contains("value: Required[IrsData]"),
2272            "flatten tagged newtype struct — IrsData fields must be inlined, got:\n{py}"
2273        );
2274        assert!(
2275            !py.contains("value: Required[FxData]"),
2276            "flatten tagged newtype struct — FxData fields must be inlined, got:\n{py}"
2277        );
2278        // Base field from parent must appear in both variants
2279        assert!(
2280            py.contains("id: Required[str]"),
2281            "flatten tagged newtype struct — base field 'id' must be inlined, got:\n{py}"
2282        );
2283        // Tag discriminators
2284        assert!(
2285            py.contains("type: Required[Literal[\"Irs\"]]"),
2286            "flatten tagged newtype struct — tag field missing for Irs, got:\n{py}"
2287        );
2288        assert!(
2289            py.contains("type: Required[Literal[\"Fx\"]]"),
2290            "flatten tagged newtype struct — tag field missing for Fx, got:\n{py}"
2291        );
2292        // Variant fields inlined
2293        assert!(
2294            py.contains("pay_rate: Required[float]"),
2295            "flatten tagged newtype struct — 'pay_rate' must be inlined, got:\n{py}"
2296        );
2297        assert!(
2298            py.contains("ccy: Required[str]"),
2299            "flatten tagged newtype struct — 'ccy' must be inlined, got:\n{py}"
2300        );
2301
2302        insta::assert_snapshot!(py);
2303    }
2304
2305    #[test]
2306    fn test_internally_tagged_newtype_primitive_variant() {
2307        // When a tagged enum has a newtype variant whose inner type is a primitive
2308        // or collection, a "value" key IS correct — the primitive cannot be inlined.
2309        #[derive(Facet)]
2310        #[facet(tag = "type")]
2311        #[repr(C)]
2312        #[allow(dead_code)]
2313        enum Event {
2314            Count(u32),
2315        }
2316
2317        let py = to_python::<Event>(false);
2318
2319        // Primitive newtype: "value" key must be present
2320        assert!(
2321            py.contains("value: Required[int]"),
2322            "tagged newtype primitive — 'value' key must be present for primitive inner type, got:\n{py}"
2323        );
2324        assert!(
2325            py.contains("type: Required[Literal[\"Count\"]]"),
2326            "tagged newtype primitive — tag field missing for Count variant, got:\n{py}"
2327        );
2328
2329        insta::assert_snapshot!(py);
2330    }
2331
2332    #[test]
2333    fn test_chrono_types() {
2334        use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
2335
2336        #[derive(Facet)]
2337        struct Event {
2338            date: NaiveDate,
2339            datetime: NaiveDateTime,
2340            time: NaiveTime,
2341            timestamp: DateTime<Utc>,
2342            optional_date: Option<NaiveDate>,
2343        }
2344
2345        let py = to_python::<Event>(false);
2346
2347        // All chrono types must map to str, not Any
2348        assert!(
2349            !py.contains("Any"),
2350            "chrono types must not produce Any, got:\n{py}"
2351        );
2352        assert!(
2353            py.contains("date: Required[str]"),
2354            "NaiveDate must map to str, got:\n{py}"
2355        );
2356        assert!(
2357            py.contains("datetime: Required[str]"),
2358            "NaiveDateTime must map to str, got:\n{py}"
2359        );
2360        assert!(
2361            py.contains("time: Required[str]"),
2362            "NaiveTime must map to str, got:\n{py}"
2363        );
2364        assert!(
2365            py.contains("timestamp: Required[str]"),
2366            "DateTime<Utc> must map to str, got:\n{py}"
2367        );
2368        // Optional chrono field must be bare str (no Required[])
2369        assert!(
2370            py.contains("optional_date: str"),
2371            "Option<NaiveDate> must map to bare str, got:\n{py}"
2372        );
2373
2374        insta::assert_snapshot!(py);
2375    }
2376
2377    #[test]
2378    fn test_proxy_preserves_doc_comment() {
2379        // Doc comments on the proxied type should appear exactly once in the
2380        // output — not doubled (once from generate_shape, once from generate_typed_dict).
2381        /// A 2-D point in wire format.
2382        #[derive(Facet)]
2383        struct WirePoint {
2384            x: f64,
2385            y: f64,
2386        }
2387
2388        /// A 2-D point (internal representation).
2389        #[derive(Facet)]
2390        #[facet(proxy = WirePoint)]
2391        #[allow(dead_code)]
2392        struct Point {
2393            internal_x: f64,
2394            internal_y: f64,
2395        }
2396
2397        impl TryFrom<WirePoint> for Point {
2398            type Error = String;
2399            fn try_from(w: WirePoint) -> Result<Self, Self::Error> {
2400                Ok(Self {
2401                    internal_x: w.x,
2402                    internal_y: w.y,
2403                })
2404            }
2405        }
2406
2407        impl From<&Point> for WirePoint {
2408            fn from(p: &Point) -> Self {
2409                Self {
2410                    x: p.internal_x,
2411                    y: p.internal_y,
2412                }
2413            }
2414        }
2415
2416        let py = to_python::<Point>(false);
2417
2418        // Proxy fields must appear, not the internal ones
2419        assert!(
2420            py.contains("x: Required[float]"),
2421            "proxy doc — proxy field 'x' must appear, got:\n{py}"
2422        );
2423        assert!(
2424            !py.contains("internal_x"),
2425            "proxy doc — internal field must not appear, got:\n{py}"
2426        );
2427
2428        insta::assert_snapshot!(py);
2429    }
2430
2431    #[test]
2432    fn test_proxy_struct() {
2433        // When a struct has #[facet(proxy = ProxyType)], facet-json uses ProxyType
2434        // for the wire format. facet-python must generate a TypedDict matching the
2435        // proxy's fields, not the internal struct fields.
2436        #[derive(Facet)]
2437        struct WireType {
2438            x: f64,
2439            y: f64,
2440        }
2441
2442        #[derive(Facet)]
2443        #[facet(proxy = WireType)]
2444        #[allow(dead_code)]
2445        struct InternalPoint {
2446            internal_x: f64,
2447            internal_y: f64,
2448        }
2449
2450        impl TryFrom<WireType> for InternalPoint {
2451            type Error = String;
2452            fn try_from(w: WireType) -> Result<Self, Self::Error> {
2453                Ok(Self {
2454                    internal_x: w.x,
2455                    internal_y: w.y,
2456                })
2457            }
2458        }
2459
2460        impl From<&InternalPoint> for WireType {
2461            fn from(p: &InternalPoint) -> Self {
2462                Self {
2463                    x: p.internal_x,
2464                    y: p.internal_y,
2465                }
2466            }
2467        }
2468
2469        let py = to_python::<InternalPoint>(false);
2470
2471        // Must NOT expose the internal fields
2472        assert!(
2473            !py.contains("internal_x"),
2474            "proxy struct — internal field must not appear, got:\n{py}"
2475        );
2476        // Must use the proxy's fields
2477        assert!(
2478            py.contains("x: Required[float]"),
2479            "proxy struct — proxy field 'x' must appear, got:\n{py}"
2480        );
2481        assert!(
2482            py.contains("y: Required[float]"),
2483            "proxy struct — proxy field 'y' must appear, got:\n{py}"
2484        );
2485
2486        insta::assert_snapshot!(py);
2487    }
2488
2489    #[test]
2490    fn test_proxy_to_scalar() {
2491        // A struct proxied to a scalar type should generate a type alias.
2492        #[derive(Facet)]
2493        #[facet(proxy = String)]
2494        #[allow(dead_code)]
2495        struct UserId(u64);
2496
2497        impl TryFrom<String> for UserId {
2498            type Error = String;
2499            fn try_from(s: String) -> Result<Self, Self::Error> {
2500                s.parse::<u64>().map(UserId).map_err(|e| e.to_string())
2501            }
2502        }
2503
2504        impl From<&UserId> for String {
2505            fn from(u: &UserId) -> Self {
2506                u.0.to_string()
2507            }
2508        }
2509
2510        let py = to_python::<UserId>(false);
2511
2512        // Should be a type alias to str, not a class with an internal u64 field
2513        assert!(
2514            !py.contains("class UserId(TypedDict"),
2515            "proxy scalar — must not generate a TypedDict class, got:\n{py}"
2516        );
2517        assert!(
2518            py.contains("type UserId = str"),
2519            "proxy scalar — should be a type alias to str, got:\n{py}"
2520        );
2521
2522        insta::assert_snapshot!(py);
2523    }
2524
2525    #[test]
2526    fn test_proxy_to_untagged_enum() {
2527        // The bug-report case: a struct proxied to an untagged enum.
2528        // facet-json serializes via the proxy, so the Python type must match the
2529        // untagged enum's union — NOT the struct's internal fields.
2530        #[derive(Facet, Clone)]
2531        struct Payload {
2532            x: f64,
2533            y: f64,
2534        }
2535
2536        #[derive(Facet, Clone)]
2537        #[facet(untagged)]
2538        #[repr(C)]
2539        #[allow(dead_code)]
2540        enum ProxyEnum {
2541            Variant(Payload),
2542            Constant { constant: f64 },
2543        }
2544
2545        #[derive(Facet)]
2546        #[facet(proxy = ProxyEnum)]
2547        #[allow(dead_code)]
2548        struct MyStruct {
2549            inner: Payload,
2550        }
2551
2552        impl TryFrom<ProxyEnum> for MyStruct {
2553            type Error = String;
2554            fn try_from(p: ProxyEnum) -> Result<Self, Self::Error> {
2555                match p {
2556                    ProxyEnum::Variant(payload) => Ok(Self { inner: payload }),
2557                    ProxyEnum::Constant { .. } => Err("cannot build".into()),
2558                }
2559            }
2560        }
2561
2562        impl From<&MyStruct> for ProxyEnum {
2563            fn from(s: &MyStruct) -> Self {
2564                ProxyEnum::Variant(s.inner.clone())
2565            }
2566        }
2567
2568        let py = to_python::<MyStruct>(false);
2569
2570        // Must NOT expose the internal 'inner' field
2571        assert!(
2572            !py.contains("inner: Required"),
2573            "proxy untagged enum — internal field must not appear, got:\n{py}"
2574        );
2575        // Must NOT generate a TypedDict class for MyStruct
2576        assert!(
2577            !py.contains("class MyStruct(TypedDict"),
2578            "proxy untagged enum — must not be a single TypedDict class, got:\n{py}"
2579        );
2580        // Must be a union type alias (from the untagged enum)
2581        assert!(
2582            py.contains("type MyStruct"),
2583            "proxy untagged enum — should be a union type alias, got:\n{py}"
2584        );
2585
2586        insta::assert_snapshot!(py);
2587    }
2588}