use crate::core::backend::GeneratedFile;
use crate::core::config::{AdapterPattern, ResolvedCrateConfig};
use heck::ToPascalCase;
use std::path::Path;
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
}
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");
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");
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");
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");
}
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\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);
assert!(out.contains("public class DefaultClient: DefaultClientRefMut"));
assert!(out.contains("public var isOwned: Bool = true"));
assert!(out.contains("__swift_bridge__$DefaultClient$_free(ptr)"));
assert!(out.contains("public class DefaultClientRefMut: DefaultClientRef"));
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() {
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"));
}
}