alef 0.58.3

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::core::ir::{MethodDef, TypeDef, TypeRef};
use std::collections::BTreeSet;

use super::types::{escape_kotlin_string, fits_single_line, kotlin_field_default, kotlin_type_with_string_imports};
use crate::backends::kotlin::gen_bindings::helpers::emit_cleaned_kdoc;
use crate::backends::kotlin::gen_bindings::shared::{ValueMethodBridge, kotlin_field_name};
use crate::core::jni::{bridge_method_name, bridgeable_value_methods, is_functional_ref_mut_value_method};
use heck::ToLowerCamelCase;

/// File-private Jackson mapper used to marshal `this` and the parameter object
/// across the JNI boundary for value-type instance methods.
const VALUE_METHOD_MAPPER: &str = "VALUE_METHOD_MAPPER";

pub(crate) fn emit_type_with_imports(
    ty: &TypeDef,
    out: &mut String,
    imports: &mut BTreeSet<String>,
    enum_defaults: &std::collections::HashMap<String, String>,
    sealed_class_names: &std::collections::HashSet<String>,
    default_constructible_types: &std::collections::HashSet<String>,
    value_method_bridge: Option<ValueMethodBridge<'_>>,
) {
    emit_cleaned_kdoc(out, &ty.doc, "");
    if ty.fields.is_empty() {
        out.push_str(&crate::backends::kotlin::template_env::render(
            "empty_class.jinja",
            minijinja::context! {
                name => &ty.name,
            },
        ));
        return;
    }

    // Enumerate before filtering so `original_idx` stays stable for field naming, then drop
    // `binding_excluded` fields entirely — matching every other backend. Keeping them (as the
    // legacy nullable `= null` branch did) leaks force-controlled / internal knobs into the public
    // DTO; a `[crates.exclude].fields` entry must remove the field, not just null its type.
    let visible_fields: Vec<(usize, &crate::core::ir::FieldDef)> = ty
        .fields
        .iter()
        .enumerate()
        .filter(|(_, f)| !f.binding_excluded)
        .collect();

    let field_sealed_annotations: Vec<Option<String>> = visible_fields
        .iter()
        .map(|(_, f)| sealed_class_field_annotation(&f.ty, sealed_class_names))
        .collect();

    let has_field_docs = visible_fields.iter().any(|(_, f)| !f.doc.is_empty());
    let has_field_annotations = visible_fields.iter().any(|(_, f)| f.serde_rename.is_some())
        || field_sealed_annotations.iter().any(Option::is_some);
    // Detect `#[serde(flatten)]` fields. In Rust these collect all unknown
    let has_flatten_field = visible_fields.iter().any(|(_, f)| f.serde_flatten);

    let mut field_strings: Vec<String> = Vec::with_capacity(visible_fields.len());
    for (original_idx, field) in visible_fields.iter() {
        let ty_str = kotlin_type_with_string_imports(&field.ty, field.optional, imports);
        let name = kotlin_field_name(&field.name, *original_idx);
        // collections (`#[serde(skip_serializing_if = "...")]`) or skip a
        // field entirely under a feature gate (`#[serde(skip)]`). Without a
        let (effective_ty_str, default_suffix) = if field.serde_flatten {
            let nullable_ty = if ty_str.ends_with('?') {
                ty_str.clone()
            } else {
                format!("{ty_str}?")
            };
            (nullable_ty, " = null".to_string())
        } else {
            let default_suffix = kotlin_field_default(
                &field.ty,
                field.optional,
                field.typed_default.as_ref(),
                enum_defaults,
                default_constructible_types,
            );
            if default_suffix.contains(".milliseconds") {
                imports.insert("import kotlin.time.Duration.Companion.milliseconds".to_string());
            }
            (ty_str, default_suffix)
        };
        field_strings.push(format!("val {name}: {effective_ty_str}{default_suffix}"));
    }

    // Instance methods are only emitted when a JNI bridge is available to back them.
    // Without one they are dropped rather than stubbed out, so no method that compiles
    // can fail at runtime.
    let bridged_methods: Vec<&MethodDef> = value_method_bridge
        .map(|bridge| bridgeable_value_methods(ty, bridge.serde_type_names))
        .unwrap_or_default();
    let has_instance_methods = !bridged_methods.is_empty();

    let prefix = format!("data class {}", ty.name);
    let use_single_line = !has_field_docs
        && !has_field_annotations
        && !has_flatten_field
        && !has_instance_methods
        && fits_single_line("", &prefix, &field_strings, "");

    if has_flatten_field {
        out.push_str("@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)\n");
    }

    if use_single_line {
        out.push_str(&crate::backends::kotlin::template_env::render(
            "data_class_inline.jinja",
            minijinja::context! {
                prefix => prefix,
                fields => field_strings.join(", "),
            },
        ));
    } else {
        out.push_str(&crate::backends::kotlin::template_env::render(
            "data_class_header_only.jinja",
            minijinja::context! {
                prefix => prefix,
            },
        ));
        for (idx, ((_, field), field_str)) in visible_fields.iter().zip(field_strings.iter()).enumerate() {
            emit_cleaned_kdoc(out, &field.doc, "    ");
            // Emit @JsonProperty when the Rust field carries #[serde(rename = "...")]
            if let Some(rename) = &field.serde_rename {
                out.push_str(&crate::backends::kotlin::template_env::render(
                    "json_property_annotation.jinja",
                    minijinja::context! {
                        indent => "    ",
                        value => escape_kotlin_string(rename),
                    },
                ));
            }
            if let Some(annotation) = &field_sealed_annotations[idx] {
                out.push_str("    ");
                out.push_str(annotation);
                out.push('\n');
            }
            out.push_str(&crate::backends::kotlin::template_env::render(
                "data_class_field_line.jinja",
                minijinja::context! {
                    indent => "    ",
                    field => field_str,
                },
            ));
        }
        out.push_str(&crate::backends::kotlin::template_env::render(
            "data_class_close.jinja",
            minijinja::context! {
                indent => "",
                suffix => if has_instance_methods { " {" } else { "" },
            },
        ));
    }

    if let Some(bridge) = value_method_bridge {
        for &method in &bridged_methods {
            emit_value_method(out, ty, method, imports, bridge);
        }
    }

    if has_instance_methods {
        out.push_str("}\n");
        emit_value_method_mapper(out);
    }
}

/// Emit one data-class instance method backed by a JNI value-method shim.
///
/// The receiver is marshalled as JSON (`this`), parameters as a JSON object
/// keyed by the Rust parameter name, and the result comes back as either a JNI
/// primitive or a JSON string that Jackson reads into the declared Kotlin type.
/// Delegating rather than reimplementing keeps behaviour the Kotlin side cannot
/// see — argument clamping, validation messages — identical to the core library.
fn emit_value_method(
    out: &mut String,
    ty: &TypeDef,
    method: &MethodDef,
    imports: &mut BTreeSet<String>,
    bridge: ValueMethodBridge<'_>,
) {
    let method_name = heck::AsLowerCamelCase(method.name.as_str()).to_string();
    let returns_receiver = is_functional_ref_mut_value_method(method);
    let return_type = if returns_receiver {
        TypeRef::Named(ty.name.clone())
    } else {
        method.return_type.clone()
    };
    let return_type_str = kotlin_type_with_string_imports(&return_type, false, imports);

    let params_sig: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let ptype = kotlin_type_with_string_imports(&p.ty, p.optional, imports);
            let pname = p.name.to_lower_camel_case();
            format!("{pname}: {ptype}")
        })
        .collect();

    out.push('\n');
    emit_cleaned_kdoc(out, &method.doc, "    ");
    out.push_str("    fun ");
    out.push_str(&method_name);
    out.push('(');
    out.push_str(&params_sig.join(", "));
    out.push_str("): ");
    out.push_str(&return_type_str);
    out.push_str(" {\n");
    out.push_str(&format!(
        "        val selfJson = {VALUE_METHOD_MAPPER}.writeValueAsString(this)\n"
    ));

    let mut call_args = vec!["selfJson".to_string()];
    if !method.params.is_empty() {
        out.push_str(&format!(
            "        val requestJson = {VALUE_METHOD_MAPPER}.writeValueAsString(\n            mapOf(\n"
        ));
        for param in &method.params {
            let key = escape_kotlin_string(&param.name.replace('-', "_"));
            let value = param.name.to_lower_camel_case();
            out.push_str(&format!("                \"{key}\" to {value},\n"));
        }
        out.push_str("            ),\n        )\n");
        call_args.push("requestJson".to_string());
    }

    let native_call = format!(
        "{}.{}({})",
        bridge.bridge_class,
        bridge_method_name(&ty.name, &method.name),
        call_args.join(", ")
    );

    match &return_type {
        TypeRef::Unit => out.push_str(&format!("        {native_call}\n")),
        TypeRef::Primitive(_) | TypeRef::String => out.push_str(&format!("        return {native_call}\n")),
        _ => {
            out.push_str(&format!("        val resultJson = {native_call}\n"));
            out.push_str(&format!("        return {VALUE_METHOD_MAPPER}.readValue(\n"));
            out.push_str("            resultJson,\n");
            out.push_str(&format!(
                "            object : com.fasterxml.jackson.core.type.TypeReference<{return_type_str}>() {{}},\n"
            ));
            out.push_str("        )\n");
        }
    }
    out.push_str("    }\n");
}

/// Emit the file-private Jackson mapper backing value-method marshalling.
///
/// Emitted at most once per file; the Kotlin visibility keeps it from colliding
/// with the mapper any other generated file declares.
fn emit_value_method_mapper(out: &mut String) {
    let declaration = crate::backends::kotlin::template_env::render(
        "value_method_mapper.jinja",
        minijinja::context! {
            name => VALUE_METHOD_MAPPER,
        },
    );
    if !out.contains(&declaration) {
        out.push('\n');
        out.push_str(&declaration);
    }
}

/// Return the `@field:JsonSerialize(...)` annotation source needed for a
/// field whose declared type references a sealed class, or `None` if the
/// type does not reference a sealed class.
///
/// Recognised shapes (Optional layers are unwrapped first):
/// - `Named(sealed)` → `@field:JsonSerialize(\`as\` = sealed::class)`
/// - `Vec<Named(sealed)>` → `@field:JsonSerialize(contentAs = sealed::class)`
/// - `Map<_, Named(sealed)>` → `@field:JsonSerialize(contentAs = sealed::class)`
///
/// Other shapes (nested generics, sealed inside `Map` key, …) are ignored —
/// they don't appear in the current codebase, and `contentAs` cannot express
/// them anyway.
fn sealed_class_field_annotation(
    ty: &TypeRef,
    sealed_class_names: &std::collections::HashSet<String>,
) -> Option<String> {
    let base = match ty {
        TypeRef::Optional(inner) => inner.as_ref(),
        other => other,
    };
    match base {
        TypeRef::Named(name) if sealed_class_names.contains(name) => Some(format!(
            "@field:com.fasterxml.jackson.databind.annotation.JsonSerialize(`as` = {name}::class)"
        )),
        TypeRef::Vec(inner) => {
            let inner_base = match inner.as_ref() {
                TypeRef::Optional(i) => i.as_ref(),
                other => other,
            };
            if let TypeRef::Named(name) = inner_base
                && sealed_class_names.contains(name)
            {
                return Some(format!(
                    "@field:com.fasterxml.jackson.databind.annotation.JsonSerialize(contentAs = {name}::class)"
                ));
            }
            None
        }
        TypeRef::Map(_, value) => {
            let value_base = match value.as_ref() {
                TypeRef::Optional(i) => i.as_ref(),
                other => other,
            };
            if let TypeRef::Named(name) = value_base
                && sealed_class_names.contains(name)
            {
                return Some(format!(
                    "@field:com.fasterxml.jackson.databind.annotation.JsonSerialize(contentAs = {name}::class)"
                ));
            }
            None
        }
        _ => None,
    }
}