alef 0.19.4

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Kotlin user-facing trait bridge support.
//!
//! For Kotlin Android backends, this module generates:
//! 1. `interface I{TraitName}` — Kotlin interface with Plugin lifecycle + trait methods
//! 2. `object {TraitName}Bridge` — registration/unregistration wrapper that:
//!    - Stores registered impls in a static map
//!    - Calls native JNI methods for registration/unregistration
//!    - Throws KreuzbergException on failure

use crate::backends::kotlin_android::naming::bridge_object_name;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::TypeDef;
use heck::ToUpperCamelCase;
use std::collections::BTreeSet;

/// Generate the complete trait bridge (interface + bridge object) for Kotlin Android.
///
/// For each bridge in the config:
/// - The interface is generated elsewhere by `emit_trait_interfaces` in gen_bindings.rs
/// - This function generates the bridge object with registration/unregistration methods
///
/// Returns (filename, content) ready for GeneratedFile emission.
pub fn gen_trait_bridge_object(
    package: &str,
    trait_name: &str,
    bridge_cfg: &TraitBridgeConfig,
    _trait_def: &TypeDef,
    bridge_class_name: &str,
) -> Option<(String, String)> {
    let interface_name = format!("I{trait_name}");
    // Use the canonical naming helper so both production and e2e emit the same name.
    let bridge_obj = bridge_object_name(trait_name);

    let _imports = BTreeSet::new();
    let mut body = String::new();

    // Bridge object declaration
    body.push_str(&format!("object {bridge_obj} {{\n"));
    body.push_str(&format!(
        "    private val registered = mutableMapOf<String, {interface_name}>()\n\n"
    ));

    // register() method
    let has_super_trait = bridge_cfg.super_trait.is_some();
    if has_super_trait {
        body.push_str(&format!("    fun register(impl: {interface_name}): Unit {{\n"));
        body.push_str("        val name = impl.name()\n");
    } else {
        body.push_str(&format!(
            "    fun register(impl: {interface_name}, name: String): Unit {{\n"
        ));
    }
    body.push_str("        registered[name] = impl\n");

    // Call native method
    let native_fn = format!("nativeRegister{}", trait_name.to_upper_camel_case());
    if has_super_trait {
        body.push_str(&format!("        {bridge_class_name}.{native_fn}(impl)\n"));
    } else {
        body.push_str(&format!("        {bridge_class_name}.{native_fn}(impl, name)\n"));
    }
    body.push_str("    }\n\n");

    // unregister() method if configured
    if bridge_cfg.unregister_fn.is_some() {
        body.push_str("    fun unregister(name: String): Unit {\n");
        body.push_str("        registered.remove(name)\n");
        let native_fn = format!("nativeUnregister{}", trait_name.to_upper_camel_case());
        body.push_str(&format!("        {bridge_class_name}.{native_fn}(name)\n"));
        body.push_str("    }\n\n");
    }

    // clearAll() method if configured
    if bridge_cfg.clear_fn.is_some() {
        body.push_str("    fun clearAll(): Unit {\n");
        body.push_str("        registered.clear()\n");
        let native_fn = format!("nativeClear{}s", trait_name.to_upper_camel_case());
        body.push_str(&format!("        {bridge_class_name}.{native_fn}()\n"));
        body.push_str("    }\n\n");
    }

    // getAll() helper for inspection
    body.push_str(&format!(
        "    fun getAll(): Map<String, {interface_name}> = registered.toMap()\n"
    ));

    body.push_str("}\n");

    let content = assemble_kt_content(package, &_imports, &body);
    Some((format!("{bridge_class_name}.kt"), content))
}

/// Assemble Kotlin file content with package and imports.
fn assemble_kt_content(package: &str, imports: &BTreeSet<String>, body: &str) -> String {
    let mut out = String::new();
    out.push_str("// Generated by alef. Do not edit by hand.\n\n");
    out.push_str(&format!("package {package}\n"));

    if !imports.is_empty() {
        out.push('\n');
        for import in imports {
            out.push_str(&format!("import {import}\n"));
        }
    }

    if !imports.is_empty() || !body.is_empty() {
        out.push('\n');
    }
    out.push_str(body);

    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use heck::ToSnakeCase;

    fn make_bridge_cfg(trait_name: &str, super_trait: Option<&str>) -> TraitBridgeConfig {
        TraitBridgeConfig {
            trait_name: trait_name.to_string(),
            param_name: None,
            type_alias: None,
            exclude_languages: vec![],
            super_trait: super_trait.map(|s| s.to_string()),
            registry_getter: None,
            register_fn: Some(format!("register_{}", trait_name.to_snake_case())),
            unregister_fn: Some(format!("unregister_{}", trait_name.to_snake_case())),
            clear_fn: Some(format!("clear_{}", trait_name.to_snake_case())),
            register_extra_args: None,
            bind_via: crate::core::config::BridgeBinding::FunctionParam,
            options_type: None,
            options_field: None,
            context_type: None,
            result_type: None,
            ffi_skip_methods: Vec::new(),
        }
    }

    fn make_trait_def(name: &str) -> TypeDef {
        TypeDef {
            name: name.to_string(),
            rust_path: format!("testcrate::{}", name),
            original_rust_path: String::new(),
            fields: vec![],
            methods: vec![],
            is_opaque: false,
            is_clone: true,
            is_copy: false,
            is_trait: true,
            has_default: false,
            has_stripped_cfg_fields: false,
            is_return_type: false,
            serde_rename_all: None,
            has_serde: false,
            super_traits: vec![],
            doc: String::new(),
            cfg: None,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }
    }

    #[test]
    fn test_bridge_object_with_super_trait() {
        let trait_def = make_trait_def("OcrBackend");
        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(content.contains("object OcrBackendBridge"));
        assert!(content.contains("fun register(impl: IOcrBackend): Unit"));
        assert!(content.contains("val name = impl.name()"));
        assert!(content.contains("TestBridge.nativeRegisterOcrBackend(impl)"));
        assert!(content.contains("nativeRegisterOcrBackend"));
    }

    #[test]
    fn test_bridge_object_without_super_trait() {
        let trait_def = make_trait_def("OcrBackend");
        let mut bridge_cfg = make_bridge_cfg("OcrBackend", None);
        bridge_cfg.super_trait = None;
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(content.contains("object OcrBackendBridge"));
        assert!(content.contains("fun register(impl: IOcrBackend, name: String): Unit"));
        assert!(!content.contains("val name = impl.name()"));
        assert!(content.contains("TestBridge.nativeRegisterOcrBackend(impl, name)"));
    }

    #[test]
    fn test_bridge_object_with_unregister() {
        let trait_def = make_trait_def("OcrBackend");
        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(content.contains("fun unregister(name: String): Unit"));
        assert!(content.contains("TestBridge.nativeUnregisterOcrBackend(name)"));
        assert!(content.contains("nativeUnregisterOcrBackend"));
    }

    #[test]
    fn test_bridge_object_with_clear() {
        let trait_def = make_trait_def("OcrBackend");
        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(content.contains("fun clearAll(): Unit"));
        assert!(content.contains("TestBridge.nativeClearOcrBackends()"));
        assert!(content.contains("nativeClearOcrBackends"));
    }

    #[test]
    fn test_bridge_object_without_unregister() {
        let trait_def = make_trait_def("OcrBackend");
        let mut bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
        bridge_cfg.unregister_fn = None;
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(!content.contains("fun unregister"));
    }

    #[test]
    fn test_bridge_object_without_clear() {
        let trait_def = make_trait_def("OcrBackend");
        let mut bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
        bridge_cfg.clear_fn = None;
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(!content.contains("fun clearAll"));
    }

    #[test]
    fn test_bridge_object_includes_getall() {
        let trait_def = make_trait_def("OcrBackend");
        let bridge_cfg = make_bridge_cfg("OcrBackend", Some("Plugin"));
        let (_filename, content) =
            gen_trait_bridge_object("dev.kreuzberg", "OcrBackend", &bridge_cfg, &trait_def, "TestBridge")
                .expect("should generate bridge");

        assert!(content.contains("fun getAll(): Map<String, IOcrBackend>"));
    }
}