extern crate alloc;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::Write;
use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType};
fn is_python_keyword(name: &str) -> bool {
const KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
"try", "while", "with", "yield",
];
KEYWORDS.binary_search(&name).is_ok()
}
struct TypedDictField<'a> {
name: &'a str,
type_string: String,
required: bool,
doc: &'a [&'a str],
}
impl<'a> TypedDictField<'a> {
fn new(name: &'a str, type_string: String, required: bool, doc: &'a [&'a str]) -> Self {
Self {
name,
type_string,
required,
doc,
}
}
fn full_type_string(&self) -> String {
if self.required {
format!("Required[{}]", self.type_string)
} else {
self.type_string.clone()
}
}
}
fn has_reserved_keyword_field(fields: &[TypedDictField]) -> bool {
fields.iter().any(|f| is_python_keyword(f.name))
}
fn write_typed_dict_functional(output: &mut String, class_name: &str, fields: &[TypedDictField]) {
writeln!(output, "{} = TypedDict(", class_name).unwrap();
writeln!(output, " \"{}\",", class_name).unwrap();
output.push_str(" {");
let mut first = true;
for field in fields {
if !first {
output.push_str(", ");
}
first = false;
write!(output, "\"{}\": {}", field.name, field.full_type_string()).unwrap();
}
output.push_str("},\n");
output.push_str(" total=False,\n");
output.push(')');
}
fn write_typed_dict_class(output: &mut String, class_name: &str, fields: &[TypedDictField]) {
writeln!(output, "class {}(TypedDict, total=False):", class_name).unwrap();
if fields.is_empty() {
output.push_str(" pass");
return;
}
for field in fields {
for line in field.doc {
output.push_str(" #");
output.push_str(line);
output.push('\n');
}
writeln!(output, " {}: {}", field.name, field.full_type_string()).unwrap();
}
}
fn write_typed_dict(output: &mut String, class_name: &str, fields: &[TypedDictField]) {
if has_reserved_keyword_field(fields) {
write_typed_dict_functional(output, class_name, fields);
} else {
write_typed_dict_class(output, class_name, fields);
}
}
pub fn to_python<T: Facet<'static>>(write_imports: bool) -> String {
let mut generator = PythonGenerator::new();
generator.add_shape(T::SHAPE);
generator.finish(write_imports)
}
pub struct PythonGenerator {
generated: BTreeMap<String, String>,
queue: Vec<&'static Shape>,
imports: BTreeSet<&'static str>,
}
impl Default for PythonGenerator {
fn default() -> Self {
Self::new()
}
}
impl PythonGenerator {
pub const fn new() -> Self {
Self {
generated: BTreeMap::new(),
queue: Vec::new(),
imports: BTreeSet::new(),
}
}
pub fn add_type<T: Facet<'static>>(&mut self) {
self.add_shape(T::SHAPE);
}
pub fn add_shape(&mut self, shape: &'static Shape) {
if !self.generated.contains_key(shape.type_identifier) {
self.queue.push(shape);
}
}
pub fn finish(mut self, write_imports: bool) -> String {
while let Some(shape) = self.queue.pop() {
if self.generated.contains_key(shape.type_identifier) {
continue;
}
self.generated
.insert(shape.type_identifier.to_string(), String::new());
self.generate_shape(shape);
}
let mut output = String::new();
if write_imports {
writeln!(output, "from __future__ import annotations").unwrap();
if !self.imports.is_empty() {
let imports: Vec<&str> = self.imports.iter().copied().collect();
writeln!(output, "from typing import {}", imports.join(", ")).unwrap();
}
output.push('\n');
}
for code in self.generated.values() {
output.push_str(code);
}
output
}
fn generate_shape(&mut self, shape: &'static Shape) {
let mut output = String::new();
if let Some(inner) = shape.inner {
self.add_shape(inner);
let inner_type = self.type_for_shape(inner, None);
write_doc_comment(&mut output, shape.doc);
writeln!(output, "type {} = {}", shape.type_identifier, inner_type).unwrap();
output.push('\n');
self.generated
.insert(shape.type_identifier.to_string(), output);
return;
}
match &shape.ty {
Type::User(UserType::Struct(st)) => {
self.generate_struct(&mut output, shape, st.fields, st.kind);
}
Type::User(UserType::Enum(en)) => {
self.generate_enum(&mut output, shape, en);
}
_ => {
let type_str = self.type_for_shape(shape, None);
write_doc_comment(&mut output, shape.doc);
writeln!(output, "type {} = {}", shape.type_identifier, type_str).unwrap();
output.push('\n');
}
}
self.generated
.insert(shape.type_identifier.to_string(), output);
}
fn generate_struct(
&mut self,
output: &mut String,
shape: &'static Shape,
fields: &'static [Field],
kind: StructKind,
) {
match kind {
StructKind::Unit => {
write_doc_comment(output, shape.doc);
writeln!(output, "{} = None", shape.type_identifier).unwrap();
}
StructKind::TupleStruct | StructKind::Tuple if fields.is_empty() => {
write_doc_comment(output, shape.doc);
writeln!(output, "{} = None", shape.type_identifier).unwrap();
}
StructKind::TupleStruct if fields.len() == 1 => {
let inner_type = self.type_for_shape(fields[0].shape.get(), None);
write_doc_comment(output, shape.doc);
writeln!(output, "{} = {}", shape.type_identifier, inner_type).unwrap();
}
StructKind::TupleStruct | StructKind::Tuple => {
let types: Vec<String> = fields
.iter()
.map(|f| self.type_for_shape(f.shape.get(), None))
.collect();
write_doc_comment(output, shape.doc);
writeln!(
output,
"{} = tuple[{}]",
shape.type_identifier,
types.join(", ")
)
.unwrap();
}
StructKind::Struct => {
self.generate_typed_dict(output, shape, fields);
}
}
output.push('\n');
}
fn generate_typed_dict(
&mut self,
output: &mut String,
shape: &'static Shape,
fields: &'static [Field],
) {
self.imports.insert("TypedDict");
let visible_fields: Vec<_> = fields
.iter()
.filter(|f| !f.flags.contains(facet_core::FieldFlags::SKIP))
.collect();
let needs_functional = visible_fields
.iter()
.any(|f| is_python_keyword(f.effective_name()));
let quote_after: Option<&str> = if needs_functional {
Some(shape.type_identifier)
} else {
None
};
let typed_dict_fields: Vec<_> = visible_fields
.iter()
.map(|f| {
let (type_string, required) = self.field_type_info(f, quote_after);
TypedDictField::new(f.effective_name(), type_string, required, f.doc)
})
.collect();
if typed_dict_fields.iter().any(|f| f.required) {
self.imports.insert("Required");
}
write_doc_comment(output, shape.doc);
write_typed_dict(output, shape.type_identifier, &typed_dict_fields);
}
fn field_type_info(&mut self, field: &Field, quote_after: Option<&str>) -> (String, bool) {
if let Def::Option(opt) = &field.shape.get().def {
(self.type_for_shape(opt.t, quote_after), false)
} else {
(self.type_for_shape(field.shape.get(), quote_after), true)
}
}
fn generate_enum(
&mut self,
output: &mut String,
shape: &'static Shape,
enum_type: &facet_core::EnumType,
) {
let all_unit = enum_type
.variants
.iter()
.all(|v| matches!(v.data.kind, StructKind::Unit));
write_doc_comment(output, shape.doc);
if all_unit {
self.generate_enum_unit_variants(output, shape, enum_type);
} else {
self.generate_enum_with_data(output, shape, enum_type);
}
output.push('\n');
}
fn generate_enum_unit_variants(
&mut self,
output: &mut String,
shape: &'static Shape,
enum_type: &facet_core::EnumType,
) {
self.imports.insert("Literal");
let variants: Vec<String> = enum_type
.variants
.iter()
.map(|v| format!("Literal[\"{}\"]", v.effective_name()))
.collect();
writeln!(
output,
"type {} = {}",
shape.type_identifier,
variants.join(" | ")
)
.unwrap();
}
fn generate_enum_with_data(
&mut self,
output: &mut String,
shape: &'static Shape,
enum_type: &facet_core::EnumType,
) {
let mut variant_class_names = Vec::new();
for variant in enum_type.variants {
let variant_type_name = self.generate_enum_variant(variant);
variant_class_names.push(variant_type_name);
}
writeln!(
output,
"type {} = {}",
shape.type_identifier,
variant_class_names.join(" | ")
)
.unwrap();
}
fn generate_enum_variant(&mut self, variant: &facet_core::Variant) -> String {
let variant_name = variant.effective_name();
let pascal_variant_name = to_pascal_case(variant_name);
match variant.data.kind {
StructKind::Unit => {
self.imports.insert("Literal");
format!("Literal[\"{}\"]", variant_name)
}
StructKind::TupleStruct if variant.data.fields.len() == 1 => {
self.generate_newtype_variant(variant_name, &pascal_variant_name, variant);
pascal_variant_name.to_string()
}
StructKind::TupleStruct => {
self.generate_tuple_variant(variant_name, &pascal_variant_name, variant);
pascal_variant_name.to_string()
}
_ => {
self.generate_struct_variant(variant_name, &pascal_variant_name, variant);
pascal_variant_name.to_string()
}
}
}
fn generate_newtype_variant(
&mut self,
variant_name: &str,
pascal_variant_name: &str,
variant: &facet_core::Variant,
) {
self.imports.insert("TypedDict");
self.imports.insert("Required");
let quote_after: Option<&str> = if is_python_keyword(variant_name) {
Some(pascal_variant_name)
} else {
None
};
let inner_type = self.type_for_shape(variant.data.fields[0].shape.get(), quote_after);
let fields = [TypedDictField::new(variant_name, inner_type, true, &[])];
let mut output = String::new();
write_typed_dict(&mut output, pascal_variant_name, &fields);
output.push('\n');
self.generated
.insert(pascal_variant_name.to_string(), output);
}
fn generate_tuple_variant(
&mut self,
variant_name: &str,
pascal_variant_name: &str,
variant: &facet_core::Variant,
) {
self.imports.insert("TypedDict");
self.imports.insert("Required");
let quote_after: Option<&str> = if is_python_keyword(variant_name) {
Some(pascal_variant_name)
} else {
None
};
let types: Vec<String> = variant
.data
.fields
.iter()
.map(|f| self.type_for_shape(f.shape.get(), quote_after))
.collect();
let inner_type = format!("tuple[{}]", types.join(", "));
let fields = [TypedDictField::new(variant_name, inner_type, true, &[])];
let mut output = String::new();
write_typed_dict(&mut output, pascal_variant_name, &fields);
output.push('\n');
self.generated
.insert(pascal_variant_name.to_string(), output);
}
fn generate_struct_variant(
&mut self,
variant_name: &str,
pascal_variant_name: &str,
variant: &facet_core::Variant,
) {
self.imports.insert("TypedDict");
self.imports.insert("Required");
let data_class_name = format!("{}Data", pascal_variant_name);
let needs_functional = variant
.data
.fields
.iter()
.any(|f| is_python_keyword(f.effective_name()));
let quote_after: Option<&str> = if needs_functional {
Some(&data_class_name)
} else {
None
};
let data_fields: Vec<_> = variant
.data
.fields
.iter()
.map(|field| {
let field_type = self.type_for_shape(field.shape.get(), quote_after);
TypedDictField::new(field.effective_name(), field_type, true, &[])
})
.collect();
let mut data_output = String::new();
write_typed_dict(&mut data_output, &data_class_name, &data_fields);
data_output.push('\n');
self.generated.insert(data_class_name.clone(), data_output);
let wrapper_type_str =
if is_python_keyword(variant_name) && data_class_name.as_str() > pascal_variant_name {
format!("\"{}\"", data_class_name)
} else {
data_class_name.clone()
};
let wrapper_fields = [TypedDictField::new(
variant_name,
wrapper_type_str,
true,
&[],
)];
let mut wrapper_output = String::new();
write_typed_dict(&mut wrapper_output, pascal_variant_name, &wrapper_fields);
wrapper_output.push('\n');
self.generated
.insert(pascal_variant_name.to_string(), wrapper_output);
}
fn type_for_shape(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
match &shape.def {
Def::Scalar => self.scalar_type(shape),
Def::Option(opt) => {
format!("{} | None", self.type_for_shape(opt.t, quote_after))
}
Def::List(list) => {
format!("list[{}]", self.type_for_shape(list.t, quote_after))
}
Def::Array(arr) => {
format!("list[{}]", self.type_for_shape(arr.t, quote_after))
}
Def::Set(set) => {
format!("list[{}]", self.type_for_shape(set.t, quote_after))
}
Def::Map(map) => {
format!(
"dict[{}, {}]",
self.type_for_shape(map.k, quote_after),
self.type_for_shape(map.v, quote_after)
)
}
Def::Pointer(ptr) => match ptr.pointee {
Some(pointee) => self.type_for_shape(pointee, quote_after),
None => {
self.imports.insert("Any");
"Any".to_string()
}
},
Def::Undefined => {
match &shape.ty {
Type::User(UserType::Struct(st)) => {
if st.kind == StructKind::Tuple {
let types: Vec<String> = st
.fields
.iter()
.map(|f| self.type_for_shape(f.shape.get(), quote_after))
.collect();
format!("tuple[{}]", types.join(", "))
} else {
self.add_shape(shape);
self.maybe_quote(shape.type_identifier, quote_after)
}
}
Type::User(UserType::Enum(_)) => {
self.add_shape(shape);
self.maybe_quote(shape.type_identifier, quote_after)
}
_ => self.inner_type_or_any(shape, quote_after),
}
}
_ => self.inner_type_or_any(shape, quote_after),
}
}
fn maybe_quote(&self, name: &str, quote_after: Option<&str>) -> String {
if let Some(after) = quote_after
&& name > after
{
return format!("\"{}\"", name);
}
name.to_string()
}
fn inner_type_or_any(&mut self, shape: &'static Shape, quote_after: Option<&str>) -> String {
match shape.inner {
Some(inner) => self.type_for_shape(inner, quote_after),
None => {
self.imports.insert("Any");
"Any".to_string()
}
}
}
fn scalar_type(&mut self, shape: &'static Shape) -> String {
match shape.type_identifier {
"String" | "str" | "&str" | "Cow" => "str".to_string(),
"bool" => "bool".to_string(),
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
| "i128" | "isize" => "int".to_string(),
"f32" | "f64" => "float".to_string(),
"char" => "str".to_string(),
_ => {
self.imports.insert("Any");
"Any".to_string()
}
}
}
}
fn write_doc_comment(output: &mut String, doc: &[&str]) {
for line in doc {
output.push('#');
output.push_str(line);
output.push('\n');
}
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
#[test]
fn test_simple_struct() {
#[derive(Facet)]
struct User {
name: String,
age: u32,
}
let py = to_python::<User>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_optional_field() {
#[derive(Facet)]
struct Config {
required: String,
optional: Option<String>,
}
let py = to_python::<Config>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_simple_enum() {
#[derive(Facet)]
#[repr(u8)]
enum Status {
Active,
Inactive,
Pending,
}
let py = to_python::<Status>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_vec() {
#[derive(Facet)]
struct Data {
items: Vec<String>,
}
let py = to_python::<Data>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_nested_types() {
#[derive(Facet)]
struct Inner {
value: i32,
}
#[derive(Facet)]
struct Outer {
inner: Inner,
name: String,
}
let py = to_python::<Outer>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_rename_all_snake_case() {
#[derive(Facet)]
#[facet(rename_all = "snake_case")]
#[repr(u8)]
enum ValidationErrorCode {
CircularDependency,
InvalidNaming,
UnknownRequirement,
}
let py = to_python::<ValidationErrorCode>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_rename_individual() {
#[derive(Facet)]
#[repr(u8)]
enum GitStatus {
#[facet(rename = "dirty")]
Dirty,
#[facet(rename = "staged")]
Staged,
#[facet(rename = "clean")]
Clean,
}
let py = to_python::<GitStatus>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_struct_rename_all_camel_case() {
#[derive(Facet)]
#[facet(rename_all = "camelCase")]
struct ApiResponse {
user_name: String,
created_at: String,
is_active: bool,
}
let py = to_python::<ApiResponse>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_struct_rename_individual() {
#[derive(Facet)]
struct UserProfile {
#[facet(rename = "userName")]
user_name: String,
#[facet(rename = "emailAddress")]
email: String,
}
let py = to_python::<UserProfile>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_with_data_rename_all() {
#[derive(Facet)]
#[facet(rename_all = "snake_case")]
#[repr(C)]
#[allow(dead_code)]
enum Message {
TextMessage { content: String },
ImageUpload { url: String, width: u32 },
}
let py = to_python::<Message>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_unit_struct() {
#[derive(Facet)]
struct Empty;
let py = to_python::<Empty>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_tuple_struct() {
#[derive(Facet)]
struct Point(f32, f64);
let py = to_python::<Point>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_newtype_struct() {
#[derive(Facet)]
struct UserId(u64);
let py = to_python::<UserId>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_hashmap() {
use std::collections::HashMap;
#[derive(Facet)]
struct Registry {
entries: HashMap<String, i32>,
}
let py = to_python::<Registry>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_mixed_enum_variants() {
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
enum Event {
Empty,
Id(u64),
Data { name: String, value: f64 },
}
let py = to_python::<Event>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_with_imports() {
#[derive(Facet)]
struct User {
name: String,
age: u32,
}
let py = to_python::<User>(true);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_with_imports() {
#[derive(Facet)]
#[repr(u8)]
enum Status {
Active,
Inactive,
}
let py = to_python::<Status>(true);
insta::assert_snapshot!(py);
}
#[test]
fn test_transparent_wrapper() {
#[derive(Facet)]
#[facet(transparent)]
struct UserId(String);
let py = to_python::<UserId>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_transparent_wrapper_with_inner_type() {
#[derive(Facet)]
struct Inner {
value: i32,
}
#[derive(Facet)]
#[facet(transparent)]
struct Wrapper(Inner);
let py = to_python::<Wrapper>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_struct_with_tuple_field() {
#[derive(Facet)]
struct Container {
coordinates: (i32, i32),
}
let py = to_python::<Container>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_struct_with_reserved_keyword_field() {
#[derive(Facet)]
struct TradeOrder {
from: f64,
to: f64,
quantity: f64,
}
let py = to_python::<TradeOrder>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_struct_with_multiple_reserved_keywords() {
#[derive(Facet)]
struct ControlFlow {
r#if: bool,
r#else: String,
r#return: i32,
}
let py = to_python::<ControlFlow>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_variant_name_is_reserved_keyword() {
#[derive(Facet)]
#[repr(C)]
#[facet(rename_all = "snake_case")]
#[allow(dead_code)]
enum ImportSource {
From(String),
Url(String),
}
let py = to_python::<ImportSource>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_data_variant_with_reserved_keyword_field() {
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
enum Transfer {
Move {
from: String,
to: String,
amount: f64,
},
Cancel,
}
let py = to_python::<Transfer>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_hashmap_with_integer_keys() {
use std::collections::HashMap;
#[derive(Facet)]
struct IntKeyedMap {
counts: HashMap<i32, String>,
}
let py = to_python::<IntKeyedMap>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_empty_tuple_struct() {
#[derive(Facet)]
struct EmptyTuple();
let py = to_python::<EmptyTuple>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_hashmap_with_enum_keys() {
use std::collections::HashMap;
#[derive(Facet, Hash, PartialEq, Eq)]
#[repr(u8)]
enum Priority {
Low,
Medium,
High,
}
#[derive(Facet)]
struct TaskMap {
tasks: HashMap<Priority, String>,
}
let py = to_python::<TaskMap>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_tuple_variant() {
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
enum TupleVariant {
Point(i32, i32),
}
let py = to_python::<TupleVariant>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_enum_struct_variant_forward_reference() {
#[derive(Facet)]
#[repr(C)]
#[allow(dead_code)]
enum Message {
Data { name: String, value: f64 },
}
let py = to_python::<Message>(false);
insta::assert_snapshot!(py);
}
#[test]
fn test_functional_typed_dict_no_type_keyword() {
#[derive(Facet)]
struct Bug {
from: Option<String>,
}
let py = to_python::<Bug>(false);
assert!(
!py.starts_with("type "),
"functional TypedDict should NOT start with `type` keyword, got:\n{py}"
);
insta::assert_snapshot!(py);
}
#[test]
fn test_functional_typed_dict_forward_ref_quoted() {
#[derive(Facet)]
#[allow(dead_code)]
struct Recipient {
name: String,
}
#[derive(Facet)]
#[allow(dead_code)]
struct Addr {
from: String,
to: Recipient,
}
let py = to_python::<Addr>(false);
assert!(
py.contains("Required[\"Recipient\"]"),
"forward reference in functional TypedDict should be quoted, got:\n{py}"
);
insta::assert_snapshot!(py);
}
}