use super::dto_coercion::{coercible_field_init, coercible_payload};
use crate::codegen::generators::RustBindingConfig;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{EnumDef, TypeRef};
use ahash::AHashSet;
pub fn enum_has_data_variants(enum_def: &EnumDef) -> bool {
enum_def.variants.iter().any(|v| !v.fields.is_empty())
}
fn enum_has_sanitized_fields(enum_def: &EnumDef) -> bool {
enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.sanitized))
}
pub fn gen_pyo3_data_enum(enum_def: &EnumDef, core_import: &str) -> String {
gen_pyo3_data_enum_with_mapper(enum_def, core_import, None)
}
pub fn gen_pyo3_data_enum_with_mapper(
enum_def: &EnumDef,
core_import: &str,
mapper: Option<&dyn TypeMapper>,
) -> String {
gen_pyo3_data_enum_with_coercion(enum_def, core_import, mapper, &AHashSet::new())
}
pub fn gen_pyo3_data_enum_with_coercion(
enum_def: &EnumDef,
core_import: &str,
mapper: Option<&dyn TypeMapper>,
coercible_dto_names: &AHashSet<&str>,
) -> String {
let name = &enum_def.name;
let core_path = crate::codegen::conversions::core_enum_path(enum_def, core_import);
let has_sanitized = enum_has_sanitized_fields(enum_def);
let has_default = enum_def.has_default || enum_def.variants.iter().any(|v| v.is_default);
let string_methods_content = crate::codegen::template_env::render(
"generators/enums/enum_string_methods.jinja",
minijinja::context! {
name => name,
value_expr => "&self.inner",
},
);
let mut variant_accessors = String::new();
write_pyo3_variant_accessors(&mut variant_accessors, enum_def, &core_path);
let mut serde_tag_content = String::new();
if let Some(tag_field) = &enum_def.serde_tag {
write_pyo3_serde_tag_getter(&mut serde_tag_content, tag_field);
}
let variant_constructors_content = match mapper {
Some(m) => gen_pyo3_enum_variant_constructors_content(enum_def, &core_path, m, coercible_dto_names),
None => String::new(),
};
crate::codegen::template_env::render(
"generators/enums/pyo3_data_enum.jinja",
minijinja::context! {
name => name,
core_path => core_path,
has_sanitized => has_sanitized,
has_default => has_default,
string_methods_content => string_methods_content,
variant_accessors_content => variant_accessors,
serde_tag_content => serde_tag_content,
serde_tag => enum_def.serde_tag,
variant_constructors_content => variant_constructors_content,
},
)
}
pub(crate) struct VariantConstructor<'a> {
pub(crate) variant_name: &'a str,
pub(crate) snake_name: String,
pub(crate) params: Vec<crate::core::ir::ParamDef>,
pub(crate) boxed: Vec<bool>,
}
pub(crate) fn collect_variant_constructors(enum_def: &EnumDef) -> Vec<VariantConstructor<'_>> {
use crate::codegen::naming::pascal_to_snake;
use crate::core::ir::ParamDef;
let method_names: ahash::AHashSet<&str> = enum_def
.methods
.iter()
.filter(|m| !m.binding_excluded)
.map(|m| m.name.as_str())
.collect();
enum_def
.variants
.iter()
.filter(|v| {
!v.fields.is_empty()
&& !v.is_tuple
&& !v.binding_excluded
&& !v.fields.iter().any(|f| f.sanitized || f.binding_excluded)
})
.filter_map(|v| {
let snake_name = pascal_to_snake(&v.name);
if method_names.contains(snake_name.as_str()) {
return None;
}
let params = v
.fields
.iter()
.map(|f| ParamDef {
name: f.name.clone(),
ty: f.ty.clone(),
optional: f.optional,
default: f.default.clone(),
sanitized: f.sanitized,
typed_default: f.typed_default.clone(),
newtype_wrapper: f.newtype_wrapper.clone(),
original_type: f.original_type.clone(),
core_wrapper: f.core_wrapper.clone(),
..ParamDef::default()
})
.collect();
let boxed = v.fields.iter().map(|f| f.is_boxed).collect();
Some(VariantConstructor {
variant_name: &v.name,
snake_name,
params,
boxed,
})
})
.collect()
}
pub(crate) fn variant_field_init(
param: &crate::core::ir::ParamDef,
promoted: bool,
cast_uints_to_i32: bool,
cast_large_ints_to_f64: bool,
is_boxed: bool,
) -> String {
use crate::codegen::conversions::helpers::{core_prim_str, needs_f64_cast, needs_i32_cast};
use crate::core::ir::TypeRef;
let name = ¶m.name;
if let TypeRef::Primitive(prim) = ¶m.ty {
let needs_cast =
(cast_uints_to_i32 && needs_i32_cast(prim)) || (cast_large_ints_to_f64 && needs_f64_cast(prim));
if needs_cast {
let core_ty = core_prim_str(prim);
return if param.optional {
format!("{name}.map(|v| v as {core_ty})")
} else if promoted {
format!("{name}.unwrap_or_default() as {core_ty}")
} else {
format!("{name} as {core_ty}")
};
}
}
if param.optional {
let inner = match ¶m.ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
return match inner {
TypeRef::Named(_) if is_boxed => format!("{name}.map(Into::into).map(Box::new)"),
TypeRef::Named(_) | TypeRef::Path => format!("{name}.map(Into::into)"),
TypeRef::Json => format!("{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())"),
TypeRef::Char => format!("{name}.and_then(|s| s.chars().next())"),
TypeRef::Duration => format!("{name}.map(std::time::Duration::from_millis)"),
TypeRef::Bytes => format!("{name}.map(|v| v.to_vec().into())"),
TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
format!("{name}.map(|v| v.into_iter().map(Into::into).collect())")
}
TypeRef::Vec(_) => format!("{name}.map(|v| v.into_iter().collect())"),
_ => name.clone(),
};
}
let base = if promoted {
format!("{name}.unwrap_or_default()")
} else {
name.clone()
};
match ¶m.ty {
TypeRef::Named(_) if is_boxed => format!("Box::new({base}.into())"),
TypeRef::Named(_) | TypeRef::Path => format!("{base}.into()"),
TypeRef::Json => format!("serde_json::from_str(&{base}).unwrap_or_default()"),
TypeRef::Char => format!("{base}.chars().next().unwrap_or('*')"),
TypeRef::Duration => format!("std::time::Duration::from_millis({base})"),
TypeRef::Bytes => format!("{base}.to_vec().into()"),
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
format!("{base}.into_iter().map(Into::into).collect()")
}
TypeRef::Vec(_) => format!("{base}.into_iter().collect()"),
_ => base,
}
}
fn gen_pyo3_enum_variant_constructors_content(
enum_def: &EnumDef,
core_path: &str,
mapper: &dyn TypeMapper,
coercible_dto_names: &AHashSet<&str>,
) -> String {
use crate::codegen::shared::{function_params, function_sig_defaults, is_promoted_optional};
let constructors = collect_variant_constructors(enum_def);
if constructors.is_empty() {
return String::new();
}
let is_coercible = |ty: &TypeRef| coercible_payload(ty, coercible_dto_names).is_some();
let map_fn = |ty: &TypeRef| {
if is_coercible(ty) {
"&Bound<'_, pyo3::types::PyAny>".to_string()
} else {
mapper.map_type(ty)
}
};
let mut out = String::new();
for ctor in &constructors {
let needs_coercion = ctor.params.iter().any(|p| is_coercible(&p.ty));
let params_str = function_params(&ctor.params, &map_fn);
let params_str = if needs_coercion {
format!("py: Python<'_>, {params_str}")
} else {
params_str
};
let field_inits: Vec<String> = ctor
.params
.iter()
.enumerate()
.map(|(idx, p)| {
let promoted = is_promoted_optional(&ctor.params, idx);
let expr = if let Some((dto, shape)) = coercible_payload(&p.ty, coercible_dto_names) {
coercible_field_init(&p.name, dto, shape, p.optional, promoted)
} else {
variant_field_init(p, promoted, false, false, ctor.boxed[idx])
};
if expr == p.name {
p.name.clone()
} else {
format!("{}: {expr}", p.name)
}
})
.collect();
let body_lines = vec![
crate::codegen::template_env::render(
"generators/enums/pyo3_variant_constructor_body.jinja",
minijinja::context! {
core_path => core_path,
variant_name => ctor.variant_name,
field_inits => field_inits,
},
)
.trim_end()
.to_string(),
];
let rust_fn_name = format!("_factory_{}", ctor.snake_name);
let has_optional = ctor.params.iter().any(|p| p.optional);
let signature_defaults = if has_optional {
function_sig_defaults(&ctor.params)
} else {
String::new()
};
out.push_str(&crate::codegen::template_env::render(
"generators/enums/pyo3_factory_method.jinja",
minijinja::context! {
doc_lines => Vec::<String>::new(),
has_pyo3_name => true,
pyo3_name => ctor.snake_name,
has_signature => has_optional,
signature_defaults => signature_defaults,
rust_fn_name => rust_fn_name,
params => params_str,
returns_result => needs_coercion,
body_lines => body_lines,
},
));
out.push_str("\n\n");
}
out.trim_end().to_string()
}
fn to_pyo3_screaming(name: &str) -> String {
use heck::ToShoutySnakeCase;
let chars: Vec<char> = name.chars().collect();
let upper_prefix_len = chars.iter().take_while(|c| c.is_uppercase()).count();
if upper_prefix_len >= 2 && chars[upper_prefix_len..].iter().all(|c| c.is_lowercase() || *c == '_') {
name.to_ascii_uppercase()
} else {
name.to_shouty_snake_case()
}
}
fn apply_rename_all(name: &str, rule: &str) -> String {
use heck::{ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
match rule {
"" => name.to_string(),
"lowercase" => name.to_ascii_lowercase(),
"UPPERCASE" => name.to_ascii_uppercase(),
"snake_case" => name.to_snake_case(),
"kebab-case" => name.to_kebab_case(),
"camelCase" => name.to_lower_camel_case(),
"PascalCase" => name.to_upper_camel_case(),
"SCREAMING_SNAKE_CASE" => name.to_shouty_snake_case(),
"SCREAMING-KEBAB-CASE" => name.to_shouty_kebab_case(),
_ => name.to_string(),
}
}
pub fn gen_enum(enum_def: &EnumDef, cfg: &RustBindingConfig) -> String {
let mut derives: Vec<&str> = cfg.enum_derives.to_vec();
derives.push("Default");
derives.push("serde::Serialize");
derives.push("serde::Deserialize");
let is_pyo3 = cfg.enum_attrs.iter().any(|a| a.contains("pyclass"));
let default_idx = enum_def.variants.iter().position(|v| v.is_default).unwrap_or(0);
let serde_rename_all = enum_def.serde_rename_all.as_deref().unwrap_or("");
let variants: Vec<_> = enum_def
.variants
.iter()
.enumerate()
.map(|(idx, v)| {
let pyo3_name = if is_pyo3 {
to_pyo3_screaming(&v.name)
} else {
String::new()
};
let wire_name = v
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&v.name, serde_rename_all));
minijinja::context! {
name => v.name.clone(),
idx => idx,
is_default => idx == default_idx,
has_pyo3_rename => is_pyo3,
pyo3_name => pyo3_name,
serde_rename => v.serde_rename.clone().unwrap_or_default(),
wire_name => wire_name,
}
})
.collect();
let string_methods = if is_pyo3 {
crate::codegen::template_env::render(
"generators/enums/enum_string_methods.jinja",
minijinja::context! {
name => enum_def.name,
value_expr => "self",
},
)
} else {
String::new()
};
crate::codegen::template_env::render(
"generators/enums/enum_definition.jinja",
minijinja::context! {
enum_name => enum_def.name,
derives => derives.join(", "),
serde_rename_all => serde_rename_all,
enum_attrs => cfg.enum_attrs.to_vec(),
variants => variants,
is_pyo3 => is_pyo3,
string_methods => string_methods,
},
)
}
const RUST_KEYWORDS: &[&str] = &[
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate", "do", "dyn", "else",
"enum", "extern", "false", "final", "fn", "for", "if", "impl", "in", "let", "loop", "macro", "match", "mod",
"move", "mut", "override", "priv", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
"true", "try", "type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
];
pub(crate) fn write_pyo3_variant_accessors(out: &mut String, enum_def: &EnumDef, core_path: &str) {
use crate::core::ir::TypeRef;
for variant in &enum_def.variants {
let variant_name_lower = crate::codegen::naming::pascal_to_snake(&variant.name);
let fn_name = if RUST_KEYWORDS.contains(&variant_name_lower.as_str()) {
format!("r#{}", variant_name_lower)
} else {
variant_name_lower.clone()
};
if variant.fields.len() == 1 {
let field = &variant.fields[0];
let is_tuple_field = field
.name
.strip_prefix('_')
.is_some_and(|s| s.chars().all(|c| c.is_ascii_digit()));
if is_tuple_field {
if let TypeRef::Named(inner_type_name) = &field.ty {
let variant_pascal = &variant.name;
let clone_expr = if field.is_boxed {
"(**data).clone().into()".to_string()
} else {
"data.clone().into()".to_string()
};
out.push('\n');
out.push_str(" #[getter]\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/getter_accessor.jinja",
minijinja::context! {
fn_name => &fn_name,
inner_type_name => inner_type_name,
},
));
out.push_str(" match &self.inner {\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/match_variant.jinja",
minijinja::context! {
core_path => &core_path,
variant_pascal => variant_pascal,
clone_expr => &clone_expr,
},
));
out.push_str(" _ => None,\n");
out.push_str(" }\n");
out.push_str(" }\n");
continue;
}
}
}
out.push('\n');
out.push_str(" #[getter]\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/py_dict_getter.jinja",
minijinja::context! {
fn_name => &fn_name,
},
));
out.push_str(" let json = serde_json::to_value(&self.inner)\n");
out.push_str(" .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;\n");
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("tag");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/tag_field_check.jinja",
minijinja::context! {
tag_field => tag_field,
},
));
out.push_str(" let tag_value = json.get(tag_field)\n");
out.push_str(" .and_then(|v| v.as_str())\n");
out.push_str(" .unwrap_or(\"\");\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/variant_tag_match.jinja",
minijinja::context! {
variant_name_lower => &variant_name_lower,
},
));
out.push_str(" return Ok(None);\n");
out.push_str(" }\n");
out.push_str(" let json_str = json.to_string();\n");
out.push_str(" let json_mod = py.import(\"json\")?;\n");
out.push_str(" let py_dict = json_mod.call_method1(\"loads\", (&json_str,))?.cast_into::<pyo3::types::PyDict>()?;\n");
out.push_str(" Ok(Some(py_dict.unbind()))\n");
out.push_str(" }\n");
}
}
pub(crate) fn write_pyo3_serde_tag_getter(out: &mut String, tag_field: &str) {
let fn_name = if RUST_KEYWORDS.contains(&tag_field) {
format!("r#{tag_field}")
} else {
tag_field.to_string()
};
out.push('\n');
out.push_str(" #[getter]\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/tag_getter_header.jinja",
minijinja::context! {
fn_name => &fn_name,
},
));
out.push_str(" let json = serde_json::to_value(&self.inner)\n");
out.push_str(" .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/json_get_field.jinja",
minijinja::context! {
tag_field => tag_field,
},
));
out.push_str(" .and_then(|v| v.as_str())\n");
out.push_str(" .map(String::from)\n");
out.push_str(&crate::codegen::template_env::render(
"generators/enums/json_get_error.jinja",
minijinja::context! {
tag_field => tag_field,
},
));
out.push_str(" }\n");
}
#[cfg(test)]
mod tests;