Skip to main content

alef_codegen/generators/
enums.rs

1use crate::generators::RustBindingConfig;
2use alef_core::ir::EnumDef;
3use alef_core::keywords::PYTHON_KEYWORDS;
4use std::fmt::Write;
5
6/// Returns true if any variant of the enum has data fields.
7/// These enums cannot be represented as flat integer enums in bindings.
8pub fn enum_has_data_variants(enum_def: &EnumDef) -> bool {
9    enum_def.variants.iter().any(|v| !v.fields.is_empty())
10}
11
12/// Returns true if any variant of the enum has a sanitized field.
13///
14/// A sanitized field means the extractor could not resolve the field's concrete type
15/// (e.g. a tuple like `Vec<(String, String)>` that has no direct IR representation).
16/// When this is true the `#[new]` constructor that round-trips via serde/JSON cannot
17/// be generated, because the Python-dict → JSON → core deserialization path would not
18/// produce a valid value for the sanitized field. The forwarding trait impls
19/// (`Default`, `Serialize`, `Deserialize`) are still generated unconditionally since
20/// the wrapper struct always delegates to the core type.
21fn enum_has_sanitized_fields(enum_def: &EnumDef) -> bool {
22    enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.sanitized))
23}
24
25/// Generate a PyO3 data enum as a `#[pyclass]` struct wrapping the core type.
26///
27/// Data enums (tagged unions like `AuthConfig`) can't be flat int enums in PyO3.
28/// Instead, generate a frozen struct with `inner` that accepts a Python dict,
29/// serializes it to JSON, and deserializes into the core Rust type via serde.
30///
31/// When any variant field is sanitized (its type could not be resolved — e.g. contains
32/// `dyn Stream + Send` which is not `Serialize`/`Deserialize`/`Default`), the serde-
33/// based `#[new]` constructor is omitted. The type is still useful as a return value
34/// from Rust (passed back via From impls). The forwarding impls for Default, Serialize,
35/// and Deserialize are always generated regardless of sanitized fields, because the
36/// wrapper struct always delegates to the core type which implements those traits.
37pub fn gen_pyo3_data_enum(enum_def: &EnumDef, core_import: &str) -> String {
38    let name = &enum_def.name;
39    let core_path = crate::conversions::core_enum_path(enum_def, core_import);
40    let has_sanitized = enum_has_sanitized_fields(enum_def);
41    let mut out = String::with_capacity(512);
42
43    writeln!(out, "#[derive(Clone)]").ok();
44    writeln!(out, "#[pyclass(frozen)]").ok();
45    writeln!(out, "pub struct {name} {{").ok();
46    writeln!(out, "    pub(crate) inner: {core_path},").ok();
47    writeln!(out, "}}").ok();
48    writeln!(out).ok();
49
50    writeln!(out, "#[pymethods]").ok();
51    writeln!(out, "impl {name} {{").ok();
52    if has_sanitized {
53        // The core type cannot be serde round-tripped from a Python dict (contains
54        // non-representable variant fields). Omit the #[new] constructor — the type
55        // is still useful as a return value from Rust passed back via From impls.
56        writeln!(out, "}}").ok();
57    } else {
58        writeln!(out, "    #[new]").ok();
59        writeln!(
60            out,
61            "    fn new(py: Python<'_>, value: &Bound<'_, pyo3::types::PyDict>) -> PyResult<Self> {{"
62        )
63        .ok();
64        writeln!(out, "        let json_mod = py.import(\"json\")?;").ok();
65        writeln!(
66            out,
67            "        let json_str: String = json_mod.call_method1(\"dumps\", (value,))?.extract()?;"
68        )
69        .ok();
70        writeln!(out, "        let inner: {core_path} = serde_json::from_str(&json_str)").ok();
71        writeln!(
72            out,
73            "            .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!(\"Invalid {name}: {{e}}\")))?;"
74        )
75        .ok();
76        writeln!(out, "        Ok(Self {{ inner }})").ok();
77        writeln!(out, "    }}").ok();
78        writeln!(out, "}}").ok();
79    }
80    writeln!(out).ok();
81
82    // From binding → core
83    writeln!(out, "impl From<{name}> for {core_path} {{").ok();
84    writeln!(out, "    fn from(val: {name}) -> Self {{ val.inner }}").ok();
85    writeln!(out, "}}").ok();
86    writeln!(out).ok();
87
88    // From core → binding
89    writeln!(out, "impl From<{core_path}> for {name} {{").ok();
90    writeln!(out, "    fn from(val: {core_path}) -> Self {{ Self {{ inner: val }} }}").ok();
91    writeln!(out, "}}").ok();
92    writeln!(out).ok();
93
94    // Serialize: forward to inner so parent structs that derive serde::Serialize compile.
95    // Always generated — the wrapper delegates to the core type which always implements Serialize.
96    writeln!(out, "impl serde::Serialize for {name} {{").ok();
97    writeln!(
98        out,
99        "    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {{"
100    )
101    .ok();
102    writeln!(out, "        self.inner.serialize(serializer)").ok();
103    writeln!(out, "    }}").ok();
104    writeln!(out, "}}").ok();
105    writeln!(out).ok();
106
107    // Default: forward to inner's Default so parent structs that derive Default compile.
108    // Always generated — the wrapper delegates to the core type which always implements Default.
109    writeln!(out, "impl Default for {name} {{").ok();
110    writeln!(
111        out,
112        "    fn default() -> Self {{ Self {{ inner: Default::default() }} }}"
113    )
114    .ok();
115    writeln!(out, "}}").ok();
116    writeln!(out).ok();
117
118    // Deserialize: forward to inner so parent structs that derive serde::Deserialize compile.
119    // Always generated — the wrapper delegates to the core type which always implements Deserialize.
120    writeln!(out, "impl<'de> serde::Deserialize<'de> for {name} {{").ok();
121    writeln!(
122        out,
123        "    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {{"
124    )
125    .ok();
126    writeln!(out, "        let inner = {core_path}::deserialize(deserializer)?;").ok();
127    writeln!(out, "        Ok(Self {{ inner }})").ok();
128    writeln!(out, "    }}").ok();
129    writeln!(out, "}}").ok();
130
131    out
132}
133
134/// Generate an enum.
135pub fn gen_enum(enum_def: &EnumDef, cfg: &RustBindingConfig) -> String {
136    // All enums are generated as unit-variant-only in the binding layer.
137    // Data variants are flattened to unit variants; the From/Into conversions
138    // handle the lossy mapping (discarding / providing defaults for field data).
139    let mut out = String::with_capacity(512);
140    let mut derives: Vec<&str> = cfg.enum_derives.to_vec();
141    // Binding enums always derive Default, Serialize, and Deserialize.
142    // Default: enables using unwrap_or_default() in constructors.
143    // Serialize/Deserialize: required for FFI/type conversion across binding boundaries.
144    derives.push("Default");
145    derives.push("serde::Serialize");
146    derives.push("serde::Deserialize");
147    if !derives.is_empty() {
148        writeln!(out, "#[derive({})]", derives.join(", ")).ok();
149    }
150    for attr in cfg.enum_attrs {
151        writeln!(out, "#[{attr}]").ok();
152    }
153    // Detect PyO3 context so we can rename Python keyword variants via #[pyo3(name = "...")].
154    // The Rust identifier stays unchanged; only the Python-exposed attribute name gets the suffix.
155    let is_pyo3 = cfg.enum_attrs.iter().any(|a| a.contains("pyclass"));
156    writeln!(out, "pub enum {} {{", enum_def.name).ok();
157    for (idx, variant) in enum_def.variants.iter().enumerate() {
158        if is_pyo3 && PYTHON_KEYWORDS.contains(&variant.name.as_str()) {
159            writeln!(out, "    #[pyo3(name = \"{}_\")]", variant.name).ok();
160        }
161        // Mark the first variant as #[default] so derive(Default) works
162        if idx == 0 {
163            writeln!(out, "    #[default]").ok();
164        }
165        writeln!(out, "    {} = {idx},", variant.name).ok();
166    }
167    writeln!(out, "}}").ok();
168
169    out
170}