use super::enums::{collect_variant_constructors, enum_has_data_variants};
use crate::core::ir::{EnumDef, TypeRef};
use ahash::AHashSet;
pub const PYO3_DTO_COERCE_HELPER: &str = r#"struct __AlefAlias {
rust: &'static str,
wire: &'static str,
kind: __AlefKind,
nested: &'static [__AlefAlias],
}
enum __AlefKind {
Leaf,
Object,
Seq,
Map,
}
fn __alef_apply_aliases(value: &mut serde_json::Value, aliases: &[__AlefAlias]) {
let serde_json::Value::Object(map) = value else {
return;
};
for alias in aliases {
if !alias.nested.is_empty() {
if let Some(child) = map.get_mut(alias.rust) {
match alias.kind {
__AlefKind::Object => __alef_apply_aliases(child, alias.nested),
__AlefKind::Seq => {
if let serde_json::Value::Array(items) = child {
for item in items.iter_mut() {
__alef_apply_aliases(item, alias.nested);
}
}
}
__AlefKind::Map => {
if let serde_json::Value::Object(entries) = child {
for entry in entries.values_mut() {
__alef_apply_aliases(entry, alias.nested);
}
}
}
__AlefKind::Leaf => {}
}
}
}
if alias.rust != alias.wire {
if let Some(taken) = map.remove(alias.rust) {
map.insert(alias.wire.to_string(), taken);
}
}
}
}
fn __alef_coerce_dto<T: serde::de::DeserializeOwned>(
py: Python<'_>,
value: &Bound<'_, pyo3::types::PyAny>,
aliases: &[__AlefAlias],
) -> PyResult<T> {
// A public @dataclass is not directly JSON-serializable: convert it via `dataclasses.asdict`
// (which emits Rust field names) and rewrite the keys to serde wire names so renamed fields
// survive. A plain dict / JSON-native value already uses wire names and passes straight through.
if value.hasattr("__dataclass_fields__")? {
let as_dict = py.import("dataclasses")?.call_method1("asdict", (value,))?;
let json_str: String = py.import("json")?.call_method1("dumps", (as_dict,))?.extract()?;
let mut json_value: serde_json::Value =
serde_json::from_str(&json_str).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
__alef_apply_aliases(&mut json_value, aliases);
serde_json::from_value(json_value).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
} else {
let json_str: String = py.import("json")?.call_method1("dumps", (value,))?.extract()?;
serde_json::from_str(&json_str).map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
}
}
fn __alef_coerce_dto_seq<T: serde::de::DeserializeOwned>(
py: Python<'_>,
value: &Bound<'_, pyo3::types::PyAny>,
aliases: &[__AlefAlias],
) -> PyResult<Vec<T>> {
let items: Vec<Bound<'_, pyo3::types::PyAny>> = value.extract()?;
let mut out = Vec::with_capacity(items.len());
for item in &items {
out.push(__alef_coerce_dto(py, item, aliases)?);
}
Ok(out)
}
fn __alef_coerce_dto_map<T: serde::de::DeserializeOwned>(
py: Python<'_>,
value: &Bound<'_, pyo3::types::PyAny>,
aliases: &[__AlefAlias],
) -> PyResult<T> {
// Build a wire-keyed JSON object from the mapping's items (each value coerced like a DTO via
// the object helper), then deserialize the whole map into the core type.
let items: Vec<(String, Bound<'_, pyo3::types::PyAny>)> = value.call_method0("items")?.extract()?;
let mut object = serde_json::Map::with_capacity(items.len());
for (key, val) in &items {
object.insert(key.clone(), __alef_coerce_dto(py, val, aliases)?);
}
serde_json::from_value(serde_json::Value::Object(object))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
}"#;
pub fn pyo3_wire_schema_const_name(type_name: &str) -> String {
use heck::ToShoutySnakeCase;
format!("__ALEF_WIRE_{}", type_name.to_shouty_snake_case())
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CoercibleShape {
Object,
Seq,
Map,
}
impl CoercibleShape {
pub fn wire_kind(self) -> &'static str {
match self {
CoercibleShape::Object => "Object",
CoercibleShape::Seq => "Seq",
CoercibleShape::Map => "Map",
}
}
}
pub fn coercible_payload<'a>(ty: &'a TypeRef, coercible: &AHashSet<&str>) -> Option<(&'a str, CoercibleShape)> {
let named_if_coercible = |t: &'a TypeRef| match t {
TypeRef::Named(n) if coercible.contains(n.as_str()) => Some(n.as_str()),
_ => None,
};
match ty {
TypeRef::Optional(inner) => coercible_payload(inner, coercible),
TypeRef::Vec(inner) => named_if_coercible(inner).map(|n| (n, CoercibleShape::Seq)),
TypeRef::Map(_, value) => named_if_coercible(value).map(|n| (n, CoercibleShape::Map)),
TypeRef::Named(_) => named_if_coercible(ty).map(|n| (n, CoercibleShape::Object)),
_ => None,
}
}
pub fn data_enum_needs_dto_coercion(enum_def: &EnumDef, coercible_dto_names: &AHashSet<&str>) -> bool {
if !enum_has_data_variants(enum_def) {
return false;
}
collect_variant_constructors(enum_def).iter().any(|c| {
c.params
.iter()
.any(|p| coercible_payload(&p.ty, coercible_dto_names).is_some())
})
}
pub(crate) fn coercible_field_init(
name: &str,
dto: &str,
shape: CoercibleShape,
optional: bool,
promoted: bool,
) -> String {
let schema = pyo3_wire_schema_const_name(dto);
let helper = match shape {
CoercibleShape::Object => "__alef_coerce_dto",
CoercibleShape::Seq => "__alef_coerce_dto_seq",
CoercibleShape::Map => "__alef_coerce_dto_map",
};
if optional {
format!("{name}.map(|v| {helper}(py, v, {schema})).transpose()?")
} else if promoted {
format!("{name}.map(|v| {helper}(py, v, {schema})).transpose()?.unwrap_or_default()")
} else {
format!("{helper}(py, {name}, {schema})?")
}
}