1extern 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
32fn is_python_keyword(name: &str) -> bool {
34 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
44struct 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 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
72fn has_reserved_keyword_field(fields: &[TypedDictField]) -> bool {
74 fields.iter().any(|f| is_python_keyword(f.name))
75}
76
77fn 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
98fn 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 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
119fn 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
128pub 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
135pub struct PythonGenerator {
137 generated: BTreeMap<String, String>,
139 queue: Vec<&'static Shape>,
141 imports: BTreeSet<&'static str>,
143}
144
145impl Default for PythonGenerator {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151impl PythonGenerator {
152 pub const fn new() -> Self {
154 Self {
155 generated: BTreeMap::new(),
156 queue: Vec::new(),
157 imports: BTreeSet::new(),
158 }
159 }
160
161 pub fn add_type<T: Facet<'static>>(&mut self) {
163 self.add_shape(T::SHAPE);
164 }
165
166 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 pub fn finish(mut self, write_imports: bool) -> String {
175 while let Some(shape) = self.queue.pop() {
177 if self.generated.contains_key(shape.type_identifier) {
178 continue;
179 }
180 self.generated
182 .insert(shape.type_identifier.to_string(), String::new());
183 self.generate_shape(shape);
184 }
185
186 let mut output = String::new();
189
190 if write_imports {
192 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 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 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 self.generate_struct(&mut output, shape, st.fields, st.kind);
233 }
234 Type::User(UserType::Enum(en)) => {
235 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 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 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 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 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 let all_fields = self.collect_flat_fields(fields);
342
343 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 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 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 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 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 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 let tag_type = format!("Literal[\"{}\"]", variant_name);
437 fields.push(TypedDictField::new(tag_key, tag_type, true, &[]));
438
439 match variant.data.kind {
441 StructKind::Unit => {
442 }
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 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 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 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 fn unwrap_to_inner_shape(shape: &'static Shape) -> (&'static Shape, bool) {
519 if let Def::Option(opt) = &shape.def {
521 let (inner, _) = Self::unwrap_to_inner_shape(opt.t);
522 return (inner, true);
523 }
524 if let Def::Pointer(ptr) = &shape.def
526 && let Some(pointee) = ptr.pointee
527 {
528 return Self::unwrap_to_inner_shape(pointee);
529 }
530 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 if let Some(proxy_def) = shape.proxy {
537 return Self::unwrap_to_inner_shape(proxy_def.shape);
538 }
539 (shape, false)
540 }
541
542 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 if field.should_skip_serializing_unconditional() {
562 continue;
563 }
564
565 if field.is_flattened() {
566 let (inner_shape, parent_is_optional) =
568 Self::unwrap_to_inner_shape(field.shape.get());
569
570 self.add_shape(inner_shape);
573
574 if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
575 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 result.push((field, force_optional));
591 }
592 } else {
593 result.push((field, force_optional));
594 }
595 }
596 result
597 }
598
599 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 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 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 fields.push(TypedDictField::new(tag_key, tag_type, true, &[]));
665
666 match variant.data.kind {
667 StructKind::Unit => {
668 }
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 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 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 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 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 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 let inner = self.type_for_shape(variant.data.fields[0].shape.get(), None);
766 variant_types.push(inner);
767 }
768 StructKind::TupleStruct => {
769 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn type_for_shape(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
1010 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 match &shape.ty {
1042 Type::User(UserType::Struct(st)) => {
1043 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 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 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 fn scalar_type(&mut self, shape: &'static Shape) -> String {
1091 match shape.type_identifier {
1092 "String" | "str" | "&str" | "Cow" => "str".to_string(),
1094
1095 "bool" => "bool".to_string(),
1097
1098 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
1100 | "i128" | "isize" => "int".to_string(),
1101
1102 "f32" | "f64" => "float".to_string(),
1104
1105 "char" => "str".to_string(),
1107
1108 "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 _ => {
1122 self.imports.insert("Any");
1123 "Any".to_string()
1124 }
1125 }
1126 }
1127}
1128
1129fn 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
1138fn 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 Empty,
1351 Id(u64),
1353 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 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 insta::assert_snapshot!(py);
1411 }
1412
1413 #[test]
1414 fn test_struct_with_tuple_field() {
1415 #[derive(Facet)]
1416 struct Container {
1417 coordinates: (i32, i32),
1419 }
1420
1421 let py = to_python::<Container>(false);
1422 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 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 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 From(String),
1463 Url(String),
1465 }
1466
1467 let py = to_python::<ImportSource>(false);
1468 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 Move {
1481 from: String,
1482 to: String,
1483 amount: f64,
1484 },
1485 Cancel,
1487 }
1488
1489 let py = to_python::<Transfer>(false);
1490 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 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 #[derive(Facet)]
1558 #[repr(C)]
1559 #[allow(dead_code)]
1560 enum Message {
1561 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 #[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 #[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 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 assert!(
1633 !py.contains("inner: Required[Inner]"),
1634 "#[facet(flatten)] — 'inner' should be inlined, not a nested field, got:\n{py}"
1635 );
1636
1637 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 #[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 assert!(
1672 py.contains("class Coords(TypedDict, total=False):"),
1673 "flatten Option — Coords should still be generated, got:\n{py}"
1674 );
1675 assert!(
1677 !py.contains("coords:"),
1678 "flatten Option — 'coords' key should not appear in Entity, got:\n{py}"
1679 );
1680 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 #[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 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 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 #[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 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 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 #[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 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 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 #[derive(Facet)]
1828 struct Dims {
1829 width: u32,
1831 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 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 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 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 #[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 #[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 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 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 #[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 assert!(
2045 !py.contains("Sp: Required"),
2046 "internally tagged — 'Sp' must not appear as an outer key, got:\n{py}"
2047 );
2048 assert!(
2050 py.contains("type: Required[Literal[\"Sp\"]]"),
2051 "internally tagged — tag field missing for Sp variant, got:\n{py}"
2052 );
2053 assert!(
2055 py.contains("first_roll: Required[int]"),
2056 "internally tagged — 'first_roll' should be inlined, got:\n{py}"
2057 );
2058 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 #[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 assert!(
2088 !py.contains("Number: Required"),
2089 "untagged — 'Number' must not appear as an outer key, got:\n{py}"
2090 );
2091 assert!(
2093 py.contains("float"),
2094 "untagged — float should appear directly in the union, got:\n{py}"
2095 );
2096 assert!(
2098 !py.contains("Point: Required"),
2099 "untagged — 'Point' must not appear as an outer key, got:\n{py}"
2100 );
2101 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 #[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 assert!(
2141 !py.contains("product:"),
2142 "flatten tagged enum — 'product' key must not appear, got:\n{py}"
2143 );
2144 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 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 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 assert!(
2173 py.contains("id: Required[str]"),
2174 "flatten tagged enum — base field 'id' must be inlined into variants, got:\n{py}"
2175 );
2176 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 #[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 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 assert!(
2218 py.contains("type: Required[Literal[\"Irs\"]]"),
2219 "tagged newtype struct — tag field missing for Irs variant, got:\n{py}"
2220 );
2221 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 #[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 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 assert!(
2280 py.contains("id: Required[str]"),
2281 "flatten tagged newtype struct — base field 'id' must be inlined, got:\n{py}"
2282 );
2283 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 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 #[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 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 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 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 #[derive(Facet)]
2383 struct WirePoint {
2384 x: f64,
2385 y: f64,
2386 }
2387
2388 #[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 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 #[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 assert!(
2473 !py.contains("internal_x"),
2474 "proxy struct — internal field must not appear, got:\n{py}"
2475 );
2476 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 #[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 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 #[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 assert!(
2572 !py.contains("inner: Required"),
2573 "proxy untagged enum — internal field must not appear, got:\n{py}"
2574 );
2575 assert!(
2577 !py.contains("class MyStruct(TypedDict"),
2578 "proxy untagged enum — must not be a single TypedDict class, got:\n{py}"
2579 );
2580 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}