use crate::codegen::keywords::{swift_case_ident, swift_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, ToKebabCase, 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 service_api;
pub mod trait_bridge;
pub struct SwiftBackend;
fn effective_exclude_types(config: &ResolvedCrateConfig) -> 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
}
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);
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_from_json_forwarders(
api,
&exclude_types,
&mapper,
&exclude_fields,
&known_dto_names,
&mut body,
);
emit_inbound_protocols(api, config, &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);
{
let handle_returned =
crate::backends::swift::gen_rust_crate::type_bridge::compute_handle_returned_types(api);
for ty in api.types.iter().filter(|t| {
!t.is_trait && !exclude_types.contains(&t.name) && (t.is_opaque || handle_returned.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);
}
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 = module_dir.join(&filename);
files.push(GeneratedFile {
path,
content,
generated_header: false,
});
}
let service_files = service_api::generate(api, config)?;
files.extend(service_files);
Ok(files)
}
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
};
format!(
"try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
(rb.{rust_accessor}().toString().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
};
format!(
"try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
(rb.{rust_accessor}().toString().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 = if let Some(rename) = &variant.serde_rename {
rename.clone()
} else {
match en.serde_rename_all.as_deref() {
Some("camelCase") => variant.name.to_lower_camel_case(),
Some("snake_case") => variant.name.to_snake_case(),
Some("SCREAMING_SNAKE_CASE") => variant.name.to_snake_case().to_uppercase(),
Some("PascalCase") => variant.name.to_upper_camel_case(),
Some("kebab-case") => variant.name.to_kebab_case(),
Some("lowercase") => variant.name.to_lowercase(),
Some("UPPERCASE") => variant.name.to_uppercase(),
_ => variant.name.clone(),
}
};
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 = if let Some(rename) = &variant.serde_rename {
rename.clone()
} else {
match en.serde_rename_all.as_deref() {
Some("camelCase") => variant.name.to_lower_camel_case(),
Some("snake_case") => variant.name.to_snake_case(),
Some("SCREAMING_SNAKE_CASE") => variant.name.to_snake_case().to_uppercase(),
Some("PascalCase") => variant.name.to_upper_camel_case(),
Some("kebab-case") => variant.name.to_kebab_case(),
Some("lowercase") => variant.name.to_lowercase(),
Some("UPPERCASE") => variant.name.to_uppercase(),
_ => variant.name.clone(),
}
};
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_raw_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_raw_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_raw_value(variant: &crate::core::ir::EnumVariant, rename_all: Option<&str>) -> String {
use heck::{ToKebabCase, ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
if let Some(rename) = &variant.serde_rename {
return rename.clone();
}
match rename_all {
Some("snake_case") => variant.name.to_snake_case(),
Some("camelCase") => variant.name.to_lower_camel_case(),
Some("SCREAMING_SNAKE_CASE") | Some("SCREAMING-KEBAB-CASE") => variant.name.to_snake_case().to_uppercase(),
Some("PascalCase") => variant.name.to_upper_camel_case(),
Some("kebab-case") | Some("train-case") => variant.name.to_kebab_case(),
Some("lowercase") => variant.name.to_lowercase(),
Some("UPPERCASE") => variant.name.to_uppercase(),
_ => variant.name.clone(),
}
}
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_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");
let needs_upgrade = header_path
.exists()
.then(|| std::fs::read_to_string(&header_path).ok())
.flatten()
.map(|content| !content.contains("__private__FfiSlice"))
.unwrap_or(false);
if needs_upgrade {
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,
}]));
}
return Ok(None);
}
};
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 = append_rust_string_ref_to_string_extension(&prepend_rust_bridge_c_import(&core_swift));
let crate_swift_content = 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, 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 result_type_name = bridge_cfg.result_type.as_deref().unwrap_or("VisitResult");
let Some(trait_def) = api.types.iter().find(|t| t.is_trait && t.name == *trait_name) else {
continue;
};
let result_enum = api.enums.iter().find(|e| e.name == result_type_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{}Handle", trait_name.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 HTML\u{2192}Markdown conversion 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);
out.push_str(&format!(" func {method_camel}({params}) -> {result_type_name}\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()))
.unwrap_or_else(|| swift_case_ident("continue"));
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"
));
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);
out.push_str(&format!(
" func {method_camel}({underscore_params}) -> {result_type_name} {{ return .{default_case} }}\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, and serialises the `{result_type_name}` return 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);
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(_en) = result_enum {
format!(
" return {}_toJson(inner.{method_camel}({call_args}))\n",
result_type_name.to_snake_case()
)
} else {
" 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 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 options_type = bridge_cfg.options_type.as_deref().unwrap_or("ConversionOptions");
let field = bridge_cfg.resolved_options_field().unwrap_or("visitor");
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.make{trait_name}Handle({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 exclude_functions: std::collections::HashSet<String> = config
.swift
.as_ref()
.map(|c| c.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
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);
sig_params.push(format!("{swift_param_name}: {swift_ty}"));
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);
sig_params.push(format!("{swift_param_name}: {swift_ty}"));
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"
));
for line in &conversion_lines {
out.push_str(&format!(" {line}\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)"),
)
}
_ => (
format!("try RustBridge.{swift_name}({args})"),
" return result".to_string(),
),
};
out.push_str(&format!(
" return {effective_try}await Task.detached(priority: .userInitiated) {{\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 {
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,
},
)
}
_ => {
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(_) => String::new(),
_ => 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 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) -> (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) => {
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) => {
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) -> 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);
format!("_ {name}: {ty}")
})
.collect();
params.join(", ")
}
fn swift_protocol_underscore_params(method: &crate::core::ir::MethodDef) -> 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);
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) -> String {
use crate::core::ir::PrimitiveType;
let inner = match ty {
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)),
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 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
}
#[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));
}
}