use crate::core::keywords::swift_ident;
pub(crate) fn emit_streaming_adapter_shims(
adapters: &[crate::core::config::AdapterConfig],
source_crate: &str,
) -> String {
use crate::core::config::AdapterPattern;
use heck::{ToPascalCase, ToSnakeCase};
let mut out = String::new();
for adapter in adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| a.owner_type.is_some())
{
let owner_type = adapter.owner_type.as_deref().unwrap_or("");
let item_type = adapter
.item_type
.as_deref()
.expect("streaming adapter must declare item_type for Swift backend");
let owner_snake = owner_type.to_snake_case();
let adapter_pascal = adapter.name.to_pascal_case();
let owner_pascal = owner_type.to_pascal_case();
let handle_name = format!("{owner_pascal}{adapter_pascal}StreamHandle");
let fn_start = format!("{owner_snake}_{}_start", adapter.name);
let core_item = format!("{source_crate}::{item_type}");
let mut start_params_vec: Vec<String> = vec![format!("client: &{owner_type}")];
for p in &adapter.params {
let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
let param_name = swift_ident(&p.name.to_snake_case());
start_params_vec.push(format!("{param_name}: &{simple_ty}"));
}
let start_params_str = start_params_vec.join(", ");
let mut param_bindings = String::new();
let call_args: Vec<String> = adapter
.params
.iter()
.map(|p| {
let name = p.name.to_snake_case();
let bound = format!("__alef_{name}");
param_bindings.push_str(&format!(" let {bound} = {name}.0.clone();\n"));
bound
})
.collect();
let call_args_str = call_args.join(", ");
let core_call = if adapter.core_path.contains("::") {
format!("{}(&client.0, {call_args_str})", adapter.core_path)
} else {
format!("client.0.{}({call_args_str})", adapter.core_path)
};
out.push_str(&crate::backends::swift::template_env::render(
"rust_stream_handle_struct.rs.jinja",
minijinja::context! {
item_type => &item_type,
fn_start => &fn_start,
handle_name => &handle_name,
core_item => &core_item,
},
));
out.push_str(&crate::backends::swift::template_env::render(
"rust_stream_handle_start.rs.jinja",
minijinja::context! {
owner_type => owner_type,
adapter_name => &adapter.name,
handle_name => &handle_name,
fn_start => &fn_start,
start_params => &start_params_str,
core_call => &core_call,
core_item => &core_item,
param_bindings => ¶m_bindings,
},
));
out.push_str(&crate::backends::swift::template_env::render(
"rust_stream_handle_next.rs.jinja",
minijinja::context! {
handle_name => &handle_name,
},
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::{AdapterConfig, AdapterParam, AdapterPattern};
fn adapter() -> AdapterConfig {
AdapterConfig {
name: "chat_stream".to_string(),
pattern: AdapterPattern::Streaming,
core_path: "chat_stream".to_string(),
params: vec![AdapterParam {
name: "req".to_string(),
ty: "sample_crate::ChatRequest".to_string(),
optional: false,
}],
returns: None,
error_type: Some("String".to_string()),
owner_type: Some("Client".to_string()),
item_type: Some("Chunk".to_string()),
gil_release: false,
trait_name: None,
trait_method: None,
detect_async: false,
request_type: Some("sample_crate::ChatRequest".to_string()),
skip_languages: vec![],
}
}
#[test]
fn start_and_next_are_async_and_never_block_a_thread() {
let out = emit_streaming_adapter_shims(&[adapter()], "sample_crate");
assert!(
out.contains("pub async fn client_chat_stream_start(client: &Client, req: &ChatRequest)"),
"start must be an `async fn`, got:\n{out}"
);
assert!(
out.contains("pub async fn next(&self) -> Result<String, String>"),
"next must be an `async fn` over `&self`, got:\n{out}"
);
assert!(
!out.contains("block_on"),
"an async streaming shim must never block a thread on the runtime, got:\n{out}"
);
assert!(
out.contains("::futures_util::lock::Mutex<"),
"a guard held across `.await` needs an async mutex, got:\n{out}"
);
assert!(
!out.contains("std::sync::Mutex"),
"a std mutex guard must not be held across `.await`, got:\n{out}"
);
}
#[test]
fn start_clones_request_params_before_spawning() {
let out = emit_streaming_adapter_shims(&[adapter()], "sample_crate");
assert!(out.contains("let __alef_req = req.0.clone();"), "got:\n{out}");
assert!(
out.contains("client.0.chat_stream(__alef_req)"),
"the core call must take the pre-spawn clone, got:\n{out}"
);
let clone_at = out.find("let __alef_req = req.0.clone();").expect("checked above");
let spawn_at = out.find(".spawn(async move").expect("start must spawn");
assert!(clone_at < spawn_at, "the clone must precede the spawn, got:\n{out}");
}
}