alef 0.25.29

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Emit Swift class declarations for opaque handle types marked with
//! `#[swift_bridge(already_declared)]` in the Rust extern blocks.
//!
//! When swift-bridge sees `#[swift_bridge(already_declared)]` on a type declaration,
//! it skips generating the corresponding Swift class wrapper (to avoid duplicate
//! `__swift_bridge__{Type}__free` symbols). Alef must then provide the Swift class
//! hierarchy. Currently, this only applies to owner types of streaming adapters,
//! which are forward-declared in the streaming extern block.

use crate::core::backend::GeneratedFile;
use crate::core::config::{AdapterPattern, ResolvedCrateConfig};
use heck::ToPascalCase;
use std::path::Path;

/// Collect all unique streaming adapter owner types that require a Swift class
/// declaration because they are marked with `#[swift_bridge(already_declared)]`.
///
/// Exposed `pub(crate)` so the Rust-glue phantom-Vec emitter can SKIP these
/// types — `Vec<AlreadyDeclaredType>` cannot be constructed in Swift because
/// swift-bridge will not synthesize a `Vectorizable` conformance for the type
/// (the `already_declared` marker exists precisely to suppress that synthesis).
pub(crate) fn collect_already_declared_owner_types(config: &ResolvedCrateConfig) -> std::collections::BTreeSet<String> {
    let mut owner_types = std::collections::BTreeSet::new();
    for adapter in &config.adapters {
        if matches!(adapter.pattern, AdapterPattern::Streaming) {
            if let Some(owner) = adapter.owner_type.as_deref() {
                owner_types.insert(owner.to_string());
            }
        }
    }
    owner_types
}

/// Emit the Swift class triple for a single opaque handle type.
///
/// Pattern:
/// ```swift
/// public class TypeName: TypeNameRefMut {
///     public var isOwned: Bool = true
///     public override init(ptr: UnsafeMutableRawPointer) {
///         super.init(ptr: ptr)
///     }
///     deinit {
///         if isOwned {
///             __swift_bridge__$TypeName$_free(ptr)
///         }
///     }
/// }
/// public class TypeNameRefMut: TypeNameRef {
///     public override init(ptr: UnsafeMutableRawPointer) {
///         super.init(ptr: ptr)
///     }
/// }
/// public class TypeNameRef {
///     public var ptr: UnsafeMutableRawPointer
///     public init(ptr: UnsafeMutableRawPointer) { self.ptr = ptr }
/// }
/// ```
fn emit_opaque_class_triple(owner_type: &str, out: &mut String) {
    let type_pascal = owner_type.to_pascal_case();
    let ref_mut_name = format!("{type_pascal}RefMut");
    let ref_name = format!("{type_pascal}Ref");

    // Main class (owned)
    out.push_str(&format!("public class {type_pascal}: {ref_mut_name} {{\n"));
    out.push_str("    public var isOwned: Bool = true\n\n");
    out.push_str("    public override init(ptr: UnsafeMutableRawPointer) {\n");
    out.push_str("        super.init(ptr: ptr)\n");
    out.push_str("    }\n\n");
    out.push_str("    deinit {\n");
    out.push_str("        if isOwned {\n");
    out.push_str(&format!("            __swift_bridge__${type_pascal}$_free(ptr)\n"));
    out.push_str("        }\n");
    out.push_str("    }\n");
    out.push_str("}\n\n");

    // RefMut class (borrowed mutable)
    out.push_str(&format!("public class {ref_mut_name}: {ref_name} {{\n"));
    out.push_str("    public override init(ptr: UnsafeMutableRawPointer) {\n");
    out.push_str("        super.init(ptr: ptr)\n");
    out.push_str("    }\n");
    out.push_str("}\n\n");

    // Ref class (borrowed immutable)
    out.push_str(&format!("public class {ref_name} {{\n"));
    out.push_str("    public var ptr: UnsafeMutableRawPointer\n");
    out.push_str("    public init(ptr: UnsafeMutableRawPointer) { self.ptr = ptr }\n");
    out.push_str("}\n\n");
}

/// Emit a Swift file declaring all opaque handle class triples for types marked
/// with `#[swift_bridge(already_declared)]` in the Rust extern blocks.
///
/// Returns `None` if there are no such types.
pub(crate) fn emit_opaque_class_declarations(
    config: &ResolvedCrateConfig,
    rust_bridge_sources: &Path,
) -> Option<GeneratedFile> {
    let owner_types = collect_already_declared_owner_types(config);
    if owner_types.is_empty() {
        return None;
    }

    let mut content = String::new();
    content.push_str("// Generated by alef — opaque handle class triples for types marked\n");
    content.push_str("// #[swift_bridge(already_declared)] in the Rust extern blocks.\n");
    content.push_str("// These classes are referenced by swift-bridge-generated code but are not\n");
    content.push_str("// auto-generated when the type is marked already_declared.\n\n");
    content.push_str("import Foundation\n");
    // RustBridgeC exports the swift-bridge C symbols (`__swift_bridge__$Type$_free`,
    // etc.) emitted by the Rust glue. The `deinit` here calls `_free`, which must
    // be in scope for Swift to find the C symbol.
    content.push_str("import RustBridgeC\n\n");

    for owner_type in &owner_types {
        emit_opaque_class_triple(owner_type, &mut content);
    }

    let path = rust_bridge_sources.join("RustBridgeOpaqueHandles.swift");
    Some(GeneratedFile {
        path,
        content,
        generated_header: false,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::config::{AdapterConfig, AdapterParam, AdapterPattern};

    fn streaming_adapter_with_owner(name: &str, owner: &str) -> AdapterConfig {
        AdapterConfig {
            name: name.to_string(),
            pattern: AdapterPattern::Streaming,
            core_path: format!("sample::{name}"),
            params: vec![AdapterParam {
                name: "req".to_string(),
                ty: "sample::StreamRequest".to_string(),
                optional: false,
            }],
            returns: None,
            error_type: Some("String".to_string()),
            owner_type: Some(owner.to_string()),
            item_type: Some("StreamItem".to_string()),
            gil_release: false,
            trait_name: None,
            trait_method: None,
            detect_async: false,
            request_type: Some("sample::StreamRequest".to_string()),
            skip_languages: vec![],
        }
    }

    #[test]
    fn emit_opaque_class_triple_generates_three_classes() {
        let mut out = String::new();
        emit_opaque_class_triple("DefaultClient", &mut out);

        // Main class
        assert!(out.contains("public class DefaultClient: DefaultClientRefMut"));
        assert!(out.contains("public var isOwned: Bool = true"));
        assert!(out.contains("__swift_bridge__$DefaultClient$_free(ptr)"));

        // RefMut class
        assert!(out.contains("public class DefaultClientRefMut: DefaultClientRef"));

        // Ref class
        assert!(out.contains("public class DefaultClientRef"));
        assert!(out.contains("public var ptr: UnsafeMutableRawPointer"));
    }

    #[test]
    fn emit_opaque_class_triple_uses_correct_pascal_case() {
        let mut out = String::new();
        emit_opaque_class_triple("my_client", &mut out);

        assert!(out.contains("public class MyClient: MyClientRefMut"));
        assert!(out.contains("public class MyClientRefMut: MyClientRef"));
        assert!(out.contains("public class MyClientRef"));
        assert!(out.contains("__swift_bridge__$MyClient$_free(ptr)"));
    }

    #[test]
    fn collect_already_declared_owner_types_deduplicates() {
        let config = ResolvedCrateConfig {
            adapters: vec![
                streaming_adapter_with_owner("chat_stream", "DefaultClient"),
                streaming_adapter_with_owner("batch_stream", "DefaultClient"),
                streaming_adapter_with_owner("crawl_stream", "CrawlEngine"),
            ],
            ..Default::default()
        };

        let owners = collect_already_declared_owner_types(&config);
        assert_eq!(owners.len(), 2);
        assert!(owners.contains("DefaultClient"));
        assert!(owners.contains("CrawlEngine"));
    }

    #[test]
    fn collect_already_declared_owner_types_skips_non_streaming() {
        // Only create a streaming adapter; it will be the only one with owner_type
        let config = ResolvedCrateConfig {
            adapters: vec![streaming_adapter_with_owner("stream", "StreamClient")],
            ..Default::default()
        };

        let owners = collect_already_declared_owner_types(&config);
        assert_eq!(owners.len(), 1);
        assert!(owners.contains("StreamClient"));
    }
}