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        match &shape.ty {
225            Type::User(UserType::Struct(st)) => {
226                self.generate_struct(&mut output, shape, st.fields, st.kind);
227            }
228            Type::User(UserType::Enum(en)) => {
229                self.generate_enum(&mut output, shape, en);
230            }
231            _ => {
232                // For other types, generate a type alias
233                let type_str = self.type_for_shape(shape, None);
234                write_doc_comment(&mut output, shape.doc);
235                writeln!(output, "type {} = {}", shape.type_identifier, type_str).unwrap();
236                output.push('\n');
237            }
238        }
239
240        self.generated
241            .insert(shape.type_identifier.to_string(), output);
242    }
243
244    fn generate_struct(
245        &mut self,
246        output: &mut String,
247        shape: &'static Shape,
248        fields: &'static [Field],
249        kind: StructKind,
250    ) {
251        match kind {
252            StructKind::Unit => {
253                write_doc_comment(output, shape.doc);
254                writeln!(output, "{} = None", shape.type_identifier).unwrap();
255            }
256            StructKind::TupleStruct | StructKind::Tuple if fields.is_empty() => {
257                // Empty tuple struct like `struct Empty();` - treat like unit struct
258                write_doc_comment(output, shape.doc);
259                writeln!(output, "{} = None", shape.type_identifier).unwrap();
260            }
261            StructKind::TupleStruct if fields.len() == 1 => {
262                let inner_type = self.type_for_shape(fields[0].shape.get(), None);
263                write_doc_comment(output, shape.doc);
264                writeln!(output, "{} = {}", shape.type_identifier, inner_type).unwrap();
265            }
266            StructKind::TupleStruct | StructKind::Tuple => {
267                let types: Vec<String> = fields
268                    .iter()
269                    .map(|f| self.type_for_shape(f.shape.get(), None))
270                    .collect();
271                write_doc_comment(output, shape.doc);
272                writeln!(
273                    output,
274                    "{} = tuple[{}]",
275                    shape.type_identifier,
276                    types.join(", ")
277                )
278                .unwrap();
279            }
280            StructKind::Struct => {
281                self.generate_typed_dict(output, shape, fields);
282            }
283        }
284        output.push('\n');
285    }
286
287    /// Generate a TypedDict for a struct, choosing between class and functional syntax.
288    fn generate_typed_dict(
289        &mut self,
290        output: &mut String,
291        shape: &'static Shape,
292        fields: &'static [Field],
293    ) {
294        self.imports.insert("TypedDict");
295
296        let visible_fields: Vec<_> = fields
297            .iter()
298            .filter(|f| !f.flags.contains(facet_core::FieldFlags::SKIP))
299            .collect();
300
301        // Functional form uses runtime expressions — quote forward references.
302        let needs_functional = visible_fields
303            .iter()
304            .any(|f| is_python_keyword(f.effective_name()));
305        let quote_after: Option<&str> = if needs_functional {
306            Some(shape.type_identifier)
307        } else {
308            None
309        };
310
311        // Convert to TypedDictField for shared generation logic
312        let typed_dict_fields: Vec<_> = visible_fields
313            .iter()
314            .map(|f| {
315                let (type_string, required) = self.field_type_info(f, quote_after);
316                TypedDictField::new(f.effective_name(), type_string, required, f.doc)
317            })
318            .collect();
319
320        // Track Required import if any field needs it
321        if typed_dict_fields.iter().any(|f| f.required) {
322            self.imports.insert("Required");
323        }
324
325        write_doc_comment(output, shape.doc);
326        write_typed_dict(output, shape.type_identifier, &typed_dict_fields);
327    }
328
329    /// Get the Python type string and required status for a field.
330    fn field_type_info(&mut self, field: &Field, quote_after: Option<&str>) -> (String, bool) {
331        if let Def::Option(opt) = &field.shape.get().def {
332            (self.type_for_shape(opt.t, quote_after), false)
333        } else {
334            (self.type_for_shape(field.shape.get(), quote_after), true)
335        }
336    }
337
338    fn generate_enum(
339        &mut self,
340        output: &mut String,
341        shape: &'static Shape,
342        enum_type: &facet_core::EnumType,
343    ) {
344        let all_unit = enum_type
345            .variants
346            .iter()
347            .all(|v| matches!(v.data.kind, StructKind::Unit));
348
349        write_doc_comment(output, shape.doc);
350
351        if all_unit {
352            self.generate_enum_unit_variants(output, shape, enum_type);
353        } else {
354            self.generate_enum_with_data(output, shape, enum_type);
355        }
356        output.push('\n');
357    }
358
359    /// Generate a simple enum where all variants are unit variants.
360    fn generate_enum_unit_variants(
361        &mut self,
362        output: &mut String,
363        shape: &'static Shape,
364        enum_type: &facet_core::EnumType,
365    ) {
366        self.imports.insert("Literal");
367
368        let variants: Vec<String> = enum_type
369            .variants
370            .iter()
371            .map(|v| format!("Literal[\"{}\"]", v.effective_name()))
372            .collect();
373
374        writeln!(
375            output,
376            "type {} = {}",
377            shape.type_identifier,
378            variants.join(" | ")
379        )
380        .unwrap();
381    }
382
383    /// Generate an enum with data variants (discriminated union).
384    fn generate_enum_with_data(
385        &mut self,
386        output: &mut String,
387        shape: &'static Shape,
388        enum_type: &facet_core::EnumType,
389    ) {
390        let mut variant_class_names = Vec::new();
391
392        for variant in enum_type.variants {
393            let variant_type_name = self.generate_enum_variant(variant);
394            variant_class_names.push(variant_type_name);
395        }
396
397        writeln!(
398            output,
399            "type {} = {}",
400            shape.type_identifier,
401            variant_class_names.join(" | ")
402        )
403        .unwrap();
404    }
405
406    /// Generate a single enum variant and return its type reference.
407    fn generate_enum_variant(&mut self, variant: &facet_core::Variant) -> String {
408        let variant_name = variant.effective_name();
409        let pascal_variant_name = to_pascal_case(variant_name);
410
411        match variant.data.kind {
412            StructKind::Unit => {
413                self.imports.insert("Literal");
414                format!("Literal[\"{}\"]", variant_name)
415            }
416            StructKind::TupleStruct if variant.data.fields.len() == 1 => {
417                self.generate_newtype_variant(variant_name, &pascal_variant_name, variant);
418                pascal_variant_name.to_string()
419            }
420            StructKind::TupleStruct => {
421                self.generate_tuple_variant(variant_name, &pascal_variant_name, variant);
422                pascal_variant_name.to_string()
423            }
424            _ => {
425                self.generate_struct_variant(variant_name, &pascal_variant_name, variant);
426                pascal_variant_name.to_string()
427            }
428        }
429    }
430
431    /// Generate a newtype variant (single-field tuple variant).
432    fn generate_newtype_variant(
433        &mut self,
434        variant_name: &str,
435        pascal_variant_name: &str,
436        variant: &facet_core::Variant,
437    ) {
438        self.imports.insert("TypedDict");
439        self.imports.insert("Required");
440
441        // Functional form uses runtime expressions — quote forward references.
442        let quote_after: Option<&str> = if is_python_keyword(variant_name) {
443            Some(pascal_variant_name)
444        } else {
445            None
446        };
447
448        let inner_type = self.type_for_shape(variant.data.fields[0].shape.get(), quote_after);
449
450        let fields = [TypedDictField::new(variant_name, inner_type, true, &[])];
451
452        let mut output = String::new();
453        write_typed_dict(&mut output, pascal_variant_name, &fields);
454        output.push('\n');
455
456        self.generated
457            .insert(pascal_variant_name.to_string(), output);
458    }
459
460    /// Generate a tuple variant (multiple fields).
461    fn generate_tuple_variant(
462        &mut self,
463        variant_name: &str,
464        pascal_variant_name: &str,
465        variant: &facet_core::Variant,
466    ) {
467        self.imports.insert("TypedDict");
468        self.imports.insert("Required");
469
470        // Functional form uses runtime expressions — quote forward references.
471        let quote_after: Option<&str> = if is_python_keyword(variant_name) {
472            Some(pascal_variant_name)
473        } else {
474            None
475        };
476
477        let types: Vec<String> = variant
478            .data
479            .fields
480            .iter()
481            .map(|f| self.type_for_shape(f.shape.get(), quote_after))
482            .collect();
483
484        // Note: types should never be empty here because:
485        // - Single-field tuple structs are handled by generate_newtype_variant
486        // - Zero-field tuple variants (e.g., A()) fail to compile in the derive macro
487        let inner_type = format!("tuple[{}]", types.join(", "));
488
489        let fields = [TypedDictField::new(variant_name, inner_type, true, &[])];
490
491        let mut output = String::new();
492        write_typed_dict(&mut output, pascal_variant_name, &fields);
493        output.push('\n');
494
495        self.generated
496            .insert(pascal_variant_name.to_string(), output);
497    }
498
499    /// Generate a struct variant (multiple fields or named fields).
500    fn generate_struct_variant(
501        &mut self,
502        variant_name: &str,
503        pascal_variant_name: &str,
504        variant: &facet_core::Variant,
505    ) {
506        self.imports.insert("TypedDict");
507        self.imports.insert("Required");
508
509        let data_class_name = format!("{}Data", pascal_variant_name);
510
511        // Functional form uses runtime expressions — quote forward references.
512        let needs_functional = variant
513            .data
514            .fields
515            .iter()
516            .any(|f| is_python_keyword(f.effective_name()));
517        let quote_after: Option<&str> = if needs_functional {
518            Some(&data_class_name)
519        } else {
520            None
521        };
522
523        // Generate the data class fields
524        let data_fields: Vec<_> = variant
525            .data
526            .fields
527            .iter()
528            .map(|field| {
529                let field_type = self.type_for_shape(field.shape.get(), quote_after);
530                TypedDictField::new(field.effective_name(), field_type, true, &[])
531            })
532            .collect();
533
534        let mut data_output = String::new();
535        write_typed_dict(&mut data_output, &data_class_name, &data_fields);
536        data_output.push('\n');
537        self.generated.insert(data_class_name.clone(), data_output);
538
539        // Quote data_class_name if wrapper will use functional form (forward ref).
540        let wrapper_type_str =
541            if is_python_keyword(variant_name) && data_class_name.as_str() > pascal_variant_name {
542                format!("\"{}\"", data_class_name)
543            } else {
544                data_class_name.clone()
545            };
546        let wrapper_fields = [TypedDictField::new(
547            variant_name,
548            wrapper_type_str,
549            true,
550            &[],
551        )];
552
553        let mut wrapper_output = String::new();
554        write_typed_dict(&mut wrapper_output, pascal_variant_name, &wrapper_fields);
555        wrapper_output.push('\n');
556
557        self.generated
558            .insert(pascal_variant_name.to_string(), wrapper_output);
559    }
560
561    /// Get the Python type string for a shape.
562    /// `quote_after` quotes user-defined names sorting after it (forward refs).
563    fn type_for_shape(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
564        // Check Def first - these take precedence over transparent wrappers
565        match &shape.def {
566            Def::Scalar => self.scalar_type(shape),
567            Def::Option(opt) => {
568                format!("{} | None", self.type_for_shape(opt.t, quote_after))
569            }
570            Def::List(list) => {
571                format!("list[{}]", self.type_for_shape(list.t, quote_after))
572            }
573            Def::Array(arr) => {
574                format!("list[{}]", self.type_for_shape(arr.t, quote_after))
575            }
576            Def::Set(set) => {
577                format!("list[{}]", self.type_for_shape(set.t, quote_after))
578            }
579            Def::Map(map) => {
580                format!(
581                    "dict[{}, {}]",
582                    self.type_for_shape(map.k, quote_after),
583                    self.type_for_shape(map.v, quote_after)
584                )
585            }
586            Def::Pointer(ptr) => match ptr.pointee {
587                Some(pointee) => self.type_for_shape(pointee, quote_after),
588                None => {
589                    self.imports.insert("Any");
590                    "Any".to_string()
591                }
592            },
593            Def::Undefined => {
594                // User-defined types - queue for generation and return name
595                match &shape.ty {
596                    Type::User(UserType::Struct(st)) => {
597                        // Handle tuples specially - inline them as tuple[...] since their
598                        // type_identifier "(…)" is not a valid Python identifier
599                        if st.kind == StructKind::Tuple {
600                            let types: Vec<String> = st
601                                .fields
602                                .iter()
603                                .map(|f| self.type_for_shape(f.shape.get(), quote_after))
604                                .collect();
605                            format!("tuple[{}]", types.join(", "))
606                        } else {
607                            self.add_shape(shape);
608                            self.maybe_quote(shape.type_identifier, quote_after)
609                        }
610                    }
611                    Type::User(UserType::Enum(_)) => {
612                        self.add_shape(shape);
613                        self.maybe_quote(shape.type_identifier, quote_after)
614                    }
615                    _ => self.inner_type_or_any(shape, quote_after),
616                }
617            }
618            _ => self.inner_type_or_any(shape, quote_after),
619        }
620    }
621
622    /// Wrap a type name in quotes if it is a forward reference (sorts after `quote_after`).
623    fn maybe_quote(&self, name: &str, quote_after: Option<&str>) -> String {
624        if let Some(after) = quote_after
625            && name > after
626        {
627            return format!("\"{}\"", name);
628        }
629        name.to_string()
630    }
631
632    /// Get the inner type for transparent wrappers, or "Any" as fallback.
633    fn inner_type_or_any(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
634        match shape.inner {
635            Some(inner) => self.type_for_shape(inner, quote_after),
636            None => {
637                self.imports.insert("Any");
638                "Any".to_string()
639            }
640        }
641    }
642
643    /// Get the Python type for a scalar shape.
644    fn scalar_type(&mut self, shape: &'static Shape) -> String {
645        match shape.type_identifier {
646            // Strings
647            "String" | "str" | "&str" | "Cow" => "str".to_string(),
648
649            // Booleans
650            "bool" => "bool".to_string(),
651
652            // Integers
653            "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
654            | "i128" | "isize" => "int".to_string(),
655
656            // Floats
657            "f32" | "f64" => "float".to_string(),
658
659            // Char as string
660            "char" => "str".to_string(),
661
662            // Unknown scalar
663            _ => {
664                self.imports.insert("Any");
665                "Any".to_string()
666            }
667        }
668    }
669}
670
671/// Write a doc comment to the output.
672fn write_doc_comment(output: &mut String, doc: &[&str]) {
673    for line in doc {
674        output.push('#');
675        output.push_str(line);
676        output.push('\n');
677    }
678}
679
680/// Convert a snake_case or other string to PascalCase.
681fn to_pascal_case(s: &str) -> String {
682    let mut result = String::new();
683    let mut capitalize_next = true;
684
685    for c in s.chars() {
686        if c == '_' || c == '-' {
687            capitalize_next = true;
688        } else if capitalize_next {
689            result.push(c.to_ascii_uppercase());
690            capitalize_next = false;
691        } else {
692            result.push(c);
693        }
694    }
695
696    result
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702    use facet::Facet;
703
704    #[test]
705    fn test_simple_struct() {
706        #[derive(Facet)]
707        struct User {
708            name: String,
709            age: u32,
710        }
711
712        let py = to_python::<User>(false);
713        insta::assert_snapshot!(py);
714    }
715
716    #[test]
717    fn test_optional_field() {
718        #[derive(Facet)]
719        struct Config {
720            required: String,
721            optional: Option<String>,
722        }
723
724        let py = to_python::<Config>(false);
725        insta::assert_snapshot!(py);
726    }
727
728    #[test]
729    fn test_simple_enum() {
730        #[derive(Facet)]
731        #[repr(u8)]
732        enum Status {
733            Active,
734            Inactive,
735            Pending,
736        }
737
738        let py = to_python::<Status>(false);
739        insta::assert_snapshot!(py);
740    }
741
742    #[test]
743    fn test_vec() {
744        #[derive(Facet)]
745        struct Data {
746            items: Vec<String>,
747        }
748
749        let py = to_python::<Data>(false);
750        insta::assert_snapshot!(py);
751    }
752
753    #[test]
754    fn test_nested_types() {
755        #[derive(Facet)]
756        struct Inner {
757            value: i32,
758        }
759
760        #[derive(Facet)]
761        struct Outer {
762            inner: Inner,
763            name: String,
764        }
765
766        let py = to_python::<Outer>(false);
767        insta::assert_snapshot!(py);
768    }
769
770    #[test]
771    fn test_enum_rename_all_snake_case() {
772        #[derive(Facet)]
773        #[facet(rename_all = "snake_case")]
774        #[repr(u8)]
775        enum ValidationErrorCode {
776            CircularDependency,
777            InvalidNaming,
778            UnknownRequirement,
779        }
780
781        let py = to_python::<ValidationErrorCode>(false);
782        insta::assert_snapshot!(py);
783    }
784
785    #[test]
786    fn test_enum_rename_individual() {
787        #[derive(Facet)]
788        #[repr(u8)]
789        enum GitStatus {
790            #[facet(rename = "dirty")]
791            Dirty,
792            #[facet(rename = "staged")]
793            Staged,
794            #[facet(rename = "clean")]
795            Clean,
796        }
797
798        let py = to_python::<GitStatus>(false);
799        insta::assert_snapshot!(py);
800    }
801
802    #[test]
803    fn test_struct_rename_all_camel_case() {
804        #[derive(Facet)]
805        #[facet(rename_all = "camelCase")]
806        struct ApiResponse {
807            user_name: String,
808            created_at: String,
809            is_active: bool,
810        }
811
812        let py = to_python::<ApiResponse>(false);
813        insta::assert_snapshot!(py);
814    }
815
816    #[test]
817    fn test_struct_rename_individual() {
818        #[derive(Facet)]
819        struct UserProfile {
820            #[facet(rename = "userName")]
821            user_name: String,
822            #[facet(rename = "emailAddress")]
823            email: String,
824        }
825
826        let py = to_python::<UserProfile>(false);
827        insta::assert_snapshot!(py);
828    }
829
830    #[test]
831    fn test_enum_with_data_rename_all() {
832        #[derive(Facet)]
833        #[facet(rename_all = "snake_case")]
834        #[repr(C)]
835        #[allow(dead_code)]
836        enum Message {
837            TextMessage { content: String },
838            ImageUpload { url: String, width: u32 },
839        }
840
841        let py = to_python::<Message>(false);
842        insta::assert_snapshot!(py);
843    }
844
845    #[test]
846    fn test_unit_struct() {
847        #[derive(Facet)]
848        struct Empty;
849
850        let py = to_python::<Empty>(false);
851        insta::assert_snapshot!(py);
852    }
853
854    #[test]
855    fn test_tuple_struct() {
856        #[derive(Facet)]
857        struct Point(f32, f64);
858
859        let py = to_python::<Point>(false);
860        insta::assert_snapshot!(py);
861    }
862
863    #[test]
864    fn test_newtype_struct() {
865        #[derive(Facet)]
866        struct UserId(u64);
867
868        let py = to_python::<UserId>(false);
869        insta::assert_snapshot!(py);
870    }
871
872    #[test]
873    fn test_hashmap() {
874        use std::collections::HashMap;
875
876        #[derive(Facet)]
877        struct Registry {
878            entries: HashMap<String, i32>,
879        }
880
881        let py = to_python::<Registry>(false);
882        insta::assert_snapshot!(py);
883    }
884
885    #[test]
886    fn test_mixed_enum_variants() {
887        #[derive(Facet)]
888        #[repr(C)]
889        #[allow(dead_code)]
890        enum Event {
891            /// Unit variant
892            Empty,
893            /// Newtype variant
894            Id(u64),
895            /// Struct variant
896            Data { name: String, value: f64 },
897        }
898
899        let py = to_python::<Event>(false);
900        insta::assert_snapshot!(py);
901    }
902
903    #[test]
904    fn test_with_imports() {
905        #[derive(Facet)]
906        struct User {
907            name: String,
908            age: u32,
909        }
910
911        let py = to_python::<User>(true);
912        insta::assert_snapshot!(py);
913    }
914
915    #[test]
916    fn test_enum_with_imports() {
917        #[derive(Facet)]
918        #[repr(u8)]
919        enum Status {
920            Active,
921            Inactive,
922        }
923
924        let py = to_python::<Status>(true);
925        insta::assert_snapshot!(py);
926    }
927
928    #[test]
929    fn test_transparent_wrapper() {
930        #[derive(Facet)]
931        #[facet(transparent)]
932        struct UserId(String);
933
934        let py = to_python::<UserId>(false);
935        // This should generate "type UserId = str" not "UserId = str"
936        insta::assert_snapshot!(py);
937    }
938
939    #[test]
940    fn test_transparent_wrapper_with_inner_type() {
941        #[derive(Facet)]
942        struct Inner {
943            value: i32,
944        }
945
946        #[derive(Facet)]
947        #[facet(transparent)]
948        struct Wrapper(Inner);
949
950        let py = to_python::<Wrapper>(false);
951        // This should generate "type Wrapper = Inner" not "Wrapper = Inner"
952        insta::assert_snapshot!(py);
953    }
954
955    #[test]
956    fn test_struct_with_tuple_field() {
957        #[derive(Facet)]
958        struct Container {
959            /// A tuple field containing coordinates
960            coordinates: (i32, i32),
961        }
962
963        let py = to_python::<Container>(false);
964        // This should NOT generate "(…)" as a type - it should properly expand the tuple
965        insta::assert_snapshot!(py);
966    }
967
968    #[test]
969    fn test_struct_with_reserved_keyword_field() {
970        #[derive(Facet)]
971        struct TradeOrder {
972            from: f64,
973            to: f64,
974            quantity: f64,
975        }
976
977        let py = to_python::<TradeOrder>(false);
978        // This should use functional TypedDict syntax since "from" is a Python keyword
979        insta::assert_snapshot!(py);
980    }
981
982    #[test]
983    fn test_struct_with_multiple_reserved_keywords() {
984        #[derive(Facet)]
985        struct ControlFlow {
986            r#if: bool,
987            r#else: String,
988            r#return: i32,
989        }
990
991        let py = to_python::<ControlFlow>(false);
992        // Multiple Python keywords - should use functional syntax
993        insta::assert_snapshot!(py);
994    }
995
996    #[test]
997    fn test_enum_variant_name_is_reserved_keyword() {
998        #[derive(Facet)]
999        #[repr(C)]
1000        #[facet(rename_all = "snake_case")]
1001        #[allow(dead_code)]
1002        enum ImportSource {
1003            /// Import from a file
1004            From(String),
1005            /// Import from a URL
1006            Url(String),
1007        }
1008
1009        let py = to_python::<ImportSource>(false);
1010        // The variant "From" becomes field name "from" which is a Python keyword
1011        // Should use functional TypedDict syntax for the wrapper class
1012        insta::assert_snapshot!(py);
1013    }
1014
1015    #[test]
1016    fn test_enum_data_variant_with_reserved_keyword_field() {
1017        #[derive(Facet)]
1018        #[repr(C)]
1019        #[allow(dead_code)]
1020        enum Transfer {
1021            /// A transfer between accounts
1022            Move {
1023                from: String,
1024                to: String,
1025                amount: f64,
1026            },
1027            /// Cancel the transfer
1028            Cancel,
1029        }
1030
1031        let py = to_python::<Transfer>(false);
1032        // The data variant "Move" has fields "from" and "to" which are Python keywords
1033        // Should use functional TypedDict syntax for the data class
1034        insta::assert_snapshot!(py);
1035    }
1036
1037    #[test]
1038    fn test_hashmap_with_integer_keys() {
1039        use std::collections::HashMap;
1040
1041        #[derive(Facet)]
1042        struct IntKeyedMap {
1043            /// Map with integer keys
1044            counts: HashMap<i32, String>,
1045        }
1046
1047        let py = to_python::<IntKeyedMap>(false);
1048        insta::assert_snapshot!(py);
1049    }
1050
1051    #[test]
1052    fn test_empty_tuple_struct() {
1053        #[derive(Facet)]
1054        struct EmptyTuple();
1055
1056        let py = to_python::<EmptyTuple>(false);
1057        insta::assert_snapshot!(py);
1058    }
1059
1060    #[test]
1061    fn test_hashmap_with_enum_keys() {
1062        use std::collections::HashMap;
1063
1064        #[derive(Facet, Hash, PartialEq, Eq)]
1065        #[repr(u8)]
1066        enum Priority {
1067            Low,
1068            Medium,
1069            High,
1070        }
1071
1072        #[derive(Facet)]
1073        struct TaskMap {
1074            tasks: HashMap<Priority, String>,
1075        }
1076
1077        let py = to_python::<TaskMap>(false);
1078        insta::assert_snapshot!(py);
1079    }
1080
1081    #[test]
1082    fn test_enum_tuple_variant() {
1083        #[derive(Facet)]
1084        #[repr(C)]
1085        #[allow(dead_code)]
1086        enum TupleVariant {
1087            Point(i32, i32),
1088        }
1089        let py = to_python::<TupleVariant>(false);
1090        insta::assert_snapshot!(py);
1091    }
1092
1093    #[test]
1094    fn test_enum_struct_variant_forward_reference() {
1095        // This test verifies that struct variant data classes are quoted
1096        // to handle forward references correctly in Python.
1097        // Without quoting, Python would fail with "NameError: name 'DataData' is not defined"
1098        // because DataData is defined after Data in alphabetical order.
1099        #[derive(Facet)]
1100        #[repr(C)]
1101        #[allow(dead_code)]
1102        enum Message {
1103            // Struct variant with inline fields - generates MessageData class
1104            Data { name: String, value: f64 },
1105        }
1106        let py = to_python::<Message>(false);
1107        insta::assert_snapshot!(py);
1108    }
1109
1110    #[test]
1111    fn test_functional_typed_dict_no_type_keyword() {
1112        // Regression test for https://github.com/facet-rs/facet/issues/2131
1113        #[derive(Facet)]
1114        struct Bug {
1115            from: Option<String>,
1116        }
1117
1118        let py = to_python::<Bug>(false);
1119        assert!(
1120            !py.starts_with("type "),
1121            "functional TypedDict should NOT start with `type` keyword, got:\n{py}"
1122        );
1123        insta::assert_snapshot!(py);
1124    }
1125
1126    #[test]
1127    fn test_functional_typed_dict_forward_ref_quoted() {
1128        // Regression test for https://github.com/facet-rs/facet/issues/2131
1129        #[derive(Facet)]
1130        #[allow(dead_code)]
1131        struct Recipient {
1132            name: String,
1133        }
1134
1135        #[derive(Facet)]
1136        #[allow(dead_code)]
1137        struct Addr {
1138            from: String,
1139            to: Recipient,
1140        }
1141
1142        let py = to_python::<Addr>(false);
1143        assert!(
1144            py.contains("Required[\"Recipient\"]"),
1145            "forward reference in functional TypedDict should be quoted, got:\n{py}"
1146        );
1147        insta::assert_snapshot!(py);
1148    }
1149}