use crate::backends::swift::naming::{swift_rust_shim_ident as swift_ident, swift_source_ident as swift_case_ident};
use crate::codegen::shared::binding_fields;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
use crate::core::config::{
AdapterConfig, AdapterPattern, BridgeBinding, Language, ResolvedCrateConfig, TraitBridgeConfig, resolve_output_dir,
};
use crate::core::ir::{
ApiSurface, DefaultValue, EnumDef, EnumVariant, ErrorDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef,
};
use heck::{AsSnakeCase, ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::collections::BTreeSet;
use std::path::PathBuf;
use crate::backends::swift::gen_rust_crate;
use crate::backends::swift::type_map::SwiftMapper;
pub mod plugin_marshal;
pub mod service_api;
pub mod trait_bridge;
pub struct SwiftBackend;
fn effective_exclude_types(config: &ResolvedCrateConfig, api: &ApiSurface) -> std::collections::HashSet<String> {
let mut exclude_types: std::collections::HashSet<String> = config
.ffi
.as_ref()
.map(|c| c.exclude_types.iter().cloned().collect())
.unwrap_or_default();
if let Some(swift) = &config.swift {
exclude_types.extend(swift.exclude_types.iter().cloned());
}
exclude_types.extend(api.types.iter().filter(|t| t.binding_excluded).map(|t| t.name.clone()));
exclude_types.extend(api.enums.iter().filter(|e| e.binding_excluded).map(|e| e.name.clone()));
exclude_types.extend(api.excluded_type_paths.keys().cloned());
exclude_types.extend(config.opaque_types.keys().cloned());
exclude_types
}
impl Backend for SwiftBackend {
fn name(&self) -> &str {
"swift"
}
fn language(&self) -> Language {
Language::Swift
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
supports_callbacks: false,
supports_streaming: true,
supports_service_api: true,
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let module_name = config.swift_module();
let mapper = SwiftMapper;
let exclude_types = effective_exclude_types(config, api);
let exclude_fields: std::collections::HashSet<String> = config
.swift
.as_ref()
.map(|c| c.exclude_fields.iter().cloned().collect())
.unwrap_or_default();
let mut imports: BTreeSet<String> = BTreeSet::new();
imports.insert("import Foundation".to_string());
if !api.types.is_empty() || !api.enums.is_empty() || !api.errors.is_empty() {
imports.insert("import RustBridge".to_string());
}
let mut body = String::new();
let unit_serde_enum_names: std::collections::HashSet<String> = api
.enums
.iter()
.filter(|e| !exclude_types.contains(&e.name))
.filter(|e| e.has_serde && e.variants.iter().all(|v| v.fields.is_empty()))
.map(|e| e.name.clone())
.collect();
let candidate_types: Vec<&crate::core::ir::TypeDef> = api
.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && t.has_serde && !exclude_types.contains(&t.name))
.filter(|t| !t.fields.is_empty())
.collect();
let untagged_enum_names: std::collections::HashSet<String> = api
.enums
.iter()
.filter(|e| !exclude_types.contains(&e.name))
.filter(|e| e.has_serde && e.variants.iter().any(|v| !v.fields.is_empty()))
.map(|e| e.name.clone())
.collect();
let mut known_dto_names: std::collections::HashSet<String> = unit_serde_enum_names.clone();
known_dto_names.extend(untagged_enum_names.iter().cloned());
loop {
let prev_len = known_dto_names.len();
for ty in &candidate_types {
if known_dto_names.contains(&ty.name) {
continue;
}
let all_supported =
binding_fields(&ty.fields).all(|field| first_class_field_supported(&field.ty, &known_dto_names));
if all_supported {
known_dto_names.insert(ty.name.clone());
}
}
if known_dto_names.len() == prev_len {
break; }
}
for ty in api
.types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
.filter(|t| t.methods.is_empty() || !t.is_opaque && t.has_serde)
{
emit_doc_comment(&ty.doc, "", &mut body);
if can_emit_first_class_struct(ty, &mapper, &exclude_fields, &known_dto_names) {
emit_first_class_struct(
ty,
&mapper,
&exclude_fields,
&known_dto_names,
&unit_serde_enum_names,
&untagged_enum_names,
&mut body,
);
} else {
body.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &ty.name,
},
));
}
body.push('\n');
}
let result_type_enums: std::collections::HashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.result_type.as_deref().map(|s| s.to_string()))
.collect();
for en in api.enums.iter().filter(|e| !exclude_types.contains(&e.name)) {
if result_type_enums.contains(&en.name) {
emit_enum_without_into_rust(en, &mut body, &mapper, &known_dto_names);
} else {
emit_enum(en, &mut body, &mapper, &known_dto_names);
}
body.push('\n');
}
for error in &api.errors {
emit_error(error, &module_name, &mut body, &mapper);
body.push('\n');
}
let client_constructor_types: std::collections::HashSet<&str> = config
.swift
.as_ref()
.map(|c| c.client_constructor_body.keys().map(String::as_str).collect())
.unwrap_or_default();
let first_class_types: std::collections::HashSet<String> = api
.types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
.filter(|t| can_emit_first_class_struct(t, &mapper, &exclude_fields, &known_dto_names))
.map(|t| t.name.clone())
.collect();
let mut sendable_emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
for ty in api.types.iter().filter(|t| {
!t.is_trait
&& !exclude_types.contains(&t.name)
&& !t.methods.is_empty()
&& (t.is_opaque || !t.has_serde)
&& client_constructor_types.contains(t.name.as_str())
}) {
emit_client_class(
ty.name.as_str(),
&ty.methods,
&mapper,
config,
&first_class_types,
&mut body,
);
body.push('\n');
let streaming_adapters: Vec<&AdapterConfig> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "swift"))
.filter(|a| a.owner_type.as_deref() == Some(ty.name.as_str()))
.collect();
if !streaming_adapters.is_empty() {
let inner_ty = ty.name.as_str();
if sendable_emitted.insert(inner_ty.to_string()) {
body.push_str(&format!(
"// MARK: - Sendable conformance for {inner_ty} (streaming client inner)\n"
));
body.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
body.push_str("// Captured by Task.detached in streaming methods — Rust type is Send + Sync.\n");
body.push_str(&format!("extension RustBridge.{inner_ty}: @unchecked Sendable {{}}\n"));
}
}
for adapter in &streaming_adapters {
emit_stream_handle_sendable(adapter, ty.name.as_str(), &mut body);
}
{
for adapter in &streaming_adapters {
for param in &adapter.params {
let simple_ty = param.ty.rsplit("::").next().unwrap_or(¶m.ty).to_string();
if sendable_emitted.insert(simple_ty.clone()) {
body.push_str(&format!(
"// MARK: - Sendable conformance for {simple_ty} (streaming request param)\n"
));
body.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
body.push_str("// Passed into Task.detached for streaming — Rust type is Send + Sync.\n");
body.push_str(&format!("extension RustBridge.{simple_ty}: @unchecked Sendable {{}}\n"));
}
}
}
}
}
emit_convenience_wrappers(api, &mut body);
emit_json_string_overloads(api, &mut body);
emit_from_json_forwarders(
api,
&exclude_types,
&mapper,
&exclude_fields,
&known_dto_names,
&mut body,
);
emit_inbound_protocols(api, config, &exclude_types, &mut body);
let client_class_names: std::collections::HashSet<String> =
client_constructor_types.iter().map(|&s| s.to_string()).collect();
emit_free_function_forwarders(api, config, &known_dto_names, &client_class_names, &mut body);
emit_trait_bridge_forwarders(config, &mut body);
emit_streaming_free_functions(config, &first_class_types, &mut body);
{
for ty in api
.types
.iter()
.filter(|t| !t.is_trait && !exclude_types.contains(&t.name))
{
if sendable_emitted.insert(ty.name.clone()) {
body.push_str("// swift-bridge opaque type used across Task.detached boundaries — Rust type is Send + Sync.\n");
body.push_str(&format!("extension RustBridge.{}: @unchecked Sendable {{}}\n", ty.name));
}
}
}
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n");
content.push_str("// swift-format-ignore-file\n\n");
for import in &imports {
content.push_str(import);
content.push('\n');
}
content.push('\n');
content.push_str(&body);
let base_dir = resolve_output_dir(config.output_paths.get("swift"), &config.name, "packages/swift");
let base_path = PathBuf::from(&base_dir);
let path = if config.explicit_output.swift.is_some() {
base_path.join(format!("{module_name}.swift"))
} else {
base_path
.join("Sources")
.join(&module_name)
.join(format!("{module_name}.swift"))
};
let mut files = vec![GeneratedFile {
path,
content,
generated_header: false,
}];
let rust_crate_files = gen_rust_crate::emit(api, config)?;
files.extend(rust_crate_files);
let binding_crate_name = format!("{}-swift", &api.crate_name);
let base_dir = resolve_output_dir(config.output_paths.get("swift"), &config.name, "packages/swift");
let package_root = PathBuf::from(&base_dir)
.ancestors()
.find(|p| p.join("Sources").is_dir())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| {
PathBuf::from(&base_dir)
.parent()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from("packages/swift"))
});
if let Some(bridge_files) = emit_swift_bridge_files(&api.crate_name, &binding_crate_name, &package_root)? {
files.extend(bridge_files);
}
let rust_bridge_sources = package_root.join("Sources").join("RustBridge");
for box_file in emit_inbound_box_files(api, config, &rust_bridge_sources) {
files.push(box_file);
}
for box_file in emit_function_param_box_files(api, config, &rust_bridge_sources, &exclude_types) {
files.push(box_file);
}
let trait_bridge_configs: Vec<(String, &TraitBridgeConfig, &TypeDef)> = config
.trait_bridges
.iter()
.filter_map(|b| {
api.types
.iter()
.find(|t| t.is_trait && t.name == b.trait_name)
.map(|t| (b.trait_name.clone(), b, t))
})
.collect();
let module_dir = if config.explicit_output.swift.is_some() {
base_path.clone()
} else {
base_path.join("Sources").join(&module_name)
};
for (filename, content) in trait_bridge::gen_trait_bridge_files(&trait_bridge_configs, &exclude_types) {
let path = rust_bridge_sources.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: false,
});
}
if let Some((filename, content)) = emit_ref_property_extensions(api) {
let path = module_dir.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: true,
});
}
if let Some((filename, content)) = trait_bridge::gen_bridge_registration_overloads_file(&trait_bridge_configs) {
let path = module_dir.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: false,
});
}
Ok(files)
}
fn generate_service_api(
&self,
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
service_api::generate(api, config)
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "swift",
crate_suffix: "-swift",
build_dep: BuildDependency::None,
post_build: vec![PostBuildStep::RunCommand {
cmd: "cargo",
args: vec!["build", "--release"],
}],
})
}
}
fn can_emit_first_class_struct(
ty: &crate::core::ir::TypeDef,
_mapper: &SwiftMapper,
_exclude_fields: &std::collections::HashSet<String>,
known_dto_names: &std::collections::HashSet<String>,
) -> bool {
!ty.is_opaque
&& ty.has_serde
&& !ty.fields.is_empty()
&& binding_fields(&ty.fields).all(|field| first_class_field_supported(&field.ty, known_dto_names))
&& !ty.fields.iter().all(|f| f.binding_excluded)
}
fn first_class_field_supported(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
match ty {
TypeRef::Primitive(_) | TypeRef::String => true,
TypeRef::Named(name) => known_dto_names.contains(name),
TypeRef::Vec(inner) => first_class_field_supported(inner, known_dto_names),
TypeRef::Optional(inner) => first_class_field_supported(inner, known_dto_names),
_ => false,
}
}
fn emit_first_class_struct(
ty: &crate::core::ir::TypeDef,
mapper: &SwiftMapper,
exclude_fields: &std::collections::HashSet<String>,
known_dto_names: &std::collections::HashSet<String>,
unit_enum_names: &std::collections::HashSet<String>,
untagged_enum_names: &std::collections::HashSet<String>,
out: &mut String,
) {
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::TypeRef;
use heck::ToLowerCamelCase;
let type_name = &ty.name;
let type_snake = AsSnakeCase(type_name.as_str()).to_string();
out.push_str(&format!("public struct {type_name}: Codable, Sendable, Hashable {{\n"));
let visible_fields: Vec<_> = binding_fields(&ty.fields).collect();
for field in &visible_fields {
emit_doc_comment(&field.doc, " ", out);
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let swift_ty = mapper.map_type(&field.ty);
if field.optional && !already_optional {
out.push_str(&format!(" public let {camel}: {swift_ty}?\n"));
} else {
out.push_str(&format!(" public let {camel}: {swift_ty}\n"));
}
}
let params: Vec<String> = visible_fields
.iter()
.map(|field| {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let swift_ty = mapper.map_type(&field.ty);
if field.optional && !already_optional {
format!("{camel}: {swift_ty}? = nil")
} else if already_optional {
format!("{camel}: {swift_ty} = nil")
} else {
format!("{camel}: {swift_ty}")
}
})
.collect();
out.push_str(&format!(" public init({}) {{\n", params.join(", ")));
for field in &visible_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
out.push_str(&format!(" self.{camel} = {camel}\n"));
}
out.push_str(" }\n");
let needs_coding_keys = ty.has_default
|| visible_fields.iter().any(|field| {
let camel = field.name.to_lower_camel_case();
camel != field.name
});
if needs_coding_keys {
out.push_str(" private enum CodingKeys: String, CodingKey {\n");
for field in &visible_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let wire_key = &field.name;
out.push_str(&format!(" case {camel} = \"{wire_key}\"\n"));
}
out.push_str(" }\n");
}
if ty.has_default {
emit_decoder_init(mapper, &visible_fields, out);
}
out.push_str("}\n");
out.push_str(&format!("\n// MARK: - Internal FFI conversions for {type_name}\n"));
out.push_str(&format!("internal extension {type_name} {{\n"));
out.push_str(&format!(" init(_ rb: RustBridge.{type_name}Ref) throws {{\n"));
for field in &visible_fields {
let swift_field = swift_case_ident(&field.name.to_lower_camel_case());
let rust_accessor = swift_ident(&field.name.to_lower_camel_case());
let is_optional = field.optional || matches!(&field.ty, TypeRef::Optional(_));
let expr = if is_field_unbridgeable_for_init(ty, field, exclude_fields, known_dto_names) {
if is_optional {
"nil".to_string()
} else {
let swift_ty = mapper.map_type(&field.ty);
format!("try JSONDecoder().decode({swift_ty}.self, from: Data(\"null\".utf8))")
}
} else if is_untagged_enum_type(&field.ty, untagged_enum_names) {
let swift_ty = mapper.map_type(&field.ty);
let swift_ty_with_opt = if is_optional && !matches!(&field.ty, TypeRef::Optional(_)) {
format!("{swift_ty}?")
} else {
swift_ty
};
let accessor_with_chain = if is_optional {
format!("rb.{rust_accessor}()?.toString() ?? \"null\"")
} else {
format!("rb.{rust_accessor}().toString()")
};
format!(
"try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
(({accessor_with_chain}).data(using: .utf8) ?? Data(\"null\".utf8)))"
)
} else if needs_json_bridge_for_swift(&field.ty) {
let swift_ty = mapper.map_type(&field.ty);
let swift_ty_with_opt = if is_optional && !matches!(&field.ty, TypeRef::Optional(_)) {
format!("{swift_ty}?")
} else {
swift_ty
};
let accessor_with_chain = format!("rb.{rust_accessor}().toString()");
format!(
"try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
(({accessor_with_chain}).data(using: .utf8) ?? Data(\"null\".utf8)))"
)
} else {
swift_ffi_read_expr(
&field.ty,
is_optional,
&rust_accessor,
known_dto_names,
unit_enum_names,
untagged_enum_names,
)
};
out.push_str(&format!(" self.{swift_field} = {expr}\n"));
}
out.push_str(" }\n");
out.push_str(&format!(" func intoRust() throws -> RustBridge.{type_name} {{\n"));
let direct_call = emit_into_rust_direct_call(ty, mapper, exclude_fields, type_name);
match direct_call {
Some(call) => out.push_str(&call),
None => {
let from_json_fn = format!("{type_snake}_from_json").to_lower_camel_case();
out.push_str(" let data = try JSONEncoder().encode(self)\n");
out.push_str(" let json = String(data: data, encoding: .utf8) ?? \"{}\"\n");
out.push_str(&format!(" return try RustBridge.{from_json_fn}(json)\n"));
}
}
out.push_str(" }\n");
out.push_str("}\n");
}
fn swift_typed_default_literal(dv: &DefaultValue) -> Option<String> {
match dv {
DefaultValue::BoolLiteral(true) => Some("true".to_string()),
DefaultValue::BoolLiteral(false) => Some("false".to_string()),
DefaultValue::IntLiteral(n) => Some(n.to_string()),
DefaultValue::FloatLiteral(f) => {
if f.is_nan() || f.is_infinite() {
None
} else {
let s = if f.fract() == 0.0 {
format!("{f:.1}")
} else {
f.to_string()
};
Some(s)
}
}
DefaultValue::StringLiteral(s) => {
let mut escaped = String::with_capacity(s.len() + 2);
escaped.push('"');
for ch in s.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c => escaped.push(c),
}
}
escaped.push('"');
Some(escaped)
}
DefaultValue::EnumVariant(_) => None,
DefaultValue::Empty | DefaultValue::None => None,
}
}
fn swift_type_based_default(ty: &TypeRef) -> Option<String> {
match ty {
TypeRef::Primitive(prim) => match prim {
PrimitiveType::Bool => Some("false".to_string()),
PrimitiveType::U8
| PrimitiveType::I8
| PrimitiveType::U16
| PrimitiveType::I16
| PrimitiveType::U32
| PrimitiveType::I32
| PrimitiveType::U64
| PrimitiveType::I64
| PrimitiveType::Usize
| PrimitiveType::Isize => Some("0".to_string()),
PrimitiveType::F32 | PrimitiveType::F64 => Some("0".to_string()),
},
TypeRef::String => Some("\"\"".to_string()),
TypeRef::Vec(_) => Some("[]".to_string()),
TypeRef::Map(_, _) => Some("[:]".to_string()),
TypeRef::Optional(_) => Some("nil".to_string()),
_ => None,
}
}
fn emit_decoder_init(mapper: &SwiftMapper, visible_fields: &[&crate::core::ir::FieldDef], out: &mut String) {
out.push_str(" public init(from decoder: any Decoder) throws {\n");
out.push_str(" let container = try decoder.container(keyedBy: CodingKeys.self)\n");
for field in visible_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let is_optional = field.optional || already_optional;
let swift_ty = mapper.map_type(&field.ty);
if is_optional {
let inner_ty = swift_ty.strip_suffix('?').unwrap_or(&swift_ty);
out.push_str(&format!(
" self.{camel} = try container.decodeIfPresent({inner_ty}.self, forKey: .{camel}) ?? nil\n"
));
continue;
}
let fallback = field
.typed_default
.as_ref()
.and_then(swift_typed_default_literal)
.or_else(|| swift_type_based_default(&field.ty));
match fallback {
Some(fb) => {
out.push_str(&format!(
" self.{camel} = try container.decodeIfPresent({swift_ty}.self, forKey: .{camel}) ?? {fb}\n"
));
}
None => {
out.push_str(&format!(
" self.{camel} = try container.decode({swift_ty}.self, forKey: .{camel})\n"
));
}
}
}
out.push_str(" }\n");
}
fn emit_into_rust_direct_call(
ty: &crate::core::ir::TypeDef,
_mapper: &SwiftMapper,
exclude_fields: &std::collections::HashSet<String>,
type_name: &str,
) -> Option<String> {
use crate::backends::swift::gen_rust_crate::extern_block::{constructor_fields, has_constructor_extern};
use heck::ToLowerCamelCase;
if !has_constructor_extern(ty, exclude_fields) {
return None;
}
let ctor_fields = constructor_fields(ty, exclude_fields);
let visible_count = ty.fields.iter().filter(|f| !f.binding_excluded).count();
if ctor_fields.len() != visible_count {
return None;
}
let mut prelude = String::new();
let mut args: Vec<String> = Vec::with_capacity(ctor_fields.len());
for field in &ctor_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let local = format!("__{}", field.name.to_lower_camel_case());
match field_intorust_arg(&field.ty, field.optional, &camel, &local) {
Some(FieldArg::Direct(expr)) => args.push(expr),
Some(FieldArg::WithPrelude { prelude: p, arg }) => {
prelude.push_str(&p);
args.push(arg);
}
None => return None,
}
}
let mut body = String::new();
body.push_str(&prelude);
body.push_str(&format!(
" return RustBridge.{type_name}({args})\n",
args = args.join(", ")
));
Some(body)
}
enum FieldArg {
Direct(String),
WithPrelude { prelude: String, arg: String },
}
fn field_intorust_arg(
ty: &crate::core::ir::TypeRef,
field_optional: bool,
self_property: &str,
local: &str,
) -> Option<FieldArg> {
use crate::core::ir::TypeRef;
let (inner_ty, is_optional) = match ty {
TypeRef::Optional(inner) => (inner.as_ref(), true),
_ => (ty, field_optional),
};
match inner_ty {
TypeRef::Primitive(_) => Some(FieldArg::Direct(format!("self.{self_property}"))),
TypeRef::String if !is_optional => Some(FieldArg::Direct(format!("RustString(self.{self_property})"))),
TypeRef::String => Some(FieldArg::Direct(format!("self.{self_property}.map(RustString.init)"))),
TypeRef::Named(_) if !is_optional => Some(FieldArg::Direct(format!("try self.{self_property}.intoRust()"))),
TypeRef::Vec(elem) if !is_optional => emit_vec_arg(elem, self_property, local),
_ => None,
}
}
fn emit_vec_arg(elem: &crate::core::ir::TypeRef, self_property: &str, local: &str) -> Option<FieldArg> {
use crate::core::ir::TypeRef;
let (rust_vec_param, elem_expr): (String, String) = match elem {
TypeRef::Primitive(prim) => {
let swift_prim = SwiftMapper.primitive(prim).into_owned();
(swift_prim, "__elem".to_string())
}
TypeRef::String => ("RustString".to_string(), "RustString(__elem)".to_string()),
TypeRef::Named(name) => (format!("RustBridge.{name}"), "try __elem.intoRust()".to_string()),
_ => return None,
};
let prelude = format!(
" let {local} = RustVec<{rust_vec_param}>()\n \
for __elem in self.{self_property} {{ {local}.push(value: {elem_expr}) }}\n",
);
Some(FieldArg::WithPrelude {
prelude,
arg: local.to_string(),
})
}
fn swift_ffi_read_expr(
ty: &crate::core::ir::TypeRef,
field_optional: bool,
accessor: &str,
known_dto_names: &std::collections::HashSet<String>,
unit_enum_names: &std::collections::HashSet<String>,
untagged_enum_names: &std::collections::HashSet<String>,
) -> String {
use crate::core::ir::TypeRef;
let opt = field_optional && !matches!(ty, TypeRef::Optional(_));
match ty {
TypeRef::String if opt => format!("rb.{accessor}()?.toString()"),
TypeRef::String => format!("rb.{accessor}().toString()"),
TypeRef::Named(name) if unit_enum_names.contains(name) && opt => {
format!("rb.{accessor}().flatMap {{ {name}(rawValue: $0.toString()) }}")
}
TypeRef::Named(name) if unit_enum_names.contains(name) => {
format!(
"{name}(rawValue: rb.{accessor}().toString()) ?? {{ fatalError(\"Unknown {name}: \\(rb.{accessor}().toString())\") }}()"
)
}
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) && opt => {
format!("try rb.{accessor}().map {{ try {name}($0) }}")
}
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
format!("try {name}(rb.{accessor}())")
}
TypeRef::Vec(inner) if opt => {
match inner.as_ref() {
TypeRef::Primitive(_) => format!("rb.{accessor}().map {{ Array($0) }}"),
TypeRef::String => format!("rb.{accessor}()?.map {{ $0.as_str().toString() }}"),
TypeRef::Named(name) if untagged_enum_names.contains(name) => {
format!(
"try rb.{accessor}()?.map {{ (s: RustStringRef) -> {name} in \
let d = s.as_str().toString().data(using: .utf8) ?? Data(); \
return try JSONDecoder().decode({name}.self, from: d) }}"
)
}
TypeRef::Named(name) if known_dto_names.contains(name) => {
format!("try rb.{accessor}()?.map {{ try {name}($0) }}")
}
_ => {
let map_expr = vec_elem_convert_expr(inner, known_dto_names);
format!("rb.{accessor}()?.map {{ {map_expr} }}")
}
}
}
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Primitive(_) => format!("Array(rb.{accessor}())"),
TypeRef::String => format!("rb.{accessor}().map {{ $0.as_str().toString() }}"),
TypeRef::Named(name) if untagged_enum_names.contains(name) => {
format!(
"try rb.{accessor}().map {{ (s: RustStringRef) -> {name} in \
let d = s.as_str().toString().data(using: .utf8) ?? Data(); \
return try JSONDecoder().decode({name}.self, from: d) }}"
)
}
TypeRef::Named(name) if known_dto_names.contains(name) => {
format!("try rb.{accessor}().map {{ try {name}($0) }}")
}
_ => format!("rb.{accessor}()"),
},
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::String => format!("rb.{accessor}()?.toString()"),
TypeRef::Named(name) if unit_enum_names.contains(name) => {
format!("rb.{accessor}().flatMap {{ {name}(rawValue: $0.toString()) }}")
}
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
format!("try rb.{accessor}().map {{ try {name}($0) }}")
}
TypeRef::Vec(elem) => match elem.as_ref() {
TypeRef::String => format!("rb.{accessor}()?.map {{ $0.as_str().toString() }}"),
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
format!("try rb.{accessor}()?.map {{ try {name}($0) }}")
}
TypeRef::Primitive(_) => format!("rb.{accessor}().map {{ Array($0) }}"),
_ => format!("rb.{accessor}()"),
},
_ => format!("rb.{accessor}()"),
},
_ => format!("rb.{accessor}()"),
}
}
fn vec_elem_convert_expr(
inner: &crate::core::ir::TypeRef,
known_dto_names: &std::collections::HashSet<String>,
) -> String {
use crate::core::ir::TypeRef;
match inner {
TypeRef::String => "$0.as_str().toString()".to_string(),
TypeRef::Named(name) if known_dto_names.contains(name) => format!("try {name}($0)"),
TypeRef::Primitive(_) => "$0".to_string(),
_ => "$0".to_string(),
}
}
fn emit_serde_tagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
let tag_key = en.serde_tag.as_deref().unwrap_or("type");
out.push_str(" private enum CodingKeys: String, CodingKey {\n");
out.push_str(&format!(" case {}\n", tag_key));
let mut field_keys = std::collections::BTreeSet::new();
for variant in &en.variants {
for (idx, field) in variant.fields.iter().enumerate() {
let swift_name = swift_associated_label(&field.name, idx);
let rust_name = field.serde_rename.as_deref().unwrap_or(&field.name);
field_keys.insert((swift_name, rust_name.to_string()));
}
}
for (swift_name, rust_name) in field_keys {
if swift_name == rust_name {
out.push_str(&format!(" case {}\n", swift_name));
} else {
out.push_str(&format!(" case {} = \"{}\"\n", swift_name, rust_name));
}
}
out.push_str(" }\n\n");
out.push_str(" public init(from decoder: Decoder) throws {\n");
out.push_str(" let container = try decoder.container(keyedBy: CodingKeys.self)\n");
out.push_str(&format!(
" let type = try container.decode(String.self, forKey: .{})\n",
tag_key
));
out.push_str(" switch type {\n");
for variant in &en.variants {
let variant_tag = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
en.serde_rename_all.as_deref(),
);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
out.push_str(&format!(" case \"{}\":\n", variant_tag));
if variant.fields.is_empty() {
out.push_str(&format!(" self = .{}\n", case_name));
} else {
out.push_str(&format!(" self = .{}(", case_name));
for (i, field) in variant.fields.iter().enumerate() {
let label = swift_associated_label(&field.name, i);
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let is_optional = field.optional || already_optional;
let ty = mapper.map_type(&field.ty);
if is_optional {
out.push_str(&format!(
"{}: try container.decodeIfPresent({}.self, forKey: .{})",
label, ty, label
));
} else {
out.push_str(&format!(
"{}: try container.decode({}.self, forKey: .{})",
label, ty, label
));
}
if i < variant.fields.len() - 1 {
out.push_str(", ");
}
}
out.push_str(")\n");
}
}
out.push_str(" default:\n");
out.push_str(" throw DecodingError.dataCorruptedError(\n");
out.push_str(&format!(" forKey: .{},\n", tag_key));
out.push_str(" in: container,\n");
out.push_str(&format!(
" debugDescription: \"Unknown {} type: \\(type)\"\n",
en.name
));
out.push_str(" )\n");
out.push_str(" }\n");
out.push_str(" }\n\n");
out.push_str(" public func encode(to encoder: Encoder) throws {\n");
out.push_str(" var container = encoder.container(keyedBy: CodingKeys.self)\n");
out.push_str(" switch self {\n");
for variant in &en.variants {
let variant_tag = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
en.serde_rename_all.as_deref(),
);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
if variant.fields.is_empty() {
out.push_str(&format!(" case .{}:\n", case_name));
out.push_str(&format!(
" try container.encode(\"{}\", forKey: .{})\n",
variant_tag, tag_key
));
} else {
let mut bindings = Vec::new();
for (i, field) in variant.fields.iter().enumerate() {
let label = swift_associated_label(&field.name, i);
bindings.push(format!("let {}", label));
}
out.push_str(&format!(" case .{}({}):\n", case_name, bindings.join(", ")));
out.push_str(&format!(
" try container.encode(\"{}\", forKey: .{})\n",
variant_tag, tag_key
));
for (i, field) in variant.fields.iter().enumerate() {
let label = swift_associated_label(&field.name, i);
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let is_optional = field.optional || already_optional;
if is_optional {
out.push_str(&format!(
" try container.encodeIfPresent({}, forKey: .{})\n",
label, label
));
} else {
out.push_str(&format!(
" try container.encode({}, forKey: .{})\n",
label, label
));
}
}
}
}
out.push_str(" }\n");
out.push_str(" }\n");
}
fn emit_serde_untagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
out.push_str(" public init(from decoder: Decoder) throws {\n");
out.push_str(" let container = try decoder.singleValueContainer()\n");
for variant in &en.variants {
if variant.fields.len() != 1 {
continue;
}
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
let payload_ty = mapper.map_type(&variant.fields[0].ty);
let label = swift_associated_label(&variant.fields[0].name, 0);
out.push_str(&format!(
" if let value = try? container.decode({payload_ty}.self) {{\n self = .{case_name}({label}: value)\n return\n }}\n"
));
}
out.push_str(
" throw DecodingError.dataCorruptedError(in: container, debugDescription: \"no matching untagged variant\")\n",
);
out.push_str(" }\n\n");
out.push_str(" public func encode(to encoder: Encoder) throws {\n");
out.push_str(" var container = encoder.singleValueContainer()\n");
out.push_str(" switch self {\n");
for variant in &en.variants {
if variant.fields.len() != 1 {
continue;
}
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
let label = swift_associated_label(&variant.fields[0].name, 0);
out.push_str(&format!(
" case .{case_name}(let {label}):\n try container.encode({label})\n"
));
}
out.push_str(" }\n");
out.push_str(" }\n");
}
fn emit_enum(
en: &EnumDef,
out: &mut String,
mapper: &SwiftMapper,
known_dto_names: &std::collections::HashSet<String>,
) {
emit_doc_comment(&en.doc, "", out);
if !en.has_serde {
out.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &en.name,
},
));
return;
}
let all_unit = en.variants.iter().all(|v| v.fields.is_empty());
if all_unit {
let _ = mapper; out.push_str(&format!(
"public enum {}: String, Codable, Sendable, Hashable {{\n",
en.name
));
for variant in &en.variants {
emit_doc_comment(&variant.doc, " ", out);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
let raw_value = unit_enum_wire_value(variant, en.serde_rename_all.as_deref());
if raw_value == case_name.trim_matches('`') {
out.push_str(&crate::backends::swift::template_env::render(
"enum_case_unit.jinja",
minijinja::context! {
case_name => &case_name,
},
));
} else {
out.push_str(&format!(" case {case_name} = \"{raw_value}\"\n"));
}
}
out.push_str("}\n");
emit_enum_into_rust_extension(&en.name, out);
return;
}
if !all_variants_codable_safe(en, known_dto_names) {
out.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &en.name,
},
));
return;
}
let has_serde_tag = en.serde_tag.is_some() && !en.serde_untagged;
let is_serde_untagged = en.serde_untagged
&& en.variants.iter().any(|v| !v.fields.is_empty())
&& en
.variants
.iter()
.filter(|v| !v.fields.is_empty())
.all(|v| v.fields.len() == 1);
if has_serde_tag {
out.push_str(&format!("public enum {}: Codable, Sendable, Hashable {{\n", en.name));
for variant in &en.variants {
emit_variant_with_data(variant, out, mapper);
}
out.push('\n');
emit_serde_tagged_codable(en, out, mapper);
out.push_str("}\n");
} else if is_serde_untagged {
out.push_str(&format!("public enum {}: Codable, Sendable, Hashable {{\n", en.name));
for variant in &en.variants {
emit_variant_with_data(variant, out, mapper);
}
out.push('\n');
emit_serde_untagged_codable(en, out, mapper);
out.push_str("}\n");
} else {
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_header.jinja",
minijinja::context! {
name => &en.name,
},
));
for variant in &en.variants {
emit_variant_with_data(variant, out, mapper);
}
out.push_str("}\n");
}
emit_enum_into_rust_extension(&en.name, out);
}
fn all_variants_codable_safe(en: &EnumDef, known_dto_names: &std::collections::HashSet<String>) -> bool {
use crate::core::ir::TypeRef;
fn supported(ty: &TypeRef, known: &std::collections::HashSet<String>) -> bool {
match ty {
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Path
| TypeRef::Json
| TypeRef::Unit
| TypeRef::Bytes
| TypeRef::Duration => true,
TypeRef::Named(n) => known.contains(n),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => supported(inner, known),
TypeRef::Map(k, v) => supported(k, known) && supported(v, known),
}
}
en.variants
.iter()
.flat_map(|v| v.fields.iter())
.all(|f| supported(&f.ty, known_dto_names))
}
fn emit_enum_without_into_rust(
en: &EnumDef,
out: &mut String,
mapper: &SwiftMapper,
known_dto_names: &std::collections::HashSet<String>,
) {
emit_doc_comment(&en.doc, "", out);
if !en.has_serde {
out.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &en.name,
},
));
return;
}
let all_unit = en.variants.iter().all(|v| v.fields.is_empty());
if all_unit {
let _ = mapper;
out.push_str(&format!(
"public enum {}: String, Codable, Sendable, Hashable {{\n",
en.name
));
for variant in &en.variants {
emit_doc_comment(&variant.doc, " ", out);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
let raw_value = unit_enum_wire_value(variant, en.serde_rename_all.as_deref());
if raw_value == case_name.trim_matches('`') {
out.push_str(&crate::backends::swift::template_env::render(
"enum_case_unit.jinja",
minijinja::context! {
case_name => &case_name,
},
));
} else {
out.push_str(&format!(" case {case_name} = \"{raw_value}\"\n"));
}
}
out.push_str("}\n");
return;
}
if all_variants_codable_safe(en, known_dto_names) {
out.push_str(&format!("public enum {}: Codable, Sendable, Hashable {{\n", en.name));
for variant in &en.variants {
emit_variant_with_data(variant, out, mapper);
}
out.push_str("}\n");
} else {
out.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &en.name,
},
));
}
}
fn emit_enum_into_rust_extension(name: &str, out: &mut String) {
let from_json_fn = format!("{}_from_json", AsSnakeCase(name)).to_lower_camel_case();
out.push_str(&format!("extension {name} {{\n"));
out.push_str(&format!(" func intoRust() throws -> RustBridge.{name} {{\n"));
out.push_str(" let data = try JSONEncoder().encode(self)\n");
out.push_str(" let json = String(data: data, encoding: .utf8) ?? \"null\"\n");
out.push_str(&format!(" return try RustBridge.{from_json_fn}(json)\n"));
out.push_str(" }\n");
out.push_str("}\n");
}
fn unit_enum_wire_value(variant: &crate::core::ir::EnumVariant, rename_all: Option<&str>) -> String {
crate::codegen::naming::wire_variant_value(&variant.name, variant.serde_rename.as_deref(), rename_all)
}
fn emit_variant_with_data(variant: &EnumVariant, out: &mut String, mapper: &SwiftMapper) {
emit_doc_comment(&variant.doc, " ", out);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
if variant.fields.is_empty() {
out.push_str(&crate::backends::swift::template_env::render(
"enum_case_unit.jinja",
minijinja::context! {
case_name => &case_name,
},
));
} else {
let assoc: Vec<String> = variant
.fields
.iter()
.enumerate()
.map(|(idx, f)| {
let already_optional = matches!(&f.ty, TypeRef::Optional(_));
let ty_str = mapper.map_type(&f.ty);
let ty_with_opt = if f.optional && !already_optional {
format!("{ty_str}?")
} else {
ty_str
};
let label = swift_associated_label(&f.name, idx);
format!("{label}: {ty_with_opt}")
})
.collect();
out.push_str(&crate::backends::swift::template_env::render(
"enum_case_with_data.jinja",
minijinja::context! {
case_name => &case_name,
associated_values => assoc.join(", "),
},
));
}
}
fn swift_associated_label(name: &str, idx: usize) -> String {
let stripped = name.trim_start_matches('_');
if stripped.is_empty() || stripped.chars().all(|c| c.is_ascii_digit()) {
return format!("field{idx}");
}
swift_case_ident(&name.to_lower_camel_case())
}
fn emit_error(error: &ErrorDef, module_name: &str, out: &mut String, mapper: &SwiftMapper) {
let name = if error.name == "Error" {
format!("{module_name}Error")
} else {
error.name.clone()
};
emit_doc_comment(&error.doc, "", out);
out.push_str(&crate::backends::swift::template_env::render(
"error_enum_header.jinja",
minijinja::context! {
name => &name,
},
));
for variant in &error.variants {
emit_doc_comment(&variant.doc, " ", out);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
if variant.is_unit || variant.fields.is_empty() {
out.push_str(&crate::backends::swift::template_env::render(
"error_case.jinja",
minijinja::context! {
case_name => &case_name,
},
));
} else {
let mut assoc: Vec<String> = Vec::with_capacity(variant.fields.len() + 1);
let mut seen_message = false;
let mut labels: BTreeSet<String> = BTreeSet::new();
for (idx, f) in variant.fields.iter().enumerate() {
let already_optional = matches!(&f.ty, TypeRef::Optional(_));
let ty_str = mapper.map_type(&f.ty);
let ty_with_opt = if f.optional && !already_optional {
format!("{ty_str}?")
} else {
ty_str
};
let mut label = swift_associated_label(&f.name, idx);
while labels.contains(&label) {
label = format!("{label}{idx}");
}
labels.insert(label.clone());
if label == "message" {
seen_message = true;
}
assoc.push(format!("{label}: {ty_with_opt}"));
}
if !seen_message {
assoc.insert(0, "message: String".to_string());
}
out.push_str(&crate::backends::swift::template_env::render(
"error_case_with_data.jinja",
minijinja::context! {
case_name => &case_name,
associated_values => assoc.join(", "),
},
));
}
}
out.push_str("}\n");
if !error.methods.is_empty() {
out.push('\n');
out.push_str(&format!("extension {name} {{\n"));
for method in &error.methods {
let prop_name = swift_case_ident(&method.name.to_lower_camel_case());
let return_ty = swift_type_name(&method.return_type);
let default_val = swift_default_for_type(&method.return_type);
out.push_str(&format!(" public var {prop_name}: {return_ty} {{\n"));
out.push_str(" switch self {\n");
for variant in &error.variants {
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
let field_match = variant.fields.iter().find(|f| {
let camel = f.name.to_lower_camel_case();
let prop_snake = method.name.as_str();
camel == prop_name
|| f.name == prop_snake
|| (prop_snake == "status_code" && (f.name == "status" || camel == "status"))
});
let wildcard = if variant.is_unit || variant.fields.is_empty() {
String::new()
} else {
let mut args: Vec<String> = variant
.fields
.iter()
.enumerate()
.map(|(i, f)| {
let label = swift_associated_label(&f.name, i);
if let Some(fm) = &field_match {
if fm.name == f.name {
return format!("{label}: let matched");
}
}
format!("{label}: _")
})
.collect();
let has_message_field = variant.fields.iter().any(|f| f.name == "message");
if !has_message_field {
args.insert(0, "message: _".to_string());
}
format!("({})", args.join(", "))
};
let ret_expr = if field_match.is_some() && !variant.is_unit && !variant.fields.is_empty() {
"matched".to_string()
} else {
default_val.clone()
};
out.push_str(&format!(" case .{case_name}{wildcard}: return {ret_expr}\n"));
}
out.push_str(" }\n");
out.push_str(" }\n");
}
out.push_str("}\n");
}
}
fn swift_default_for_type(ty: &TypeRef) -> String {
match ty {
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "false".to_string(),
_ => "0".to_string(),
}
}
TypeRef::String => "\"\"".to_string(),
TypeRef::Optional(_) => "nil".to_string(),
_ => "nil".to_string(),
}
}
fn emit_client_class(
type_name: &str,
methods: &[MethodDef],
mapper: &impl TypeMapper,
config: &ResolvedCrateConfig,
first_class_types: &std::collections::HashSet<String>,
out: &mut String,
) {
use heck::ToSnakeCase;
let snake_name = type_name.to_snake_case();
let constructor_fn = swift_ident(&format!("create_{snake_name}").to_lower_camel_case());
out.push_str(&format!("public final class {type_name} {{\n"));
out.push_str(&format!(" private let inner: RustBridge.{type_name}\n"));
out.push_str(" public init(apiKey: String, baseUrl: String? = nil) throws {\n");
out.push_str(&format!(
" self.inner = try RustBridge.{constructor_fn}(apiKey, baseUrl)\n"
));
out.push_str(" }\n");
out.push_str(&format!(" internal init(_ inner: RustBridge.{type_name}) {{\n"));
out.push_str(" self.inner = inner\n");
out.push_str(" }\n");
for method in methods {
if method.sanitized {
continue;
}
let method_snake = method.name.to_snake_case();
let method_camel = swift_ident(&method_snake.to_lower_camel_case());
let bridge_fn_snake = format!("{snake_name}_{method_snake}");
let bridge_fn_camel = swift_ident(&bridge_fn_snake.to_lower_camel_case());
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let swift_name = swift_ident(&p.name.to_lower_camel_case());
let ty_str = if p.optional {
format!("{}?", mapper.map_type(&p.ty))
} else {
mapper.map_type(&p.ty)
};
format!("_ {swift_name}: {ty_str}")
})
.collect();
let params_str = params.join(", ");
let has_dto_param = method
.params
.iter()
.any(|p| matches!(&p.ty, TypeRef::Named(n) if first_class_types.contains(n)));
let args: Vec<String> = method
.params
.iter()
.map(|p| {
let swift_name = swift_ident(&p.name.to_lower_camel_case());
match &p.ty {
TypeRef::Named(n) if first_class_types.contains(n) => {
if p.optional {
format!("try {swift_name}?.intoRust()")
} else {
format!("try {swift_name}.intoRust()")
}
}
_ => swift_name,
}
})
.collect();
let args_str = if args.is_empty() {
String::new()
} else {
format!(", {}", args.join(", "))
};
let return_ty = mapper.map_type(&method.return_type);
let needs_return_init = matches!(
&method.return_type,
TypeRef::Named(n) if first_class_types.contains(n)
);
let needs_throws = method.error_type.is_some() || has_dto_param || needs_return_init;
let throws_clause = if needs_throws { " throws" } else { "" };
let async_clause = if method.is_async { " async" } else { "" };
let return_clause = if matches!(method.return_type, TypeRef::Unit) {
String::new()
} else {
format!(" -> {return_ty}")
};
emit_doc_comment(&method.doc, " ", out);
out.push_str(&format!(
" public func {method_camel}({params_str}){async_clause}{throws_clause}{return_clause} {{\n"
));
if matches!(method.return_type, TypeRef::Unit) {
out.push_str(&format!(
" {throws_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str})\n",
throws_kw = if needs_throws { "try " } else { "" }
));
} else {
let await_kw = if method.is_async { "await " } else { "" };
let try_kw = if needs_throws { "try " } else { "" };
let bytes_suffix = if matches!(method.return_type, TypeRef::Bytes) {
".map { Data($0.map { $0 }) }"
} else {
""
};
if bytes_suffix.is_empty() {
if needs_return_init {
out.push_str(&format!(
" return try {return_ty}({try_kw}{await_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str}))\n"
));
} else {
out.push_str(&format!(
" return {try_kw}{await_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str})\n"
));
}
} else {
out.push_str(&format!(
" let _bytes = {try_kw}{await_kw}RustBridge.{bridge_fn_camel}(self.inner{args_str})\n"
));
out.push_str(" return Data(_bytes.map { $0 })\n");
}
}
out.push_str(" }\n");
}
for adapter in config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| a.owner_type.as_deref() == Some(type_name))
{
emit_streaming_client_method(adapter, &snake_name, first_class_types, out);
}
out.push_str("}\n");
}
fn emit_streaming_client_method(
adapter: &AdapterConfig,
owner_snake: &str,
first_class_types: &std::collections::HashSet<String>,
out: &mut String,
) {
use heck::{AsSnakeCase, ToLowerCamelCase};
let method_camel = swift_ident(&adapter.name.to_lower_camel_case());
let start_fn_snake = format!("{owner_snake}_{}_start", adapter.name);
let start_fn_camel = swift_ident(&start_fn_snake.to_lower_camel_case());
let item_type = adapter.item_type.as_deref().unwrap_or("String");
let item_type_from_json = swift_ident(&format!("{}_from_json", AsSnakeCase(item_type)).to_lower_camel_case());
let params: Vec<String> = adapter
.params
.iter()
.map(|p| {
let swift_name = swift_ident(&p.name.to_lower_camel_case());
let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
format!("_ {swift_name}: {simple_ty}")
})
.collect();
let params_str = params.join(", ");
let call_args: Vec<String> = adapter
.params
.iter()
.map(|p| {
let swift_name = swift_ident(&p.name.to_lower_camel_case());
let simple_ty = p.ty.rsplit("::").next().unwrap_or(&p.ty);
if first_class_types.contains(simple_ty) {
format!("(try {swift_name}.intoRust())")
} else {
swift_name
}
})
.collect();
let call_args_str = if call_args.is_empty() {
String::new()
} else {
format!(", {}", call_args.join(", "))
};
out.push_str(&format!(
" public func {method_camel}({params_str}) async throws -> AsyncThrowingStream<{item_type}, Error> {{\n"
));
out.push_str(" let inner = self.inner\n");
out.push_str(" let handle = try await Task.detached(priority: .userInitiated) {\n");
out.push_str(&format!(
" try RustBridge.{start_fn_camel}(inner{call_args_str})\n"
));
out.push_str(" }.value\n\n");
out.push_str(&format!(
" return AsyncThrowingStream<{item_type}, Error> {{ continuation in\n"
));
out.push_str(" let task = Task.detached(priority: .userInitiated) {\n");
out.push_str(" do {\n");
out.push_str(" while !Task.isCancelled {\n");
out.push_str(" let json = try handle.next().toString()\n");
out.push_str(" if json.isEmpty { break }\n");
if first_class_types.contains(item_type) {
out.push_str(" let chunkData = json.data(using: .utf8) ?? Data()\n");
out.push_str(&format!(
" let chunk = try JSONDecoder().decode({item_type}.self, from: chunkData)\n"
));
} else {
out.push_str(&format!(
" let chunk = try RustBridge.{item_type_from_json}(json)\n"
));
}
out.push_str(" continuation.yield(chunk)\n");
out.push_str(" }\n");
out.push_str(" continuation.finish()\n");
out.push_str(" } catch {\n");
out.push_str(" continuation.finish(throwing: error)\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" continuation.onTermination = { _ in task.cancel() }\n");
out.push_str(" }\n");
out.push_str(" }\n");
}
fn emit_stream_handle_sendable(adapter: &AdapterConfig, owner_type: &str, out: &mut String) {
use heck::ToPascalCase;
let owner_pascal = owner_type.to_pascal_case();
let adapter_pascal = adapter.name.to_pascal_case();
let handle_name = format!("{owner_pascal}{adapter_pascal}StreamHandle");
out.push_str(&format!("// MARK: - Sendable conformance for {handle_name}\n"));
out.push_str("// swift-bridge opaque types are not automatically Sendable. The Rust\n");
out.push_str("// side uses Mutex<stream> + tokio Runtime — both Send + Sync — so\n");
out.push_str("// @unchecked is correct: thread-safety is enforced by Rust.\n");
out.push_str(&format!(
"extension RustBridge.{handle_name}: @unchecked Sendable {{}}\n"
));
}
fn emit_streaming_free_functions(
config: &ResolvedCrateConfig,
first_class_types: &std::collections::HashSet<String>,
out: &mut String,
) {
use heck::{AsSnakeCase, ToLowerCamelCase, ToSnakeCase};
let client_constructor_types: std::collections::HashSet<&str> = config
.swift
.as_ref()
.map(|c| c.client_constructor_body.keys().map(String::as_str).collect())
.unwrap_or_default();
let orphan_adapters: Vec<&AdapterConfig> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "swift"))
.filter(|a| {
a.owner_type
.as_deref()
.map(|t| !client_constructor_types.contains(t))
.unwrap_or(false)
})
.collect();
if orphan_adapters.is_empty() {
return;
}
out.push_str("// MARK: - Streaming free functions\n");
out.push_str(
"// These adapters are owned by opaque handle types that do not have a\n\
// Swift class wrapper (no client_constructor_body in alef.toml). The\n\
// streaming methods are therefore exposed as module-level free functions\n\
// that accept the owner handle as their first parameter.\n\n",
);
let mut sendable_emitted: std::collections::HashSet<String> = std::collections::HashSet::new();
for adapter in &orphan_adapters {
let owner_type = adapter.owner_type.as_deref().unwrap_or("");
if sendable_emitted.insert(owner_type.to_string()) {
out.push_str(&format!(
"// MARK: - Sendable conformance for {owner_type} (streaming owner)\n"
));
out.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
out.push_str("// Captured by Task.detached in streaming free functions — Rust type is Send + Sync.\n");
out.push_str(&format!(
"extension RustBridge.{owner_type}: @unchecked Sendable {{}}\n\n"
));
}
emit_stream_handle_sendable(adapter, owner_type, out);
out.push('\n');
for param in &adapter.params {
let simple_ty = param.ty.rsplit("::").next().unwrap_or(¶m.ty);
if !first_class_types.contains(simple_ty) {
let key = format!("param:{simple_ty}");
if sendable_emitted.insert(key) {
out.push_str(&format!(
"// MARK: - Sendable conformance for {simple_ty} (streaming request param)\n"
));
out.push_str("// swift-bridge opaque types are not automatically Sendable.\n");
out.push_str("// Passed into Task.detached for streaming — Rust type is Send + Sync.\n");
out.push_str(&format!(
"extension RustBridge.{simple_ty}: @unchecked Sendable {{}}\n\n"
));
}
}
}
}
for adapter in &orphan_adapters {
let owner_type = adapter.owner_type.as_deref().unwrap_or("");
let owner_snake = owner_type.to_snake_case();
let method_camel = swift_ident(&adapter.name.to_lower_camel_case());
let start_fn_snake = format!("{owner_snake}_{}_start", adapter.name);
let start_fn_camel = swift_ident(&start_fn_snake.to_lower_camel_case());
let item_type = adapter.item_type.as_deref().unwrap_or("String");
let item_type_from_json = swift_ident(&format!("{}_from_json", AsSnakeCase(item_type)).to_lower_camel_case());
let owner_camel = swift_ident(&owner_type.to_lower_camel_case());
let mut sig_params: Vec<String> = vec![format!("_ {owner_camel}: {owner_type}")];
let mut call_args: Vec<String> = vec![];
for param in &adapter.params {
let swift_name = swift_ident(¶m.name.to_lower_camel_case());
let simple_ty = param.ty.rsplit("::").next().unwrap_or(¶m.ty);
sig_params.push(format!("_ {swift_name}: {simple_ty}"));
if first_class_types.contains(simple_ty) {
call_args.push(format!("(try {swift_name}.intoRust())"));
} else {
call_args.push(swift_name);
}
}
let params_str = sig_params.join(", ");
let call_args_str = if call_args.is_empty() {
String::new()
} else {
format!(", {}", call_args.join(", "))
};
out.push_str(&format!(
"public func {method_camel}({params_str}) async throws -> AsyncThrowingStream<{item_type}, Error> {{\n"
));
out.push_str(" let handle = try await Task.detached(priority: .userInitiated) {\n");
out.push_str(&format!(
" try RustBridge.{start_fn_camel}({owner_camel}{call_args_str})\n"
));
out.push_str(" }.value\n\n");
out.push_str(&format!(
" return AsyncThrowingStream<{item_type}, Error> {{ continuation in\n"
));
out.push_str(" let task = Task.detached(priority: .userInitiated) {\n");
out.push_str(" do {\n");
out.push_str(" while !Task.isCancelled {\n");
out.push_str(" let json = try handle.next().toString()\n");
out.push_str(" if json.isEmpty { break }\n");
if first_class_types.contains(item_type) {
out.push_str(" let chunkData = json.data(using: .utf8) ?? Data()\n");
out.push_str(&format!(
" let chunk = try JSONDecoder().decode({item_type}.self, from: chunkData)\n"
));
} else {
out.push_str(&format!(
" let chunk = try RustBridge.{item_type_from_json}(json)\n"
));
}
out.push_str(" continuation.yield(chunk)\n");
out.push_str(" }\n");
out.push_str(" continuation.finish()\n");
out.push_str(" } catch {\n");
out.push_str(" continuation.finish(throwing: error)\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" continuation.onTermination = { _ in task.cancel() }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
}
}
fn emit_doc_comment(doc: &str, indent: &str, out: &mut String) {
if doc.is_empty() {
return;
}
out.push_str(&crate::backends::swift::template_env::render(
"doc_comment.jinja",
minijinja::context! {
indent => indent,
lines => doc.lines().collect::<Vec<_>>(),
},
));
}
fn first_param_is(func_def: &FunctionDef, ty: &TypeRef) -> bool {
func_def.params.first().map(|p| &p.ty == ty).unwrap_or(false)
}
fn emit_convenience_wrappers(api: &ApiSurface, out: &mut String) {
let all_names: std::collections::HashSet<&str> = api.functions.iter().map(|f| f.name.as_str()).collect();
let bytes_candidates: Vec<&FunctionDef> = api
.functions
.iter()
.filter(|f| first_param_is(f, &TypeRef::Bytes) && !f.is_async && !convenience_name_shadows_bridge(f))
.collect();
let path_candidates: Vec<&FunctionDef> = api
.functions
.iter()
.filter(|f| first_param_is(f, &TypeRef::Path) && !f.is_async && !convenience_name_shadows_bridge(f))
.collect();
if bytes_candidates.is_empty() && path_candidates.is_empty() {
return;
}
out.push_str("// MARK: - Convenience Wrapper Functions\n");
out.push_str("// These wrappers bridge String / [UInt8] inputs to RustBridge's\n");
out.push_str("// RustVec<UInt8> requirement. The config parameter must be a fully\n");
out.push_str("// constructed opaque type (built via the generated initializer);\n");
out.push_str("// JSON-config decoding is not available because swift-bridge opaque\n");
out.push_str("// proxy classes are not Codable Swift structs.\n\n");
if !bytes_candidates.is_empty() {
out.push_str("/// Converts a Swift `[UInt8]` array to a `RustVec<UInt8>` by pushing each byte.\n");
out.push_str("/// swift-bridge's `RustVec<T>` runtime only exposes `init()` and `push(value:)`;\n");
out.push_str("/// no array-initializer shorthand exists.\n");
out.push_str("private func makeByteVec(_ bytes: [UInt8]) -> RustVec<UInt8> {\n");
out.push_str(" let vec = RustVec<UInt8>()\n");
out.push_str(" for b in bytes { vec.push(value: b) }\n");
out.push_str(" return vec\n");
out.push_str("}\n\n");
}
for func in &bytes_candidates {
emit_bytes_overloads(func, &all_names, out);
}
for func in &path_candidates {
emit_path_overload(func, &all_names, out);
}
let _ = api;
}
fn emit_json_string_overloads(api: &ApiSurface, out: &mut String) {
use heck::AsSnakeCase;
let json_overload_candidates: Vec<(&FunctionDef, usize, &str)> = api
.functions
.iter()
.flat_map(|func| {
func.params
.iter()
.enumerate()
.filter_map(move |(idx, param)| {
if let TypeRef::Named(type_name) = ¶m.ty {
if let Some(typ) = api.types.iter().find(|t| &t.name == type_name) {
if typ.has_serde && !typ.is_opaque {
return Some((func, idx, type_name.as_str()));
}
}
}
None
})
.collect::<Vec<_>>()
})
.collect();
if json_overload_candidates.is_empty() {
return;
}
out.push_str("// MARK: - JSON-String Convenience Overloads\n");
out.push_str("// These overloads accept JSON-encoded config parameters and decode them automatically.\n");
out.push_str("// Enables e2e tests to pass JSON strings directly without typed config construction.\n\n");
emit_load_bytes_from_path_or_utf8(out);
let mut emitted_funcs: std::collections::HashSet<String> = std::collections::HashSet::new();
for (func, config_param_idx, config_type_name) in json_overload_candidates {
let func_key = format!("{}_{}", func.name, config_param_idx);
if !emitted_funcs.insert(func_key) {
continue; }
let swift_func_name = swift_ident(&func.name.to_lower_camel_case());
let config_type_snake = AsSnakeCase(config_type_name).to_string();
let config_from_json_name = format!("{config_type_snake}_from_json").to_lower_camel_case();
let mut param_strs: Vec<String> = Vec::new();
for (i, param) in func.params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
if i == config_param_idx {
param_strs.push("_ configJson: String".to_string());
} else {
let ty_str = if param.optional {
format!("{}?", swift_type_name(¶m.ty))
} else {
swift_type_name(¶m.ty)
};
param_strs.push(format!("_ {param_name}: {ty_str}"));
}
}
let params_sig = param_strs.join(", ");
let return_ty = swift_return_type(&func.return_type);
let async_clause = if func.is_async { " async" } else { "" };
let throws_clause = " throws";
let return_suffix = swift_return_conversion_suffix(&func.return_type);
let mut call_args: Vec<String> = Vec::new();
for (i, param) in func.params.iter().enumerate() {
let param_name = param.name.to_lower_camel_case();
if i == config_param_idx {
call_args.push(format!("{param_name}: config"));
} else {
call_args.push(format!("{param_name}: {param_name}"));
}
}
let call_args_str = call_args.join(", ");
out.push_str(&format!(
"public func {swift_func_name}({params_sig}){async_clause}{throws_clause} -> {return_ty} {{\n"
));
out.push_str(&format!(" let config = try {config_from_json_name}(configJson)\n"));
let await_kw = if func.is_async { "await " } else { "" };
out.push_str(&format!(
" return try {await_kw}{swift_func_name}({call_args_str}){return_suffix}\n"
));
out.push_str("}\n\n");
}
}
fn emit_load_bytes_from_path_or_utf8(out: &mut String) {
out.push_str("/// Resolves a string argument as either a file path or literal UTF-8 content.\n");
out.push_str("/// Searches: current working directory, ALEF_TEST_DOCUMENTS_DIR env var,\n");
out.push_str("/// and ancestor `test_documents/` or `fixtures/` directories (up to 16 levels).\n");
out.push_str("/// If no file is found, treats the string as UTF-8 content and returns its bytes.\n");
out.push_str("private func _loadBytesFromPathOrUtf8(_ pathOrContent: String) throws -> [UInt8] {\n");
out.push_str(" let fm = FileManager.default\n");
out.push_str(" var roots: [String] = [fm.currentDirectoryPath]\n");
out.push_str(" if let envRoot = ProcessInfo.processInfo.environment[\"ALEF_TEST_DOCUMENTS_DIR\"] {\n");
out.push_str(" roots.append(envRoot)\n");
out.push_str(" }\n");
out.push_str(" var walker = URL(fileURLWithPath: fm.currentDirectoryPath)\n");
out.push_str(" for _ in 0..<16 {\n");
out.push_str(" roots.append(walker.appendingPathComponent(\"test_documents\").path)\n");
out.push_str(" roots.append(walker.appendingPathComponent(\"fixtures\").path)\n");
out.push_str(" let parent = walker.deletingLastPathComponent()\n");
out.push_str(" if parent.path == walker.path { break }\n");
out.push_str(" walker = parent\n");
out.push_str(" }\n");
out.push_str(
" let candidates = [pathOrContent] + roots.map { ($0 as NSString).appendingPathComponent(pathOrContent) }\n",
);
out.push_str(" for path in candidates {\n");
out.push_str(
" if fm.fileExists(atPath: path), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {\n",
);
out.push_str(" return [UInt8](data)\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str(" return [UInt8](pathOrContent.utf8)\n");
out.push_str("}\n\n");
}
fn emit_from_json_forwarders(
api: &ApiSurface,
exclude_types: &std::collections::HashSet<String>,
mapper: &SwiftMapper,
exclude_fields: &std::collections::HashSet<String>,
known_dto_names: &std::collections::HashSet<String>,
out: &mut String,
) {
use heck::AsSnakeCase;
let struct_candidates: Vec<&str> = api
.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && t.has_serde)
.filter(|t| !exclude_types.contains(&t.name))
.map(|t| t.name.as_str())
.collect();
let enum_candidates: Vec<&str> = api
.enums
.iter()
.filter(|e| e.has_serde && !exclude_types.contains(&e.name))
.map(|e| e.name.as_str())
.collect();
if struct_candidates.is_empty() && enum_candidates.is_empty() {
return;
}
out.push_str("// MARK: - From-JSON Helpers\n");
out.push_str("// Public helpers that decode JSON into first-class Swift types.\n");
out.push_str("// First-class struct types (Codable) use JSONDecoder directly.\n");
out.push_str("// Opaque RustBridge types forward to RustBridge.\n\n");
let first_class_set: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| !t.is_trait && can_emit_first_class_struct(t, mapper, exclude_fields, known_dto_names))
.map(|t| t.name.as_str())
.collect();
for type_name in struct_candidates {
let type_snake = AsSnakeCase(type_name).to_string();
let swift_name = format!("{type_snake}_from_json").to_lower_camel_case();
if first_class_set.contains(type_name) {
out.push_str(&format!(
"public func {swift_name}(_ json: String) throws -> {type_name} {{\n let data = json.data(using: .utf8) ?? Data()\n return try JSONDecoder().decode({type_name}.self, from: data)\n}}\n\n"
));
} else {
out.push_str(&format!(
"public func {swift_name}(_ json: String) throws -> {type_name} {{\n return try RustBridge.{swift_name}(json)\n}}\n\n"
));
}
}
let codable_enum_set: std::collections::HashSet<&str> = api
.enums
.iter()
.filter(|e| e.has_serde && enum_emits_codable(e, known_dto_names))
.map(|e| e.name.as_str())
.collect();
for enum_name in enum_candidates {
let enum_snake = AsSnakeCase(enum_name).to_string();
let swift_name = format!("{enum_snake}_from_json").to_lower_camel_case();
if codable_enum_set.contains(enum_name) {
out.push_str(&format!(
"public func {swift_name}(_ json: String) throws -> {enum_name} {{\n let data = json.data(using: .utf8) ?? Data()\n return try JSONDecoder().decode({enum_name}.self, from: data)\n}}\n\n"
));
} else {
out.push_str(&format!(
"public func {swift_name}(_ json: String) throws -> {enum_name} {{\n return try RustBridge.{swift_name}(json)\n}}\n\n"
));
}
}
}
fn enum_emits_codable(en: &crate::core::ir::EnumDef, known_dto_names: &std::collections::HashSet<String>) -> bool {
if !en.has_serde {
return false;
}
let all_unit = en.variants.iter().all(|v| v.fields.is_empty());
if all_unit {
return true;
}
all_variants_codable_safe(en, known_dto_names)
}
fn emit_bytes_overloads(func: &FunctionDef, _all_names: &std::collections::HashSet<&str>, out: &mut String) {
let swift_inner = swift_ident(&func.name.to_lower_camel_case());
let wrapper_name = if swift_inner.ends_with("Sync") {
swift_inner[..swift_inner.len() - 4].to_string()
} else {
swift_inner.clone()
};
let inner_call = swift_inner.clone();
let trailing_params: Vec<&crate::core::ir::ParamDef> = func.params.iter().skip(1).collect();
let return_ty = swift_return_type(&func.return_type);
let throws_clause = if func.error_type.is_some() { " throws" } else { "" };
let return_suffix = swift_return_conversion_suffix(&func.return_type);
let trailing_param_text = render_trailing_params(trailing_params.iter().copied());
let trailing_args = render_trailing_args(trailing_params.iter().copied());
out.push_str(&crate::backends::swift::template_env::render(
"swift_bytes_string_overload.jinja",
minijinja::context! {
wrapper_name => &wrapper_name,
trailing_params => &trailing_param_text,
throws_clause => throws_clause,
return_ty => &return_ty,
inner_call => &inner_call,
trailing_args => &trailing_args,
return_suffix => &return_suffix,
},
));
out.push_str(&crate::backends::swift::template_env::render(
"swift_bytes_array_overload.jinja",
minijinja::context! {
wrapper_name => &wrapper_name,
trailing_params => &trailing_param_text,
throws_clause => throws_clause,
return_ty => &return_ty,
inner_call => &inner_call,
trailing_args => &trailing_args,
return_suffix => &return_suffix,
},
));
}
fn emit_path_overload(func: &FunctionDef, _all_names: &std::collections::HashSet<&str>, out: &mut String) {
let swift_inner = swift_ident(&func.name.to_lower_camel_case());
let wrapper_name = if swift_inner.ends_with("Sync") {
swift_inner[..swift_inner.len() - 4].to_string()
} else {
swift_inner.clone()
};
let inner_call = swift_inner.clone();
let trailing_params: Vec<&crate::core::ir::ParamDef> = func.params.iter().skip(1).collect();
let return_ty = swift_return_type(&func.return_type);
let throws_clause = if func.error_type.is_some() { " throws" } else { "" };
let return_suffix = swift_return_conversion_suffix(&func.return_type);
let trailing_param_text = render_trailing_params_with_defaults(trailing_params.iter().copied());
let trailing_args = render_trailing_args(trailing_params.iter().copied());
out.push_str(&crate::backends::swift::template_env::render(
"swift_path_overload.jinja",
minijinja::context! {
wrapper_name => &wrapper_name,
trailing_params => &trailing_param_text,
throws_clause => throws_clause,
return_ty => &return_ty,
inner_call => &inner_call,
trailing_args => &trailing_args,
return_suffix => &return_suffix,
},
));
}
fn render_trailing_params<'a>(params: impl Iterator<Item = &'a crate::core::ir::ParamDef>) -> String {
let mut out = String::new();
for p in params {
let swift_name = p.name.to_lower_camel_case();
let ty_str = if p.optional {
format!("{}?", swift_type_name(&p.ty))
} else {
swift_type_name(&p.ty)
};
out.push_str(&crate::backends::swift::template_env::render(
"swift_trailing_param.jinja",
minijinja::context! {
swift_name => &swift_name,
ty_str => &ty_str,
},
));
}
out
}
fn render_trailing_params_with_defaults<'a>(params: impl Iterator<Item = &'a crate::core::ir::ParamDef>) -> String {
let mut out = String::new();
for p in params {
let swift_name = p.name.to_lower_camel_case();
if p.optional {
let ty_str = swift_type_name(&p.ty);
out.push_str(&crate::backends::swift::template_env::render(
"swift_trailing_param_optional_default.jinja",
minijinja::context! {
swift_name => &swift_name,
ty_str => &ty_str,
},
));
} else {
let ty_str = swift_type_name(&p.ty);
out.push_str(&crate::backends::swift::template_env::render(
"swift_trailing_param.jinja",
minijinja::context! {
swift_name => &swift_name,
ty_str => &ty_str,
},
));
}
}
out
}
fn render_trailing_args<'a>(params: impl Iterator<Item = &'a crate::core::ir::ParamDef>) -> String {
let mut out = String::new();
for p in params {
let swift_name = p.name.to_lower_camel_case();
out.push_str(&crate::backends::swift::template_env::render(
"swift_trailing_arg.jinja",
minijinja::context! {
swift_name => &swift_name,
},
));
}
out
}
fn swift_type_name(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "String".to_string(),
TypeRef::Bytes => "[UInt8]".to_string(),
TypeRef::Path => "String".to_string(),
TypeRef::Named(name) => name.clone(),
TypeRef::Optional(inner) => format!("{}?", swift_type_name(inner)),
TypeRef::Vec(inner) => format!("[{}]", swift_type_name(inner)),
TypeRef::Map(k, v) => format!("[{}: {}]", swift_type_name(k), swift_type_name(v)),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "Bool",
PrimitiveType::U8 => "UInt8",
PrimitiveType::U16 => "UInt16",
PrimitiveType::U32 => "UInt32",
PrimitiveType::U64 => "UInt64",
PrimitiveType::I8 => "Int8",
PrimitiveType::I16 => "Int16",
PrimitiveType::I32 => "Int32",
PrimitiveType::I64 => "Int64",
PrimitiveType::Usize => "UInt",
PrimitiveType::Isize => "Int",
PrimitiveType::F32 => "Float",
PrimitiveType::F64 => "Double",
}
.to_string()
}
TypeRef::Unit => "Void".to_string(),
TypeRef::Json => "String".to_string(),
TypeRef::Duration => "Duration".to_string(),
TypeRef::Char => "Character".to_string(),
}
}
fn swift_return_type(ty: &TypeRef) -> String {
swift_type_name(ty)
}
fn swift_return_conversion_suffix(ty: &TypeRef) -> String {
match ty {
TypeRef::Bytes => ".map { $0 }".to_string(),
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(_)) => ".map { $0 }".to_string(),
_ => String::new(),
}
}
fn convenience_name_shadows_bridge(func: &FunctionDef) -> bool {
let swift_inner = swift_ident(&func.name.to_lower_camel_case());
let wrapper_name = if swift_inner.ends_with("Sync") {
swift_inner[..swift_inner.len() - 4].to_string()
} else {
swift_inner.clone()
};
wrapper_name == swift_inner
}
fn find_swift_bridge_out_dir(binding_crate_name: &str) -> Option<PathBuf> {
let cwd = std::env::current_dir().ok()?;
let workspace_root = std::iter::once(cwd.clone())
.chain(cwd.ancestors().skip(1).map(|p| p.to_path_buf()))
.take(8)
.find(|p| p.join("Cargo.lock").exists())?;
let target = workspace_root.join("target");
let crate_prefix = format!("{binding_crate_name}-");
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
for profile in ["release", "debug"] {
let build_dir = target.join(profile).join("build");
let entries = match std::fs::read_dir(&build_dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !name_str.starts_with(&crate_prefix) {
continue;
}
let out = entry.path().join("out");
let marker = out.join("SwiftBridgeCore.swift");
if !marker.exists() {
continue;
}
let mtime = std::fs::metadata(&marker)
.and_then(|m| m.modified())
.unwrap_or(std::time::UNIX_EPOCH);
if best.as_ref().map(|(t, _)| mtime > *t).unwrap_or(true) {
best = Some((mtime, out));
}
}
}
best.map(|(_, p)| p)
}
fn emit_swift_bridge_files(
crate_name: &str,
binding_crate_name: &str,
package_root: &std::path::Path,
) -> anyhow::Result<Option<Vec<GeneratedFile>>> {
let out_dir = match find_swift_bridge_out_dir(binding_crate_name) {
Some(d) => d,
None => {
let sources_rust_bridge_c = package_root.join("Sources").join("RustBridgeC");
let header_path = sources_rust_bridge_c.join("RustBridgeC.h");
if let Ok(existing) = std::fs::read_to_string(&header_path) {
if existing.contains("Concatenates SwiftBridgeCore.h") {
return Ok(None);
}
}
let minimal_header = format!(
"#ifndef RUST_BRIDGE_C_H\n\
#define RUST_BRIDGE_C_H\n\
\n\
// Placeholder header for the RustBridgeC SwiftPM target.\n\
// Run `cargo build -p {binding_crate_name}` and re-run `alef generate` to populate.\n\
// The typedefs below are the minimum required for SwiftBridgeCore.swift\n\
// to compile before the full cargo build has been run.\n\
\n\
#include <stdint.h>\n\
#include <stdbool.h>\n\
\n\
typedef struct RustStr {{ uint8_t* const start; uintptr_t len; }} RustStr;\n\
typedef struct __private__FfiSlice {{ void* const start; uintptr_t len; }} __private__FfiSlice;\n\
typedef struct __private__OptionU8 {{ uint8_t val; bool is_some; }} __private__OptionU8;\n\
typedef struct __private__OptionI8 {{ int8_t val; bool is_some; }} __private__OptionI8;\n\
typedef struct __private__OptionU16 {{ uint16_t val; bool is_some; }} __private__OptionU16;\n\
typedef struct __private__OptionI16 {{ int16_t val; bool is_some; }} __private__OptionI16;\n\
typedef struct __private__OptionU32 {{ uint32_t val; bool is_some; }} __private__OptionU32;\n\
typedef struct __private__OptionI32 {{ int32_t val; bool is_some; }} __private__OptionI32;\n\
typedef struct __private__OptionU64 {{ uint64_t val; bool is_some; }} __private__OptionU64;\n\
typedef struct __private__OptionI64 {{ int64_t val; bool is_some; }} __private__OptionI64;\n\
typedef struct __private__OptionUsize {{ uintptr_t val; bool is_some; }} __private__OptionUsize;\n\
typedef struct __private__OptionIsize {{ intptr_t val; bool is_some; }} __private__OptionIsize;\n\
typedef struct __private__OptionF32 {{ float val; bool is_some; }} __private__OptionF32;\n\
typedef struct __private__OptionF64 {{ double val; bool is_some; }} __private__OptionF64;\n\
typedef struct __private__OptionBool {{ bool val; bool is_some; }} __private__OptionBool;\n\
\n\
#endif /* RUST_BRIDGE_C_H */\n"
);
return Ok(Some(vec![GeneratedFile {
path: header_path,
content: minimal_header,
generated_header: false,
}]));
}
};
let core_swift_src = out_dir.join("SwiftBridgeCore.swift");
let crate_swift_src = out_dir
.join(binding_crate_name)
.join(format!("{binding_crate_name}.swift"));
let core_h_src = out_dir.join("SwiftBridgeCore.h");
let crate_h_src = out_dir.join(binding_crate_name).join(format!("{binding_crate_name}.h"));
for p in [&core_swift_src, &crate_swift_src, &core_h_src, &crate_h_src] {
if !p.exists() {
return Ok(None);
}
}
let core_swift = std::fs::read_to_string(&core_swift_src)
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", core_swift_src.display()))?;
let crate_swift = std::fs::read_to_string(&crate_swift_src)
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", crate_swift_src.display()))?;
let core_h = std::fs::read_to_string(&core_h_src)
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", core_h_src.display()))?;
let crate_h = std::fs::read_to_string(&crate_h_src)
.map_err(|e| anyhow::anyhow!("failed to read {}: {e}", crate_h_src.display()))?;
let core_swift_content = make_swift_bridge_ref_ptr_public(&append_rust_string_ref_to_string_extension(
&prepend_rust_bridge_c_import(&core_swift),
));
let crate_swift_content = make_swift_bridge_ref_ptr_public(&prepend_rust_bridge_c_import(&crate_swift));
let rust_bridge_c_h = format!(
"#ifndef RUST_BRIDGE_C_H\n\
#define RUST_BRIDGE_C_H\n\
\n\
// Auto-generated by alef — do not edit by hand.\n\
// Concatenates SwiftBridgeCore.h and {binding_crate_name}.h produced by\n\
// `cargo build -p {binding_crate_name}` via swift_bridge_build.\n\
\n\
{core_h}\n\
{crate_h}\n\
#endif /* RUST_BRIDGE_C_H */\n"
);
let sources_rust_bridge = package_root.join("Sources").join("RustBridge");
let sources_rust_bridge_c = package_root.join("Sources").join("RustBridgeC");
let _ = crate_name; let files = vec![
GeneratedFile {
path: sources_rust_bridge.join("SwiftBridgeCore.swift"),
content: core_swift_content,
generated_header: false,
},
GeneratedFile {
path: sources_rust_bridge.join(format!("{binding_crate_name}.swift")),
content: crate_swift_content,
generated_header: false,
},
GeneratedFile {
path: sources_rust_bridge_c.join("RustBridgeC.h"),
content: rust_bridge_c_h,
generated_header: false,
},
];
Ok(Some(files))
}
fn emit_inbound_protocols(
api: &ApiSurface,
config: &ResolvedCrateConfig,
exclude_types: &std::collections::HashSet<String>,
out: &mut String,
) {
for bridge_cfg in &config.trait_bridges {
if bridge_cfg.bind_via != BridgeBinding::OptionsField {
continue;
}
if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
continue;
}
let trait_name = &bridge_cfg.trait_name;
let type_alias = match bridge_cfg.type_alias.as_deref() {
Some(a) => a,
None => continue,
};
let Some(options_type) = bridge_cfg.options_type.as_deref() else {
continue;
};
let Some(field) = bridge_cfg.resolved_options_field() else {
continue;
};
let result_type_name = bridge_cfg.result_type.as_deref();
let protocol_return_type = result_type_name.unwrap_or("Void");
let Some(trait_def) = api.types.iter().find(|t| t.is_trait && t.name == *trait_name) else {
continue;
};
let result_enum = result_type_name.and_then(|name| api.enums.iter().find(|e| e.name == name));
let box_name = format!("Swift{trait_name}Box");
let adapter_name = format!("_{trait_name}ProtocolAdapter");
let protocol_name = format!("{trait_name}Protocol");
let delegate_protocol_name = format!("_Swift{trait_name}BoxDelegate");
let factory_fn = format!(
"make{}{}",
trait_name.to_upper_camel_case(),
type_alias.to_upper_camel_case()
);
out.push_str(&format!(
"/// Swift protocol that Swift classes implement to provide visitor callbacks.\n\
/// Conform to this protocol to intercept configured Rust trait bridge events.\n\
public protocol {protocol_name}: AnyObject {{\n"
));
for method in &trait_def.methods {
let method_snake = method.name.to_snake_case();
let method_camel = method_snake.to_lower_camel_case();
let params = swift_protocol_params(method, exclude_types);
out.push_str(&format!(
" func {method_camel}({params}) -> {protocol_return_type}\n"
));
}
out.push_str("}\n\n");
let default_case = result_enum
.and_then(|en| en.variants.iter().find(|v| v.fields.is_empty()))
.map(|v| swift_case_ident(&v.name.to_lower_camel_case()));
if let Some(default_case) = &default_case {
out.push_str(&format!(
"/// Default implementation: every method returns `.{default_case}` so conforming\n\
/// types only need to implement the callbacks they care about.\n\
public extension {protocol_name} {{\n"
));
} else {
out.push_str(&format!(
"/// Default implementation: conforming types only need to implement\n\
/// the callbacks they care about.\n\
public extension {protocol_name} {{\n"
));
}
for method in &trait_def.methods {
let method_snake = method.name.to_snake_case();
let method_camel = method_snake.to_lower_camel_case();
let underscore_params = swift_protocol_underscore_params(method, exclude_types);
if let Some(default_case) = &default_case {
out.push_str(&format!(
" func {method_camel}({underscore_params}) -> {protocol_return_type} {{ return .{default_case} }}\n"
));
} else {
out.push_str(&format!(" func {method_camel}({underscore_params}) {{}}\n"));
}
}
out.push_str("}\n\n");
out.push_str(&format!(
"/// Internal adapter: wraps a `{protocol_name}` conformer as a `{delegate_protocol_name}`.\n\
/// Converts swift-bridge raw types (RustString, UInt, etc.) to user-friendly Swift\n\
/// types before dispatching, then serialises configured return values to JSON.\n\
private final class {adapter_name}: {delegate_protocol_name} {{\n\
\x20 private let inner: any {protocol_name}\n\
\x20 init(_ inner: any {protocol_name}) {{ self.inner = inner }}\n"
));
for method in &trait_def.methods {
let method_snake = method.name.to_snake_case();
let method_camel = method_snake.to_lower_camel_case();
let delegate_method = swift_ident(&method_camel);
let delegate_params = swift_box_params(method); let (conversion_lines, call_args) = swift_adapter_conversions(method, exclude_types);
out.push_str(&format!(" func {delegate_method}({delegate_params}) -> String {{\n"));
for line in &conversion_lines {
out.push_str(&format!(" {line}\n"));
}
let result_json = if let Some(result_type_name) = result_type_name.filter(|_| result_enum.is_some()) {
format!(
" return {}_toJson(inner.{method_camel}({call_args}))\n",
result_type_name.to_snake_case()
)
} else {
let call = if call_args.is_empty() {
format!("inner.{method_camel}()")
} else {
format!("inner.{method_camel}({call_args})")
};
out.push_str(&format!(" {call}\n"));
" return \"{}\"\n".to_string()
};
out.push_str(&result_json);
out.push_str(" }\n");
}
out.push_str("}\n\n");
if let Some(en) = result_enum {
let result_type_name = en.name.as_str();
let fn_name = format!("{}_toJson", result_type_name.to_snake_case());
out.push_str(&format!(
"/// Serialise a `{result_type_name}` to a JSON string matching Rust serde defaults.\n\
private func {fn_name}(_ result: {result_type_name}) -> String {{\n\
\x20 switch result {{\n"
));
for variant in &en.variants {
let variant_name = &variant.name;
let swift_case = swift_case_ident(&variant_name.to_lower_camel_case());
if variant.fields.is_empty() {
out.push_str(&format!(" case .{swift_case}: return \"\\\"{}\\\"\"", variant_name));
} else if variant.is_tuple && variant.fields.len() == 1 {
let _field_swift = if variant.fields[0].name.starts_with("field") {
"field0".to_string()
} else {
variant.fields[0].name.to_lower_camel_case()
};
out.push_str(&format!(
" case .{swift_case}(let v): return \"{{\\\"{}\\\":\\\"\\(jsonEscapeStr(v))\\\"}}\"",
variant_name
));
}
out.push('\n');
}
out.push_str(" }\n}\n\n");
out.push_str(
"/// Escape a Swift String for embedding in a JSON string literal.\n\
private func jsonEscapeStr(_ s: String) -> String {\n\
\x20 s.replacingOccurrences(of: \"\\\\\", with: \"\\\\\\\\\")\n\
\x20 .replacingOccurrences(of: \"\\\"\", with: \"\\\\\\\"\")\n\
\x20 .replacingOccurrences(of: \"\\n\", with: \"\\\\n\")\n\
\x20 .replacingOccurrences(of: \"\\r\", with: \"\\\\r\")\n\
\x20 .replacingOccurrences(of: \"\\t\", with: \"\\\\t\")\n\
}\n\n",
);
}
let opts_snake = options_type.to_snake_case();
let options_fn = format!("{opts_snake}FromJsonWith{}", field.to_upper_camel_case()).to_lower_camel_case();
out.push_str(&format!(
"/// Wrap a `{protocol_name}` conformer in an opaque `{type_alias}` handle\n\
/// that can be passed to `{options_fn}(...)` on the Rust side.\n\
public func {factory_fn}(_ visitor: any {protocol_name}) -> {type_alias} {{\n\
\x20 return RustBridge.{factory_fn}({box_name}({adapter_name}(visitor)))\n\
}}\n\n",
));
out.push_str(&format!(
"/// Decode `{options_type}` JSON and attach a `{type_alias}` visitor handle.\n\
/// Forwards to the swift-bridge shim emitted in the `RustBridge` module —\n\
/// re-exposed here so callers do not need `import RustBridge`.\n\
public func {options_fn}(_ json: String, _ {field}: {type_alias}?) throws -> {options_type} {{\n\
\x20 return try RustBridge.{options_fn}(json, {field})\n\
}}\n\n",
));
let _ = api;
}
}
fn already_emitted_top_level_names(api: &ApiSurface) -> std::collections::HashSet<String> {
let mut names: std::collections::HashSet<String> = std::collections::HashSet::new();
for func in &api.functions {
if func.is_async {
continue;
}
let first = func.params.first().map(|p| &p.ty);
let is_bytes_or_path = matches!(first, Some(TypeRef::Bytes) | Some(TypeRef::Path));
if !is_bytes_or_path {
continue;
}
if convenience_name_shadows_bridge(func) {
continue;
}
let swift_inner = swift_ident(&func.name.to_lower_camel_case());
let wrapper_name = if swift_inner.ends_with("Sync") {
swift_inner[..swift_inner.len() - 4].to_string()
} else {
swift_inner
};
names.insert(wrapper_name);
}
names
}
fn emit_free_function_forwarders(
api: &ApiSurface,
config: &ResolvedCrateConfig,
known_dto_names: &std::collections::HashSet<String>,
client_class_names: &std::collections::HashSet<String>,
out: &mut String,
) {
let mut exclude_functions: std::collections::HashSet<String> = config
.swift
.as_ref()
.map(|c| c.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
for contract in &api.handler_contracts {
if let Some(adapter) = contract.response_adapter.as_deref() {
if let Some(short) = adapter.rsplit("::").next() {
exclude_functions.insert(short.to_string());
}
}
}
let already = already_emitted_top_level_names(api);
let mut emitted_any = false;
for func in &api.functions {
if func.binding_excluded {
continue;
}
if exclude_functions.contains(&func.name) {
continue;
}
if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&func.name, &config.trait_bridges) {
continue;
}
let swift_name = swift_ident(&func.name.to_lower_camel_case());
if already.contains(&swift_name) {
continue;
}
if !emitted_any {
out.push_str("// MARK: - Free-function Forwarders\n");
out.push_str(
"// Re-export every public free function on the source Rust crate as a\n\
// top-level `public func` on the host module so consumers do not need to\n\
// `import RustBridge` directly. Forwarders take Swift-native parameter\n\
// types and convert to the swift-bridge runtime types internally.\n\n",
);
emitted_any = true;
}
if func.is_async {
emit_async_free_function_forwarder(func, &swift_name, known_dto_names, out);
} else {
emit_single_free_function_forwarder(func, &swift_name, known_dto_names, client_class_names, out);
}
}
}
fn emit_single_free_function_forwarder(
func: &FunctionDef,
swift_name: &str,
known_dto_names: &std::collections::HashSet<String>,
client_class_names: &std::collections::HashSet<String>,
out: &mut String,
) {
let return_conversion_throws = return_value_conversion_throws(&func.return_type, known_dto_names);
let any_param_throws = func
.params
.iter()
.any(|p| param_conversion_throws(&p.ty, known_dto_names));
let throws_clause = if func.error_type.is_some() || return_conversion_throws || any_param_throws {
" throws"
} else {
""
};
let try_keyword = if func.error_type.is_some() { "try " } else { "" };
let return_ty = forwarder_return_type(&func.return_type);
let return_clause = if matches!(&func.return_type, TypeRef::Unit) {
String::new()
} else {
format!(" -> {return_ty}")
};
let mut sig_params: Vec<String> = Vec::with_capacity(func.params.len());
let mut conversion_lines: Vec<String> = Vec::new();
let mut call_args: Vec<String> = Vec::with_capacity(func.params.len());
for param in &func.params {
let swift_param_name = swift_ident(¶m.name.to_lower_camel_case());
let (swift_ty, local_expr) =
forwarder_param_signature(¶m.ty, &swift_param_name, param.optional, known_dto_names);
let param_default = if param.optional { " = nil" } else { "" };
sig_params.push(format!("{swift_param_name}: {swift_ty}{param_default}"));
if let Some(line) = local_expr.setup_line.clone() {
conversion_lines.push(line);
}
call_args.push(local_expr.arg_expr);
}
let sig = sig_params.join(", ");
let args = call_args.join(", ");
if !func.doc.is_empty() {
emit_doc_comment(&func.doc, "", out);
}
out.push_str(&format!(
"public func {swift_name}({sig}){throws_clause}{return_clause} {{\n"
));
for line in &conversion_lines {
out.push_str(&format!(" {line}\n"));
}
let return_suffix =
forwarder_return_conversion_suffix_with_throws(&func.return_type, known_dto_names, func.error_type.is_some());
let effective_try = if func.error_type.is_some() || return_conversion_throws {
"try "
} else {
""
};
let _ = try_keyword;
if func.error_type.is_some() && return_uses_json_bridge(&func.return_type) {
let decode_ty = forwarder_return_type(&func.return_type);
out.push_str(&format!(
" let _rb_json = try RustBridge.{swift_name}({args}).toString()\n"
));
out.push_str(" let _rb_data = _rb_json.data(using: .utf8) ?? Data()\n");
out.push_str(&format!(
" return try JSONDecoder().decode({decode_ty}.self, from: _rb_data)\n"
));
} else if non_throwing_optional_string_uses_json_bridge(&func.return_type, func.error_type.is_some()) {
let decode_ty = forwarder_return_type(&func.return_type);
out.push_str(&format!(
" let _rb_json = RustBridge.{swift_name}({args}).toString()\n"
));
out.push_str(" let _rb_data = _rb_json.data(using: .utf8) ?? Data()\n");
out.push_str(&format!(
" return (try? JSONDecoder().decode({decode_ty}.self, from: _rb_data)) ?? nil\n"
));
} else if matches!(&func.return_type, TypeRef::Named(n) if client_class_names.contains(n)) {
let bridge_call_try = if func.error_type.is_some() { "try " } else { "" };
let class_name = swift_type_name(&func.return_type);
out.push_str(&format!(
" let _rb = {bridge_call_try}RustBridge.{swift_name}({args})\n"
));
out.push_str(&format!(" return {class_name}(_rb)\n"));
} else if bare_named_dto_return(&func.return_type, known_dto_names) {
let bridge_call_try = if func.error_type.is_some() { "try " } else { "" };
let dto_name = swift_type_name(&func.return_type);
out.push_str(&format!(
" let _rb = {bridge_call_try}RustBridge.{swift_name}({args})\n"
));
out.push_str(&format!(" return try {dto_name}(_rb)\n"));
} else {
out.push_str(&format!(
" return {effective_try}RustBridge.{swift_name}({args}){return_suffix}\n"
));
}
out.push_str("}\n\n");
}
fn emit_async_free_function_forwarder(
func: &FunctionDef,
swift_name: &str,
known_dto_names: &std::collections::HashSet<String>,
out: &mut String,
) {
let return_conversion_throws = return_value_conversion_throws(&func.return_type, known_dto_names);
let any_param_throws = func
.params
.iter()
.any(|p| param_conversion_throws(&p.ty, known_dto_names));
let throws_clause = if func.error_type.is_some() || return_conversion_throws || any_param_throws {
" throws"
} else {
""
};
let return_ty = forwarder_return_type(&func.return_type);
let return_clause = if matches!(&func.return_type, TypeRef::Unit) {
String::new()
} else {
format!(" -> {return_ty}")
};
let mut sig_params: Vec<String> = Vec::with_capacity(func.params.len());
let mut conversion_lines: Vec<String> = Vec::new();
let mut call_args: Vec<String> = Vec::with_capacity(func.params.len());
for param in &func.params {
let swift_param_name = swift_ident(¶m.name.to_lower_camel_case());
let (swift_ty, local_expr) =
forwarder_param_signature(¶m.ty, &swift_param_name, param.optional, known_dto_names);
let param_default = if param.optional { " = nil" } else { "" };
sig_params.push(format!("{swift_param_name}: {swift_ty}{param_default}"));
if let Some(line) = local_expr.setup_line.clone() {
conversion_lines.push(line);
}
let arg_expr = if matches!(¶m.ty, TypeRef::String) && !param.optional {
format!("RustString({swift_param_name})")
} else {
local_expr.arg_expr
};
call_args.push(arg_expr);
}
let sig = sig_params.join(", ");
let args = call_args.join(", ");
if !func.doc.is_empty() {
emit_doc_comment(&func.doc, "", out);
}
out.push_str(&format!(
"public func {swift_name}({sig}) async{throws_clause}{return_clause} {{\n"
));
let effective_try = if func.error_type.is_some() || return_conversion_throws {
"try "
} else {
""
};
let (bridge_call, return_stmt) = match &func.return_type {
TypeRef::Named(name) if known_dto_names.contains(name) => {
let struct_name = swift_ident(name);
(
format!("try RustBridge.{swift_name}({args})"),
format!(" return try {struct_name}(_rb_obj)"),
)
}
_ if return_uses_json_bridge(&func.return_type) && func.error_type.is_some() => {
let decode_ty = forwarder_return_type(&func.return_type);
(
format!("try RustBridge.{swift_name}({args}).toString()"),
format!(
" let _rb_data = _rb_result.data(using: .utf8) ?? Data()\n return try JSONDecoder().decode({decode_ty}.self, from: _rb_data)"
),
)
}
_ => (
format!("try RustBridge.{swift_name}({args})"),
" return result".to_string(),
),
};
out.push_str(&format!(
" return {effective_try}await Task.detached(priority: .userInitiated) {{\n"
));
for line in &conversion_lines {
out.push_str(&format!(" {line}\n"));
}
if matches!(&func.return_type, TypeRef::Named(name) if known_dto_names.contains(name)) {
out.push_str(&format!(" let _rb_obj = {bridge_call}\n"));
out.push_str(&format!("{return_stmt}\n"));
} else if return_uses_json_bridge(&func.return_type) && func.error_type.is_some() {
out.push_str(&format!(" let _rb_result = {bridge_call}\n"));
out.push_str(&format!("{return_stmt}\n"));
} else if matches!(&func.return_type, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_))) {
out.push_str(&format!(" let result = {bridge_call}\n"));
if func.error_type.is_some() {
if let TypeRef::Vec(inner) = &func.return_type {
if let TypeRef::Named(name) = inner.as_ref() {
let struct_name = swift_ident(name);
out.push_str(" var items: [");
out.push_str(&forwarder_return_type(inner.as_ref()));
out.push_str("] = []\n");
out.push_str(" for ref in result {\n");
if known_dto_names.contains(name) {
out.push_str(&format!(" let item = try {struct_name}(ref)\n"));
} else {
out.push_str(&format!(
" var item = try RustBridge.{struct_name}(ptr: ref.ptr)\n"
));
out.push_str(" item.isOwned = false\n");
}
out.push_str(" items.append(item)\n");
out.push_str(" }\n");
out.push_str(" return items\n");
} else {
let suffix =
forwarder_return_conversion_suffix_with_throws(&func.return_type, known_dto_names, true);
out.push_str(&format!(" return result{suffix}\n"));
}
}
} else {
let suffix = forwarder_return_conversion_suffix_with_throws(&func.return_type, known_dto_names, false);
out.push_str(&format!(" return result{suffix}\n"));
}
} else {
out.push_str(&format!(" let result = {bridge_call}\n"));
out.push_str(&format!("{return_stmt}\n"));
}
out.push_str(" }.value\n}\n\n");
}
fn bare_named_dto_return(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
matches!(ty, TypeRef::Named(name) if known_dto_names.contains(name))
}
fn return_uses_json_bridge(ty: &TypeRef) -> bool {
match ty {
TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)),
TypeRef::Map(_, _) | TypeRef::Json => true,
TypeRef::Optional(inner) => return_uses_json_bridge(inner),
_ => false,
}
}
fn non_throwing_optional_string_uses_json_bridge(ty: &TypeRef, throws: bool) -> bool {
if throws {
return false;
}
matches!(
ty,
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Primitive(_))
)
}
struct ForwarderArg {
setup_line: Option<String>,
arg_expr: String,
}
fn forwarder_param_signature(
ty: &TypeRef,
swift_param_name: &str,
optional: bool,
known_dto_names: &std::collections::HashSet<String>,
) -> (String, ForwarderArg) {
let inner_ty = if optional {
if let TypeRef::Optional(inner) = ty {
inner.as_ref().clone()
} else {
ty.clone()
}
} else {
ty.clone()
};
let make_optional = |inner: &str| -> String {
if optional || matches!(ty, TypeRef::Optional(_)) {
format!("{inner}?")
} else {
inner.to_string()
}
};
match &inner_ty {
TypeRef::Bytes => {
let swift_ty = make_optional("[UInt8]");
let local = format!("_rb_{swift_param_name}");
let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
Some(format!(
"let {local} = {swift_param_name}.map {{ bytes -> RustVec<UInt8> in let v = RustVec<UInt8>(); for b in bytes {{ v.push(value: b) }}; return v }}"
))
} else {
Some(format!(
"let {local}: RustVec<UInt8> = {{ let v = RustVec<UInt8>(); for b in {swift_param_name} {{ v.push(value: b) }}; return v }}()"
))
};
(
swift_ty,
ForwarderArg {
setup_line: setup,
arg_expr: local,
},
)
}
TypeRef::Vec(elem) => match elem.as_ref() {
TypeRef::String => {
let swift_ty = make_optional("[String]");
let local = format!("_rb_{swift_param_name}");
let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
Some(format!(
"let {local} = {swift_param_name}.map {{ strs -> RustVec<RustString> in let v = RustVec<RustString>(); for s in strs {{ v.push(value: RustString(s)) }}; return v }}"
))
} else {
Some(format!(
"let {local}: RustVec<RustString> = {{ let v = RustVec<RustString>(); for s in {swift_param_name} {{ v.push(value: RustString(s)) }}; return v }}()"
))
};
(
swift_ty,
ForwarderArg {
setup_line: setup,
arg_expr: local,
},
)
}
TypeRef::Primitive(_) => {
let inner = swift_type_name(elem);
let swift_ty = make_optional(&format!("[{inner}]"));
let local = format!("_rb_{swift_param_name}");
let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
Some(format!(
"let {local} = {swift_param_name}.map {{ xs -> RustVec<{inner}> in let v = RustVec<{inner}>(); for x in xs {{ v.push(value: x) }}; return v }}"
))
} else {
Some(format!(
"let {local}: RustVec<{inner}> = {{ let v = RustVec<{inner}>(); for x in {swift_param_name} {{ v.push(value: x) }}; return v }}()"
))
};
(
swift_ty,
ForwarderArg {
setup_line: setup,
arg_expr: local,
},
)
}
TypeRef::Named(name) if known_dto_names.contains(name) => {
let swift_ty = make_optional(&format!("[{name}]"));
let local = format!("_rb_{swift_param_name}");
let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
Some(format!(
"let {local} = try {swift_param_name}.map {{ items -> RustVec<RustString> in let v = RustVec<RustString>(); for item in items {{ let data = try JSONEncoder().encode(item); let json = String(data: data, encoding: .utf8) ?? \"null\"; v.push(value: RustString(json)) }}; return v }}"
))
} else {
Some(format!(
"let {local}: RustVec<RustString> = try ({{ () throws -> RustVec<RustString> in let v = RustVec<RustString>(); for item in {swift_param_name} {{ let data = try JSONEncoder().encode(item); let json = String(data: data, encoding: .utf8) ?? \"null\"; v.push(value: RustString(json)) }}; return v }}())"
))
};
(
swift_ty,
ForwarderArg {
setup_line: setup,
arg_expr: local,
},
)
}
TypeRef::Named(name) => {
let swift_ty = make_optional(&format!("[{name}]"));
let local = format!("_rb_{swift_param_name}");
let inner = swift_type_name(elem);
let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
Some(format!(
"let {local} = {swift_param_name}.map {{ xs -> RustVec<{inner}> in let v = RustVec<{inner}>(); for x in xs {{ v.push(value: x) }}; return v }}"
))
} else {
Some(format!(
"let {local}: RustVec<{inner}> = {{ let v = RustVec<{inner}>(); for x in {swift_param_name} {{ v.push(value: x) }}; return v }}()"
))
};
(
swift_ty,
ForwarderArg {
setup_line: setup,
arg_expr: local,
},
)
}
_ => {
let swift_ty = make_optional(&swift_type_name(ty));
(
swift_ty,
ForwarderArg {
setup_line: None,
arg_expr: swift_param_name.to_string(),
},
)
}
},
TypeRef::Named(name) if known_dto_names.contains(name) => {
let swift_ty = make_optional(&swift_type_name(&inner_ty));
let local = format!("_rb_{swift_param_name}");
let setup = if optional || matches!(ty, TypeRef::Optional(_)) {
Some(format!("let {local} = try {swift_param_name}?.intoRust()"))
} else {
Some(format!("let {local} = try {swift_param_name}.intoRust()"))
};
(
swift_ty,
ForwarderArg {
setup_line: setup,
arg_expr: local,
},
)
}
_ => {
let swift_ty = make_optional(&swift_type_name(&inner_ty));
(
swift_ty,
ForwarderArg {
setup_line: None,
arg_expr: swift_param_name.to_string(),
},
)
}
}
}
fn param_conversion_throws(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
match ty {
TypeRef::Named(name) => known_dto_names.contains(name),
TypeRef::Optional(inner) => matches!(
inner.as_ref(),
TypeRef::Named(name) if known_dto_names.contains(name)
),
TypeRef::Vec(elem) => matches!(
elem.as_ref(),
TypeRef::Named(name) if known_dto_names.contains(name)
),
_ => false,
}
}
fn forwarder_return_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "String".to_string(),
TypeRef::Bytes => "[UInt8]".to_string(),
TypeRef::Vec(inner) => format!("[{}]", forwarder_return_type(inner)),
TypeRef::Optional(inner) => format!("{}?", forwarder_return_type(inner)),
_ => swift_type_name(ty),
}
}
fn forwarder_return_conversion_suffix_with_throws(
ty: &TypeRef,
known_dto_names: &std::collections::HashSet<String>,
throws: bool,
) -> String {
forwarder_return_conversion_suffix_inner(ty, known_dto_names, throws)
}
fn forwarder_return_conversion_suffix_inner(
ty: &TypeRef,
known_dto_names: &std::collections::HashSet<String>,
throws: bool,
) -> String {
match ty {
TypeRef::String if throws => ".toString()".to_string(),
TypeRef::Bytes => ".map { $0 }".to_string(),
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::String => ".map { $0.as_str().toString() }".to_string(),
TypeRef::Primitive(_) => ".map { $0 }".to_string(),
TypeRef::Named(name) => {
let struct_name = swift_ident(name);
if known_dto_names.contains(name) {
format!(".map {{ ref in try {struct_name}(ref) }}")
} else {
format!(
".map {{ ref in var item = try RustBridge.{struct_name}(ptr: ref.ptr); item.isOwned = false; return item }}"
)
}
}
_ => String::new(),
},
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if known_dto_names.contains(name) => {
format!(".map {{ try {name}($0) }}")
}
TypeRef::String => String::new(),
_ => String::new(),
},
_ => String::new(),
}
}
fn return_value_conversion_throws(ty: &TypeRef, known_dto_names: &std::collections::HashSet<String>) -> bool {
match ty {
TypeRef::Optional(inner) => matches!(
inner.as_ref(),
TypeRef::Named(name) if known_dto_names.contains(name)
),
TypeRef::Named(name) => known_dto_names.contains(name),
_ => false,
}
}
fn emit_trait_bridge_forwarders(config: &ResolvedCrateConfig, out: &mut String) {
let mut emitted_any = false;
for bridge_cfg in &config.trait_bridges {
if bridge_cfg.bind_via != BridgeBinding::FunctionParam {
continue;
}
if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
continue;
}
if bridge_cfg.register_fn.is_none() && bridge_cfg.unregister_fn.is_none() && bridge_cfg.clear_fn.is_none() {
continue;
}
if !emitted_any {
out.push_str("// MARK: - Trait Bridge Registration Forwarders\n");
out.push_str(
"// Top-level `public func` re-exports of the swift-bridge–generated\n\
// `register_*` / `unregister_*` / `clear_*` plugin registration entry\n\
// points so consumers do not need to `import RustBridge` for plugin work.\n\n",
);
emitted_any = true;
}
let trait_name = &bridge_cfg.trait_name;
let box_type = format!("Swift{trait_name}Box");
if let Some(register_fn) = bridge_cfg.register_fn.as_deref() {
let camel = register_fn.to_lower_camel_case();
out.push_str(&format!(
"/// Register an inbound `{trait_name}` plugin implementation. The Swift\n\
/// host wraps a `{trait_name}` conformer in a `{box_type}` adapter\n\
/// (see `Sources/RustBridge/Plugins.swift`); pass the wrapped instance to\n\
/// register the plugin in the global registry.\n\
public func {camel}(_ swiftBox: {box_type}) throws {{\n\
\x20 try RustBridge.{camel}(swiftBox)\n\
}}\n\n"
));
}
if let Some(unregister_fn) = bridge_cfg.unregister_fn.as_deref() {
let camel = unregister_fn.to_lower_camel_case();
out.push_str(&format!(
"/// Unregister a previously-registered `{trait_name}` plugin by name.\n\
public func {camel}(_ name: String) throws {{\n\
\x20 try RustBridge.{camel}(name)\n\
}}\n\n"
));
}
if let Some(clear_fn) = bridge_cfg.clear_fn.as_deref() {
let camel = clear_fn.to_lower_camel_case();
out.push_str(&format!(
"/// Remove every registered `{trait_name}` plugin. Typically used in test teardown.\n\
public func {camel}() throws {{\n\
\x20 try RustBridge.{camel}()\n\
}}\n\n"
));
}
}
}
fn emit_inbound_box_files(
api: &ApiSurface,
config: &ResolvedCrateConfig,
rust_bridge_dir: &std::path::Path,
) -> Vec<GeneratedFile> {
let mut files = Vec::new();
for bridge_cfg in &config.trait_bridges {
if bridge_cfg.bind_via != BridgeBinding::OptionsField {
continue;
}
if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
continue;
}
let trait_name = &bridge_cfg.trait_name;
let Some(trait_def) = api.types.iter().find(|t| t.is_trait && t.name == *trait_name) else {
continue;
};
let box_name = format!("Swift{trait_name}Box");
let delegate_protocol_name = format!("_Swift{trait_name}BoxDelegate");
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n");
content.push_str("// swift-format-ignore-file\n");
content.push_str("// This file is in Sources/RustBridge/ because the swift-bridge @_cdecl\n");
content.push_str("// shims reference Swift");
content.push_str(trait_name);
content.push_str("Box by name and must see it in the same module.\n\n");
content.push_str("import RustBridgeC\n\n");
content.push_str(&format!(
"/// Delegate protocol for `{box_name}`.\n\
/// Conforming types convert raw FFI params (RustString etc.) to user-friendly\n\
/// Swift types and return a JSON-encoded result string.\n\
/// Implemented by the private adapter class in the `{trait_name}` module.\n\
///\n\
/// Leading `_` flags this as an internal binding surface — not part of the\n\
/// user-facing API. Must remain `public` because Swift requires public visibility\n\
/// for cross-module protocol conformance (implementer lives in the main target).\n\
public protocol {delegate_protocol_name}: AnyObject {{\n"
));
for method in &trait_def.methods {
let method_camel = swift_ident(&method.name.to_lower_camel_case());
let delegate_params = swift_box_params(method);
content.push_str(&format!(" func {method_camel}({delegate_params}) -> String\n"));
}
content.push_str("}\n\n");
content.push_str(&format!(
"/// Opaque box class retained by Rust via `Unmanaged<{box_name}>.passRetained`.\n\
/// Each `alef_*` method corresponds to an `extern \"Swift\"` declaration in the\n\
/// Rust bridge crate; swift-bridge generates @_cdecl shims that call these.\n\
/// Delegates to a `{delegate_protocol_name}` (implemented in the main module).\n\
public final class {box_name} {{\n\
\x20 private let delegate: any {delegate_protocol_name}\n\
\x20 public init(_ delegate: any {delegate_protocol_name}) {{ self.delegate = delegate }}\n"
));
for method in &trait_def.methods {
let method_snake = method.name.to_snake_case();
let shim_name = format!("alef_{method_snake}");
let box_params = swift_box_params_keyword(method);
let delegate_call_args = swift_box_delegate_call_args(method);
let method_camel = swift_ident(&method.name.to_lower_camel_case());
content.push_str(&format!(
" public func {shim_name}({box_params}) -> String {{\n\
\x20 return delegate.{method_camel}({delegate_call_args})\n\
\x20 }}\n"
));
}
content.push_str("}\n");
files.push(GeneratedFile {
path: rust_bridge_dir.join(format!("Swift{trait_name}Box.swift")),
content,
generated_header: false,
});
}
files
}
fn emit_function_param_box_files(
api: &ApiSurface,
config: &ResolvedCrateConfig,
rust_bridge_dir: &std::path::Path,
excluded_types: &std::collections::HashSet<String>,
) -> Vec<GeneratedFile> {
use crate::backends::swift::gen_bindings::plugin_marshal::{
swift_shim_param_decode, swift_shim_param_ffi_type, swift_shim_return_ffi_type, swift_shim_return_marshal,
};
let mut files = Vec::new();
let helpers_content = r#"// Generated by alef. Do not edit by hand.
// swift-format-ignore-file
import Foundation
import RustBridgeC
// MARK: - JSON Envelope
/// JSON envelope for Box method returns. Carries `Ok(T)` as `{"ok": <serialised T>}`
/// and `Err(String)` as `{"err": "<message>"}`.
///
/// Visibility is `internal` (default) — sibling Box files in the same `RustBridge` SwiftPM
/// target call these helpers. `private` would scope them file-local and break that.
enum InboundEnvelope<T: Encodable>: Encodable {
case ok(T)
case err(String)
enum CodingKeys: String, CodingKey { case ok, err }
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .ok(let value): try container.encode(value, forKey: .ok)
case .err(let message): try container.encode(message, forKey: .err)
}
}
}
/// Encode a successful `()` result as `{"ok":null}`.
func encodeOkVoidEnvelope() -> RustString {
return RustString("{\"ok\":null}")
}
/// Encode a successful `T: Encodable` result as `{"ok": <T>}`.
func encodeOkEnvelope<T: Encodable>(_ value: T) -> RustString {
do {
let payload = InboundEnvelope.ok(value)
let data = try JSONEncoder().encode(payload)
return RustString(
String(data: data, encoding: .utf8) ?? "{\"err\":\"swift: invalid utf8 in envelope\"}")
} catch {
return encodeErrEnvelope("swift: failed to encode ok envelope: \(error)")
}
}
/// Encode a failure as `{"err": "<message>"}`.
func encodeErrEnvelope(_ message: String) -> RustString {
let escaped = message.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(
of: "\"", with: "\\\"")
return RustString("{\"err\":\"\(escaped)\"}")
}
/// Decode a JSON-encoded payload into a `Decodable` type.
func decodeJson<T: Decodable>(_ json: String, as type: T.Type) throws -> T {
let data = json.data(using: .utf8) ?? Data()
return try JSONDecoder().decode(type, from: data)
}
"#;
files.push(GeneratedFile {
path: rust_bridge_dir.join("SwiftPluginHelpers.swift"),
content: helpers_content.to_string(),
generated_header: false,
});
for bridge_cfg in &config.trait_bridges {
if bridge_cfg.bind_via != BridgeBinding::FunctionParam {
continue;
}
if bridge_cfg.exclude_languages.iter().any(|l| l == "swift") {
continue;
}
let trait_name = &bridge_cfg.trait_name;
let Some(trait_def) = api.types.iter().find(|t| t.is_trait && t.name == *trait_name) else {
continue;
};
let box_name = format!("Swift{trait_name}Box");
let bridge_protocol_name = format!("Swift{trait_name}Bridge");
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n");
content.push_str("// swift-format-ignore-file\n");
content.push_str("// This file contains generated FFI glue for plugin Box classes.\n\n");
content.push_str("import Foundation\n");
content.push_str("import RustBridge\n\n");
content.push_str(&format!(
"/// Opaque box class retained by Rust via `Unmanaged<{box_name}>.passRetained`.\n\
/// Wraps `any {bridge_protocol_name}` and exposes `alef_*` FFI shim methods.\n\
/// swift-bridge @_cdecl shims call these methods directly.\n\
public final class {box_name} {{\n\
\x20 private let bridge: any {bridge_protocol_name}\n\
\x20 public init(_ bridge: any {bridge_protocol_name}) {{ self.bridge = bridge }}\n\n"
));
content.push_str(" // MARK: Plugin super-trait shims\n\n");
content.push_str(" public func alef_name() -> RustString {\n");
content.push_str(" return RustString(bridge.name)\n");
content.push_str(" }\n\n");
content.push_str(" public func alef_version() -> RustString {\n");
content.push_str(" return RustString(bridge.version())\n");
content.push_str(" }\n\n");
content.push_str(" public func alef_initialize() -> RustString {\n");
content.push_str(" do {\n");
content.push_str(" try bridge.initialize()\n");
content.push_str(" return encodeOkVoidEnvelope()\n");
content.push_str(" } catch { return encodeErrEnvelope(\"\\(error)\") }\n");
content.push_str(" }\n\n");
content.push_str(" public func alef_shutdown() -> RustString {\n");
content.push_str(" do {\n");
content.push_str(" try bridge.shutdown()\n");
content.push_str(" return encodeOkVoidEnvelope()\n");
content.push_str(" } catch { return encodeErrEnvelope(\"\\(error)\") }\n");
content.push_str(" }\n\n");
content.push_str(" // MARK: Trait-specific shims\n\n");
let bridge_exclude_types = trait_bridge::excluded_named_type_bridge_policy(trait_def, excluded_types);
for method in &trait_def.methods {
let method_snake = method.name.to_snake_case();
let shim_name = format!("alef_{method_snake}");
let method_camel = swift_ident(&method.name.to_lower_camel_case());
let mut param_sig = String::new();
for (idx, param) in method.params.iter().enumerate() {
if idx > 0 {
param_sig.push_str(", ");
}
let external = param.name.to_snake_case();
let internal = swift_ident(¶m.name.to_lower_camel_case());
let ffi_type = swift_shim_param_ffi_type(¶m.ty, param.optional);
if external == internal {
param_sig.push_str(&format!("{internal}: {ffi_type}"));
} else {
param_sig.push_str(&format!("{external} {internal}: {ffi_type}"));
}
}
let return_ffi_type = swift_shim_return_ffi_type(method);
content.push_str(&format!(
" public func {shim_name}({param_sig}) -> {return_ffi_type} {{\n"
));
let mut setup_lines = Vec::new();
let mut call_args = Vec::new();
for param in &method.params {
let param_camel = swift_ident(¶m.name.to_lower_camel_case());
let decode = swift_shim_param_decode(¶m_camel, ¶m.ty, param.optional, &bridge_exclude_types);
setup_lines.extend(decode.setup);
call_args.push(format!("{}: {}", param.name.to_lower_camel_case(), decode.expr));
}
let has_throwing_param = setup_lines.iter().any(|s| s.contains("JSONDecoder"));
let method_throws = method.error_type.is_some();
if has_throwing_param {
content.push_str(" do {\n");
for setup_line in &setup_lines {
content.push_str(&format!(" let {setup_line}\n"));
}
let call_args_str = call_args.join(", ");
let bridge_call = format!("bridge.{method_camel}({call_args_str})");
if method_throws {
match &method.return_type {
TypeRef::Unit => {
content.push_str(&format!(" try {bridge_call}\n"));
content.push_str(" return encodeOkVoidEnvelope()\n");
}
_ => {
content.push_str(&format!(" let result = try {bridge_call}\n"));
content.push_str(" return encodeOkEnvelope(result)\n");
}
}
} else {
let return_lines = swift_shim_return_marshal(method, &bridge_call);
for line in return_lines {
content.push_str(&format!(" {line}\n"));
}
}
content.push_str(" } catch { return encodeErrEnvelope(\"\\(error)\") }\n");
} else {
for setup_line in &setup_lines {
content.push_str(&format!(" {setup_line}\n"));
}
let call_args_str = call_args.join(", ");
let bridge_call = format!("bridge.{method_camel}({call_args_str})");
let return_lines = swift_shim_return_marshal(method, &bridge_call);
for line in return_lines {
content.push_str(&format!(" {line}\n"));
}
}
content.push_str(" }\n\n");
}
content.push_str("}\n");
files.push(GeneratedFile {
path: rust_bridge_dir.join(format!("Swift{trait_name}Box.swift")),
content,
generated_header: false,
});
}
files
}
fn swift_box_ffi_type(ty: &TypeRef, optional: bool) -> String {
use crate::core::ir::PrimitiveType;
let inner = match ty {
TypeRef::String | TypeRef::Named(_) | TypeRef::Path | TypeRef::Json | TypeRef::Map(_, _) => {
"RustString".to_string()
}
TypeRef::Optional(inner) => return format!("{}?", swift_box_ffi_type(inner, false)),
TypeRef::Vec(inner) => format!("RustVec<{}>", swift_box_ffi_type(inner, false)),
TypeRef::Primitive(PrimitiveType::Usize) | TypeRef::Primitive(PrimitiveType::Isize) => "UInt".to_string(),
TypeRef::Primitive(PrimitiveType::Bool) => "Bool".to_string(),
TypeRef::Primitive(PrimitiveType::U32) => "UInt32".to_string(),
TypeRef::Primitive(PrimitiveType::U64) => "UInt64".to_string(),
TypeRef::Primitive(PrimitiveType::I32) => "Int32".to_string(),
TypeRef::Primitive(PrimitiveType::I64) => "Int64".to_string(),
TypeRef::Primitive(PrimitiveType::F32) => "Float".to_string(),
TypeRef::Primitive(PrimitiveType::F64) => "Double".to_string(),
TypeRef::Primitive(PrimitiveType::U8) => "UInt8".to_string(),
TypeRef::Primitive(PrimitiveType::I8) => "Int8".to_string(),
TypeRef::Primitive(PrimitiveType::U16) => "UInt16".to_string(),
TypeRef::Primitive(PrimitiveType::I16) => "Int16".to_string(),
TypeRef::Bytes => "RustVec<UInt8>".to_string(),
TypeRef::Char => "Character".to_string(),
TypeRef::Duration => "Double".to_string(),
TypeRef::Unit => "Void".to_string(),
};
if optional { format!("{inner}?") } else { inner }
}
fn swift_box_params(method: &crate::core::ir::MethodDef) -> String {
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let name = swift_ident(&p.name.to_lower_camel_case());
let ty = swift_box_ffi_type(&p.ty, p.optional);
format!("_ {name}: {ty}")
})
.collect();
params.join(", ")
}
fn swift_box_params_keyword(method: &crate::core::ir::MethodDef) -> String {
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let external = p.name.to_snake_case();
let internal = swift_ident(&p.name.to_lower_camel_case());
let ty = swift_box_ffi_type(&p.ty, p.optional);
if external == internal {
format!("{internal}: {ty}")
} else {
format!("{external} {internal}: {ty}")
}
})
.collect();
params.join(", ")
}
fn swift_adapter_conversions(
method: &crate::core::ir::MethodDef,
exclude_types: &std::collections::HashSet<String>,
) -> (Vec<String>, String) {
use crate::core::ir::PrimitiveType;
let mut setup_lines: Vec<String> = Vec::new();
let call_args: Vec<String> = method
.params
.iter()
.map(|p| {
let snake = swift_ident(&p.name.to_lower_camel_case());
match &p.ty {
TypeRef::Named(name) if exclude_types.contains(name) => {
if p.optional {
format!("{snake}?.toString()")
} else {
format!("{snake}.toString()")
}
}
TypeRef::Named(name) => {
let from_json = format!("{name}FromJson").to_lower_camel_case();
let local = format!("{snake}Decoded");
if p.optional {
setup_lines.push(format!(
"let {local}: {name}? = {snake}.flatMap {{ try? {from_json}($0.toString()) }}"
));
} else {
setup_lines.push(format!(
"let {local}: {name} = (try? {from_json}({snake}.toString())) ?? (try! {from_json}(\"{{}}\"))"
));
}
local
}
TypeRef::String | TypeRef::Path | TypeRef::Json | TypeRef::Map(_, _) => {
if p.optional {
format!("{snake}?.toString()")
} else {
format!("{snake}.toString()")
}
}
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if exclude_types.contains(name) => {
format!("{snake}.flatMap {{ $0.toString() }}")
}
TypeRef::Named(name) => {
let from_json = format!("{name}FromJson").to_lower_camel_case();
let local = format!("{snake}Decoded");
setup_lines.push(format!(
"let {local}: {name}? = {snake}.flatMap {{ try? {from_json}($0.toString()) }}"
));
local
}
TypeRef::String | TypeRef::Path | TypeRef::Json | TypeRef::Map(_, _) => {
format!("{snake}?.toString()")
}
_ => snake,
},
TypeRef::Primitive(PrimitiveType::Usize) | TypeRef::Primitive(PrimitiveType::Isize) => {
format!("Int({snake})")
}
_ => snake,
}
})
.collect();
(setup_lines, call_args.join(", "))
}
fn swift_box_delegate_call_args(method: &crate::core::ir::MethodDef) -> String {
method
.params
.iter()
.map(|p| swift_ident(&p.name.to_lower_camel_case()))
.collect::<Vec<_>>()
.join(", ")
}
fn swift_protocol_params(
method: &crate::core::ir::MethodDef,
exclude_types: &std::collections::HashSet<String>,
) -> String {
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let name = p.name.to_lower_camel_case();
let ty = swift_inbound_type(&p.ty, p.optional, exclude_types);
format!("_ {name}: {ty}")
})
.collect();
params.join(", ")
}
fn swift_protocol_underscore_params(
method: &crate::core::ir::MethodDef,
exclude_types: &std::collections::HashSet<String>,
) -> String {
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let name = p.name.to_lower_camel_case();
let ty = swift_inbound_type(&p.ty, p.optional, exclude_types);
format!("_ _{name}: {ty}")
})
.collect();
params.join(", ")
}
#[allow(dead_code)]
fn swift_shim_call_args(method: &crate::core::ir::MethodDef) -> String {
method
.params
.iter()
.map(|p| swift_ident(&p.name.to_lower_camel_case()))
.collect::<Vec<_>>()
.join(", ")
}
fn swift_inbound_type(ty: &TypeRef, optional: bool, exclude_types: &std::collections::HashSet<String>) -> String {
use crate::core::ir::PrimitiveType;
let inner = match ty {
TypeRef::Named(name) if exclude_types.contains(name) => "String".to_string(),
TypeRef::Named(name) => name.clone(),
TypeRef::String => "String".to_string(),
TypeRef::Primitive(PrimitiveType::Bool) => "Bool".to_string(),
TypeRef::Primitive(PrimitiveType::U32) => "UInt32".to_string(),
TypeRef::Primitive(PrimitiveType::U64) => "UInt64".to_string(),
TypeRef::Primitive(PrimitiveType::I32) => "Int32".to_string(),
TypeRef::Primitive(PrimitiveType::I64) => "Int64".to_string(),
TypeRef::Primitive(PrimitiveType::Usize) => "Int".to_string(),
TypeRef::Primitive(PrimitiveType::Isize) => "Int".to_string(),
TypeRef::Primitive(PrimitiveType::F32) => "Float".to_string(),
TypeRef::Primitive(PrimitiveType::F64) => "Double".to_string(),
TypeRef::Primitive(PrimitiveType::U8) => "UInt8".to_string(),
TypeRef::Primitive(PrimitiveType::I8) => "Int8".to_string(),
TypeRef::Primitive(PrimitiveType::U16) => "UInt16".to_string(),
TypeRef::Primitive(PrimitiveType::I16) => "Int16".to_string(),
TypeRef::Vec(inner) => format!("RustVec<{}>", swift_box_ffi_type(inner, false)),
TypeRef::Optional(inner) => return format!("{}?", swift_inbound_type(inner, false, exclude_types)),
TypeRef::Unit => "Void".to_string(),
TypeRef::Bytes => "RustVec<UInt8>".to_string(),
TypeRef::Char => "Character".to_string(),
TypeRef::Path => "String".to_string(),
TypeRef::Json => "String".to_string(),
TypeRef::Duration => "Double".to_string(),
TypeRef::Map(_, _) => "String".to_string(),
};
if optional { format!("{inner}?") } else { inner }
}
fn append_rust_string_ref_to_string_extension(content: &str) -> String {
const MARKER: &str = "// alef: RustStringRef.toString() shim";
if let Some(idx) = content.find(MARKER) {
let mut head = content[..idx].to_string();
while head.ends_with('\n') {
head.pop();
}
head.push('\n');
head
} else {
content.to_string()
}
}
fn make_swift_bridge_ref_ptr_public(content: &str) -> String {
content
.replace(
" var ptr: UnsafeMutableRawPointer",
" public var ptr: UnsafeMutableRawPointer",
)
.replace(" var isOwned: Bool = true", " public var isOwned: Bool = true")
}
fn prepend_rust_bridge_c_import(content: &str) -> String {
const IMPORT: &str = "import RustBridgeC";
const IGNORE: &str = "// swift-format-ignore-file";
let head: Vec<&str> = content.lines().take(5).collect();
let has_import = head.iter().any(|l| l.trim() == IMPORT);
let has_ignore = head.iter().any(|l| l.trim() == IGNORE);
match (has_import, has_ignore) {
(true, true) => content.to_string(),
(true, false) => format!("{IGNORE}\n{content}"),
(false, true) => format!("{IMPORT}\n\n{content}"),
(false, false) => format!("{IGNORE}\n{IMPORT}\n\n{content}"),
}
}
fn is_untagged_enum_type(
ty: &crate::core::ir::TypeRef,
untagged_enum_names: &std::collections::HashSet<String>,
) -> bool {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(n) => untagged_enum_names.contains(n),
TypeRef::Optional(inner) => is_untagged_enum_type(inner, untagged_enum_names),
_ => false,
}
}
fn needs_json_bridge_for_swift(ty: &crate::core::ir::TypeRef) -> bool {
use crate::core::ir::TypeRef;
fn is_leaf(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Path
| TypeRef::Json
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Bytes
| TypeRef::Named(_),
)
}
match ty {
TypeRef::Map(_, _) => true,
TypeRef::Vec(inner) => !is_leaf(inner),
TypeRef::Optional(inner) => needs_json_bridge_for_swift(inner),
_ => false,
}
}
fn is_field_unbridgeable_for_init(
ty: &crate::core::ir::TypeDef,
field: &crate::core::ir::FieldDef,
exclude_fields: &std::collections::HashSet<String>,
known_dto_names: &std::collections::HashSet<String>,
) -> bool {
use crate::core::ir::TypeRef;
use heck::ToSnakeCase;
let name = field.name.to_snake_case();
let field_key = format!("{}.{}", ty.name, name);
if field.binding_excluded || exclude_fields.contains(&field_key) {
return true;
}
if let TypeRef::Vec(inner) = &field.ty
&& field.sanitized
&& !matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::Bytes)
{
return true;
}
if !ty.has_serde
&& let TypeRef::Vec(inner) = &field.ty
&& !matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::Bytes)
{
return true;
}
if needs_json_bridge_for_swift(&field.ty) {
let inner_named = match &field.ty {
TypeRef::Optional(inner) | TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(n) => Some(n.as_str()),
_ => None,
},
TypeRef::Named(n) => Some(n.as_str()),
_ => None,
};
if let Some(n) = inner_named
&& !known_dto_names.contains(n)
{
return true;
}
}
false
}
fn is_extension_param_bridgeable(ty: &TypeRef, api: &ApiSurface) -> bool {
match ty {
TypeRef::Named(n) if n.starts_with("Result") || n == "Result" => false,
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Path
| TypeRef::Bytes
| TypeRef::Duration
| TypeRef::Unit => true,
TypeRef::Named(n) => {
if let Some(enum_def) = api.enums.iter().find(|e| &e.name == n) {
enum_def.has_serde
} else {
true
}
}
TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_extension_param_bridgeable(inner, api),
TypeRef::Map(..) | TypeRef::Char | TypeRef::Json => false,
}
}
fn emit_ref_property_extensions(api: &ApiSurface) -> Option<(String, String)> {
let eligible_types: Vec<_> = api
.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque && !t.methods.is_empty())
.collect();
if eligible_types.is_empty() {
return None;
}
let mut content = String::new();
content.push_str("import RustBridge\n\n");
content.push_str("// MARK: - Property-access ergonomics for e2e tests\n");
content.push_str("//\n");
content.push_str("// This file provides computed-property aliases for methods on swift-bridge-generated types,\n");
content.push_str("// allowing callers to write `result.mimeType` rather than `result.mimeType()`.\n");
content.push_str("// These extensions are especially useful in e2e test assertions where the alef\n");
content.push_str("// fixture generator emits property-access syntax.\n");
content.push_str("//\n");
content.push_str("// Although these are primarily for test convenience, they are part of the public API\n");
content.push_str("// and can be used in production code for more ergonomic access to generated ref types.\n");
let mut has_any_extensions = false;
for ty in eligible_types {
let mut type_has_extensions = false;
let mut type_content = String::new();
for method in &ty.methods {
if method.is_async || method.is_static {
continue;
}
if method.binding_excluded {
continue;
}
let is_string_return = matches!(&method.return_type, TypeRef::String);
if !is_string_return {
continue;
}
if method.params.is_empty() {
continue;
}
if !method.params.iter().all(|p| is_extension_param_bridgeable(&p.ty, api)) {
continue;
}
if !type_has_extensions {
type_content.push('\n');
type_content.push_str(&format!("extension RustBridge.{}Ref {{\n", ty.name));
type_has_extensions = true;
} else {
type_content.push('\n');
}
let camel = method.name.to_lower_camel_case();
type_content.push_str(&format!(" /// Computed-property alias for `{}()` method.\n", camel));
type_content.push_str(&format!(" public var {}: String {{\n", camel));
type_content.push_str(&format!(" self.{}()\n", camel));
type_content.push_str(" }\n");
}
if type_has_extensions {
type_content.push_str("}\n");
type_content.push_str(&format!(
"\n// {}RefMut and {} inherit the extensions automatically\n",
ty.name, ty.name
));
content.push_str(&type_content);
has_any_extensions = true;
}
}
if has_any_extensions {
Some(("RustBridgeRefExtensions.swift".to_string(), content))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn prepend_adds_ignore_and_import_for_fresh_content() {
let bridge_out = "import Foundation\n\npublic class Foo {}\n";
let result = prepend_rust_bridge_c_import(bridge_out);
assert!(
result.starts_with("// swift-format-ignore-file\nimport RustBridgeC\n\n"),
"expected ignore directive then RustBridgeC import as the file prologue, got: {result:?}",
);
assert!(result.contains("public class Foo {}"));
}
#[test]
fn prepend_is_idempotent_when_both_present() {
let already_prepared =
"// swift-format-ignore-file\nimport RustBridgeC\n\nimport Foundation\n\npublic class Foo {}\n";
let result = prepend_rust_bridge_c_import(already_prepared);
assert_eq!(result, already_prepared, "second pass must not duplicate directives");
}
#[test]
fn prepend_adds_only_ignore_when_import_present() {
let import_only = "import RustBridgeC\n\nimport Foundation\n\npublic class Foo {}\n";
let result = prepend_rust_bridge_c_import(import_only);
assert_eq!(
result, "// swift-format-ignore-file\nimport RustBridgeC\n\nimport Foundation\n\npublic class Foo {}\n",
"missing ignore directive must be prepended without duplicating the import line",
);
assert_eq!(
result.matches("import RustBridgeC").count(),
1,
"import RustBridgeC must appear exactly once",
);
}
#[test]
fn prepend_adds_only_import_when_ignore_present() {
let ignore_only = "// swift-format-ignore-file\nimport Foundation\n\npublic class Foo {}\n";
let result = prepend_rust_bridge_c_import(ignore_only);
assert!(
result.starts_with("import RustBridgeC\n\n// swift-format-ignore-file\n"),
"expected import prepended ahead of pre-existing ignore directive, got: {result:?}",
);
assert_eq!(
result.matches("// swift-format-ignore-file").count(),
1,
"ignore directive must appear exactly once",
);
}
#[test]
fn append_passes_through_when_no_marker() {
let core_swift = "import Foundation\n\npublic class RustString {}\n";
let result = append_rust_string_ref_to_string_extension(core_swift);
assert_eq!(result, core_swift, "no-op when the marker is absent");
}
#[test]
fn append_strips_legacy_shim_when_present() {
let core_swift = "import Foundation\n\npublic class RustString {}\n\n\
// alef: RustStringRef.toString() shim\n\
extension RustStringRef {\n public func toString() -> String { self.as_str().toString() }\n}\n";
let result = append_rust_string_ref_to_string_extension(core_swift);
assert!(
!result.contains("// alef: RustStringRef.toString() shim"),
"expected the legacy shim to be stripped, got: {result:?}",
);
assert!(
result.contains("public class RustString"),
"non-shim content must be preserved verbatim",
);
}
#[test]
fn optional_named_dto_return_emits_throwing_map_conversion() {
let mut known: std::collections::HashSet<String> = Default::default();
known.insert("EmbeddingPreset".to_string());
let ty = TypeRef::Optional(Box::new(TypeRef::Named("EmbeddingPreset".to_string())));
let suffix = forwarder_return_conversion_suffix_with_throws(&ty, &known, false);
assert_eq!(suffix, ".map { try EmbeddingPreset($0) }");
assert!(
return_value_conversion_throws(&ty, &known),
"Optional<Named DTO> conversion must report as throwing so the outer forwarder is `throws`",
);
}
#[test]
fn optional_non_dto_named_return_passes_through_unchanged() {
let known: std::collections::HashSet<String> = Default::default();
let ty = TypeRef::Optional(Box::new(TypeRef::Named("OpaqueHandle".to_string())));
let suffix = forwarder_return_conversion_suffix_with_throws(&ty, &known, false);
assert!(suffix.is_empty(), "non-DTO Named return must not emit any suffix");
assert!(!return_value_conversion_throws(&ty, &known));
}
#[test]
fn inbound_protocol_maps_excluded_named_params_to_json_string() {
let mut excluded = std::collections::HashSet::new();
excluded.insert("PrivatePayload".to_string());
let method = MethodDef {
name: "process".to_string(),
params: vec![crate::core::ir::ParamDef {
name: "payload".to_string(),
ty: TypeRef::Named("PrivatePayload".to_string()),
optional: false,
is_mut: true,
..Default::default()
}],
return_type: TypeRef::Unit,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
assert_eq!(
swift_protocol_params(&method, &excluded),
"_ payload: String",
"public inbound protocols must not expose excluded named types",
);
let (setup, args) = swift_adapter_conversions(&method, &excluded);
assert!(
setup.is_empty(),
"excluded JSON strings must not route through missing fromJson helpers"
);
assert_eq!(args, "payload.toString()");
}
#[test]
fn function_param_box_uses_named_type_bridge_policy_for_shims() {
use crate::core::config::new_config::NewAlefConfig;
let method = MethodDef {
name: "process".to_string(),
params: vec![crate::core::ir::ParamDef {
name: "payload".to_string(),
ty: TypeRef::Named("PrivatePayload".to_string()),
..Default::default()
}],
return_type: TypeRef::String,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let trait_def = TypeDef {
name: "Processor".to_string(),
rust_path: "demo::Processor".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![method],
is_opaque: false,
is_clone: true,
is_copy: false,
doc: String::new(),
cfg: None,
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![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
};
let api = ApiSurface {
crate_name: "demo".to_string(),
version: "0.1.0".to_string(),
types: vec![trait_def],
..Default::default()
};
let toml = r#"
[workspace]
languages = ["swift"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
[[crates.trait_bridges]]
trait_name = "Processor"
bind_via = "function_param"
register_fn = "register_processor"
"#;
let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
let config = cfg.resolve().expect("test config must resolve").remove(0);
let files = emit_function_param_box_files(
&api,
&config,
std::path::Path::new("/tmp/RustBridge"),
&std::collections::HashSet::new(),
);
let box_file = files
.iter()
.find(|f| f.path.ends_with("SwiftProcessorBox.swift"))
.expect("SwiftProcessorBox.swift must be generated");
assert!(
box_file
.content
.contains("public func alef_process(payload: RustString) -> RustString"),
"named payload should stay a RustString at the shim boundary:\n{}",
box_file.content
);
assert!(
box_file.content.contains("bridge.process(payload: payload.toString())"),
"named payload discovered by bridge policy should pass through as a JSON string:\n{}",
box_file.content
);
assert!(
!box_file.content.contains("JSONDecoder().decode(PrivatePayload.self"),
"generic bridge-policy named types must not be decoded in plugin shims:\n{}",
box_file.content
);
}
#[test]
fn emit_ref_property_extensions_returns_none_for_empty_api() {
let api = ApiSurface::default();
let result = emit_ref_property_extensions(&api);
assert!(result.is_none(), "should not generate extensions when API is empty");
}
#[test]
fn emit_swift_bridge_files_preserves_populated_header_when_no_cargo_output() {
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("failed to create temp directory");
let package_root = temp_dir.path();
let sources_rust_bridge_c = package_root.join("Sources").join("RustBridgeC");
std::fs::create_dir_all(&sources_rust_bridge_c).expect("failed to create RustBridgeC directory");
let populated_header = "#ifndef RUST_BRIDGE_C_H\n\
#define RUST_BRIDGE_C_H\n\
\n\
// Auto-generated by alef — do not edit by hand.\n\
// Concatenates SwiftBridgeCore.h and binding.h produced by\n\
// `cargo build -p binding_crate` via swift_bridge_build.\n\
\n\
typedef struct RustStr { } RustStr;\n\
#endif /* RUST_BRIDGE_C_H */\n";
let header_path = sources_rust_bridge_c.join("RustBridgeC.h");
std::fs::write(&header_path, populated_header).expect("failed to write populated header");
let result = emit_swift_bridge_files("test-crate", "test_binding", package_root)
.expect("emit_swift_bridge_files failed");
assert!(
result.is_none(),
"emit_swift_bridge_files should return Ok(None) to preserve populated header"
);
let on_disk = std::fs::read_to_string(&header_path).expect("failed to read header after emit");
assert_eq!(
on_disk, populated_header,
"populated header must not be overwritten when preserve check succeeds"
);
}
}