alef 0.23.34

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::core::ir::ErrorDef;
use std::collections::BTreeSet;

use super::types::{fits_single_line, kotlin_type_with_string_imports, kotlin_zero_value};
use crate::backends::kotlin::gen_bindings::helpers::emit_cleaned_kdoc;
use crate::backends::kotlin::gen_bindings::shared::{kotlin_field_name, to_lower_camel};

fn interpolate_error_message_template(template: &str) -> String {
    let mut out = String::with_capacity(template.len());
    let mut remaining = template;
    while let Some(open) = remaining.find('{') {
        let after_open = &remaining[open + 1..];
        if let Some(close) = after_open.find('}') {
            let token = &after_open[..close];
            if token.chars().all(|c| c.is_ascii_digit()) && !token.is_empty() {
                out.push_str(&remaining[..open]);
                let after_close = &after_open[close + 1..];
                let next_is_ident_cont = after_close
                    .chars()
                    .next()
                    .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_');
                if next_is_ident_cont {
                    out.push_str("${field");
                    out.push_str(token);
                    out.push('}');
                } else {
                    out.push_str("$field");
                    out.push_str(token);
                }
                remaining = &remaining[open + 1 + close + 1..];
                continue;
            }
        }
        out.push_str(&remaining[..open + 1]);
        remaining = &remaining[open + 1..];
    }
    out.push_str(remaining);
    out
}

pub(crate) fn emit_error_type_with_imports(error: &ErrorDef, out: &mut String, imports: &mut BTreeSet<String>) {
    emit_cleaned_kdoc(out, &error.doc, "");
    out.push_str(&crate::backends::kotlin::template_env::render(
        "error_sealed_class_header.jinja",
        minijinja::context! {
            name => &error.name,
        },
    ));
    for variant in &error.variants {
        if variant.is_unit {
            let raw_msg = variant.message_template.as_deref().unwrap_or(&variant.name);
            // Unit variants have no fields so {N} tokens would be invalid in
            // practice, but run through the interpolator for consistency and
            // to avoid emitting literal placeholder tokens if they appear.
            let message = interpolate_error_message_template(raw_msg);
            out.push_str(&crate::backends::kotlin::template_env::render(
                "error_object_variant.jinja",
                minijinja::context! {
                    name => &variant.name,
                    parent_name => &error.name,
                    message => message,
                },
            ));
        } else {
            let raw_msg = variant.message_template.as_deref().unwrap_or(&variant.name);
            let message = interpolate_error_message_template(raw_msg);

            // Pre-build field strings for the ktfmt single-line heuristic.
            // Each entry includes the optional `override` modifier for `message` fields.
            let mut err_field_strings: Vec<String> = Vec::with_capacity(variant.fields.len());
            for (idx, f) in variant.fields.iter().enumerate() {
                let ty_str = kotlin_type_with_string_imports(&f.ty, f.optional, imports);
                let name = kotlin_field_name(&f.name, idx);
                let modifier = if name == "message" { "override " } else { "" };
                err_field_strings.push(format!("{modifier}val {name}: {ty_str}"));
            }

            let err_prefix = format!("data class {}", variant.name);
            let err_suffix = format!(" : {}(\"{message}\")", error.name);
            let use_single_line = fits_single_line("    ", &err_prefix, &err_field_strings, &err_suffix);

            if use_single_line {
                out.push_str(&crate::backends::kotlin::template_env::render(
                    "error_variant_inline.jinja",
                    minijinja::context! {
                        err_prefix => err_prefix,
                        fields => err_field_strings.join(", "),
                        err_suffix => err_suffix,
                    },
                ));
            } else {
                out.push_str(&crate::backends::kotlin::template_env::render(
                    "error_variant_header.jinja",
                    minijinja::context! {
                        err_prefix => err_prefix,
                    },
                ));
                for field_str in &err_field_strings {
                    out.push_str(&crate::backends::kotlin::template_env::render(
                        "error_variant_field.jinja",
                        minijinja::context! {
                            field => field_str,
                        },
                    ));
                }
                out.push_str(&crate::backends::kotlin::template_env::render(
                    "error_variant_close_multiline.jinja",
                    minijinja::context! {
                        err_suffix => err_suffix,
                    },
                ));
            }
        }
    }
    // Emit `open val` property declarations with sensible defaults for each
    // whitelisted introspection method (status_code, is_transient, error_type).
    // The JNI bridge throws a flat `<App>BridgeException` rather than
    // constructing sealed-class variants, so requiring every variant to
    // override these abstract properties would break compilation. Each variant
    // can still opt into a concrete override when domain code constructs the
    // error directly, while the defaults keep the sealed class self-contained.
    for method in error.methods.iter().filter(|m| !m.sanitized) {
        let prop_name = to_lower_camel(&method.name);
        let ty_str = kotlin_type_with_string_imports(&method.return_type, false, imports);
        let default = kotlin_zero_value(&ty_str);
        out.push_str(&crate::backends::kotlin::template_env::render(
            "error_open_property.jinja",
            minijinja::context! {
                prop_name => prop_name,
                ty => ty_str,
                default => default,
            },
        ));
    }
    out.push_str("}\n");
}