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