alef 0.68.0

Opinionated polyglot binding generator for Rust libraries
Documentation
pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str, seen_exceptions: &mut AHashSet<String>) -> String {
    let mut variant_names = Vec::new();
    for variant in &error.variants {
        let variant_name = python_exception_name(&variant.name, &error.name);
        if seen_exceptions.insert(variant_name.clone()) {
            variant_names.push(variant_name);
        }
    }

    let include_base = seen_exceptions.insert(error.name.clone());

    crate::codegen::template_env::render(
        "error_gen/pyo3_error_types.jinja",
        minijinja::context! {
            variant_names => variant_names,
            module_name => module_name,
            error_name => error.name.as_str(),
            include_base => include_base,
        },
    )
}

/// Generate a `to_py_err` converter function that maps each Rust error variant to a Python exception.
/// Uses Error-suffixed names for variant exceptions (N818 compliance).
///
/// When `error.methods` is non-empty, the constructor receives a tuple
/// `(message, code, status_code, is_transient, error_type)` so that the `#[getter]`
/// impl generated by [`gen_pyo3_error_methods_impl`] can read them back. The message stays
/// the message alone — the stable numeric `code` travels as its own tuple element, never
/// interpolated into the human-facing text. (~keep)
pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
    let rust_path = if error.rust_path.is_empty() {
        format!("{core_import}::{}", error.name)
    } else {
        let normalized = error.rust_path.replace('-', "_");
        let segments: Vec<&str> = normalized.split("::").collect();
        if segments.len() > 2 {
            let crate_name = segments[0];
            let error_name = segments[segments.len() - 1];
            format!("{crate_name}::{error_name}")
        } else {
            normalized
        }
    };

    let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
    let has_methods = !error.methods.is_empty();

    // Branch on `has_methods` here, in Rust, rather than inside the template's `{% for %}`
    // loop: a `{%- if %}...{%- else %}...{%- endif %}` nested inside a `{%- for %}` collapses
    // the newline between successive match arms (the leading `-` on each inner tag eats the
    // newline emitted by the previous iteration), producing a single-line `match` body. A flat
    // loop over pre-rendered arm strings has no such interaction. (~keep)
    let ctor_args = |code: u32| -> String {
        if has_methods {
            format!("(msg, {code}u32, e.status_code(), e.is_transient(), e.error_type().to_string())")
        } else {
            "msg".to_string()
        }
    };

    let mut arms = Vec::with_capacity(error.variants.len());
    for variant in &error.variants {
        let pattern = error_variant_wildcard_pattern(&rust_path, variant);
        let variant_exc_name = python_exception_name(&variant.name, &error.name);
        let code = variant
            .taxonomy(&error.rust_path)
            .map_or(crate::core::ir::ApiSurface::FFI_ERROR_CODE_UNKNOWN, |taxonomy| {
                taxonomy.code
            });
        arms.push(format!(
            "{pattern} => {variant_exc_name}::new_err({}),",
            ctor_args(code)
        ));
    }
    let default_arm = format!(
        "_ => {}::new_err({}),",
        error.name,
        ctor_args(crate::core::ir::ApiSurface::FFI_ERROR_CODE_UNKNOWN)
    );

    crate::codegen::template_env::render(
        "error_gen/pyo3_error_converter.jinja",
        minijinja::context! {
            rust_path => rust_path.as_str(),
            fn_name => fn_name.as_str(),
            arms => arms,
            default_arm => default_arm.as_str(),
        },
    )
}

/// Generate `m.add(...)` registration calls for each exception type.
/// Uses Error-suffixed names for variant exceptions (N818 compliance).
/// Prefixes names that would shadow Python builtins (A004 compliance).
pub fn gen_pyo3_error_registration(error: &ErrorDef, seen_registrations: &mut AHashSet<String>) -> Vec<String> {
    let mut registrations = Vec::with_capacity(error.variants.len() + 1);

    for variant in &error.variants {
        let variant_exc_name = python_exception_name(&variant.name, &error.name);
        if seen_registrations.insert(variant_exc_name.clone()) {
            registrations.push(format!(
                "    m.add(\"{}\", m.py().get_type::<{}>())?;",
                variant_exc_name, variant_exc_name
            ));
        }
    }

    if seen_registrations.insert(error.name.clone()) {
        registrations.push(format!(
            "    m.add(\"{}\", m.py().get_type::<{}>())?;",
            error.name, error.name
        ));
    }

    registrations
}

/// Return the converter function name for a given error type.
pub fn converter_fn_name(error: &ErrorDef) -> String {
    format!("{}_to_py_err", to_snake_case(&error.name))
}

pub fn gen_pyo3_error_methods_impl(error: &ErrorDef) -> String {
    if error.methods.is_empty() {
        return String::new();
    }

    let struct_name = format!("{}Info", error.name);
    let snake_name = to_snake_case(&error.name);
    let fn_name = format!("{snake_name}_info");

    let mut fields = vec!["    pub code: u32,".to_string()];
    let mut getters = vec![
        concat!(
            "    /// Stable numeric error code identifying the specific error variant\n",
            "    /// (see the crate's FFI error taxonomy).\n",
            "    #[getter]\n",
            "    fn code(&self) -> u32 {\n",
            "        self.code\n",
            "    }",
        )
        .to_string(),
    ];

    let has_status_code = error.methods.iter().any(|m| m.name == "status_code");
    let has_is_transient = error.methods.iter().any(|m| m.name == "is_transient");
    let has_error_type = error.methods.iter().any(|m| m.name == "error_type");

    if has_status_code {
        fields.push("    pub status_code: u16,".to_string());
        getters.push(
            concat!(
                "    /// HTTP status code for this error (0 means no associated status).\n",
                "    #[getter]\n",
                "    fn status_code(&self) -> u16 {\n",
                "        self.status_code\n",
                "    }",
            )
            .to_string(),
        );
    }
    if has_is_transient {
        fields.push("    pub is_transient: bool,".to_string());
        getters.push(
            concat!(
                "    /// Returns `true` if the error is transient and a retry may succeed.\n",
                "    #[getter]\n",
                "    fn is_transient(&self) -> bool {\n",
                "        self.is_transient\n",
                "    }",
            )
            .to_string(),
        );
    }
    if has_error_type {
        fields.push("    pub error_type: String,".to_string());
        getters.push(
            concat!(
                "    /// Machine-readable error category string for matching and logging.\n",
                "    #[getter]\n",
                "    fn error_type(&self) -> String {\n",
                "        self.error_type.clone()\n",
                "    }",
            )
            .to_string(),
        );
    }
    for method in &error.methods {
        match method.name.as_str() {
            "status_code" | "is_transient" | "error_type" => {}
            other => getters.push(format!(
                "    // Not emitted: getter for method `{other}` on `{struct_name}`"
            )),
        }
    }

    let mut ctor_fields = vec![
        "        code: args\n\
         \x20           .as_ref()\n\
         \x20           .and_then(|a| a.get_item(1).ok())\n\
         \x20           .and_then(|v| v.extract::<u32>().ok())\n\
         \x20           .unwrap_or(0),",
    ];
    if has_status_code {
        ctor_fields.push(
            "        status_code: args\n\
             \x20           .as_ref()\n\
             \x20           .and_then(|a| a.get_item(2).ok())\n\
             \x20           .and_then(|v| v.extract::<u16>().ok())\n\
             \x20           .unwrap_or(0),",
        );
    }
    if has_is_transient {
        ctor_fields.push(
            "        is_transient: args\n\
             \x20           .as_ref()\n\
             \x20           .and_then(|a| a.get_item(3).ok())\n\
             \x20           .and_then(|v| v.extract::<bool>().ok())\n\
             \x20           .unwrap_or(false),",
        );
    }
    if has_error_type {
        ctor_fields.push(
            "        error_type: args\n\
             \x20           .as_ref()\n\
             \x20           .and_then(|a| a.get_item(4).ok())\n\
             \x20           .and_then(|v| v.extract::<String>().ok())\n\
             \x20           .unwrap_or_default(),",
        );
    }

    let struct_def = format!(
        "#[pyclass(name = \"{struct_name}\")]\npub struct {struct_name} {{\n{}\n}}",
        fields.join("\n")
    );

    let impl_block = format!("#[pymethods]\nimpl {struct_name} {{\n{}\n}}", getters.join("\n\n"));

    let free_fn = format!(
        "/// Build a `{struct_name}` from any exception raised by the `{error_name}` hierarchy.\n\
         ///\n\
         /// The converter stores `(message, code, status_code, is_transient, error_type)` in\n\
         /// the exception args tuple; this function extracts those values at indices 1–4.\n\
         #[pyfunction]\n\
         pub fn {fn_name}(err: pyo3::Bound<'_, pyo3::types::PyAny>) -> {struct_name} {{\n\
             let args = err.getattr(\"args\").ok();\n\
             {struct_name} {{\n\
         {ctor}\n\
             }}\n\
         }}",
        error_name = error.name,
        ctor = ctor_fields.join("\n"),
    );

    format!("{struct_def}\n\n{impl_block}\n\n{free_fn}")
}

/// Returns `true` when the error has whitelisted introspection methods that
/// require extended constructor args `(msg, code, status_code, is_transient, error_type)`.
pub fn pyo3_error_has_methods(error: &ErrorDef) -> bool {
    !error.methods.is_empty()
}

/// Return the name of the companion info struct for an error type.
pub fn pyo3_error_info_struct_name(error: &ErrorDef) -> String {
    format!("{}Info", error.name)
}

/// Return the name of the free function that builds the companion info struct.
pub fn pyo3_error_info_fn_name(error: &ErrorDef) -> String {
    format!("{}_info", to_snake_case(&error.name))
}

use crate::core::ir::ErrorDef;
/// Generate a `#[napi]` class struct for the error type that stores the
/// whitelisted introspection method values as fields and exposes them as methods.
///
/// Returns an empty string when `error.methods` is empty.
use ahash::AHashSet;

use super::shared::{error_variant_wildcard_pattern, python_exception_name, to_snake_case};