use crate::backends::swift::gen_bindings::trait_bridge::gen_trait_bridge_files;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, ParamDef, PrimitiveType, TypeDef, TypeRef};
use std::collections::HashSet;
fn param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
..Default::default()
}
}
fn method(name: &str, params: Vec<ParamDef>, return_type: TypeRef, error_type: Option<&str>) -> MethodDef {
MethodDef {
name: name.to_string(),
params,
return_type,
error_type: error_type.map(|name| name.to_string()),
..Default::default()
}
}
fn make_trait(name: &str, methods: Vec<MethodDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("testcrate::{name}"),
is_trait: true,
methods,
..Default::default()
}
}
fn bridge_source(trait_def: &TypeDef) -> String {
let bridge_cfg = TraitBridgeConfig {
trait_name: trait_def.name.clone(),
register_fn: Some(format!("register{}", trait_def.name)),
..Default::default()
};
let bridges = vec![(trait_def.name.clone(), &bridge_cfg, trait_def)];
let files = gen_trait_bridge_files(&bridges, &HashSet::new(), &HashSet::new());
let wanted = format!("Swift{}Bridge.swift", trait_def.name);
files
.into_iter()
.find(|(name, _)| *name == wanted)
.unwrap_or_else(|| panic!("expected {wanted} among generated trait bridge files"))
.1
}
#[test]
fn adapter_emits_a_call_method_for_every_protocol_method() {
let source = bridge_source(&make_trait(
"TextBackend",
vec![
method(
"find_all",
vec![param("text", TypeRef::String)],
TypeRef::Vec(Box::new(TypeRef::String)),
Some("BackendError"),
),
method("scan", vec![], TypeRef::Primitive(PrimitiveType::Bool), None),
],
));
assert!(
source.contains("func findAll(text: String) throws -> [String]"),
"protocol must declare findAll: {source}"
);
assert!(
source.contains("func findAllCall(text: String) throws -> String {"),
"adapter must register findAllCall: {source}"
);
assert!(
source.contains("func scan() -> Bool"),
"protocol must declare scan: {source}"
);
assert!(
source.contains("func scanCall() -> Bool {"),
"adapter must register scanCall: {source}"
);
}
#[test]
fn adapter_converts_string_return_to_native_string() {
let source = bridge_source(&make_trait(
"TextBackend",
vec![method("extract_text", vec![], TypeRef::String, Some("BackendError"))],
));
assert!(
source.contains("return marshal_ok_result(String(result))"),
"String returns must be wrapped via String(result): {source}"
);
}
#[test]
fn adapter_converts_vec_string_return_element_wise() {
let source = bridge_source(&make_trait(
"TextBackend",
vec![method(
"find_all",
vec![],
TypeRef::Vec(Box::new(TypeRef::String)),
Some("BackendError"),
)],
));
assert!(
source.contains("return marshal_ok_result(result.map { String($0) })"),
"Vec<String> returns must be converted element-wise: {source}"
);
}
fn trait_with_defaulted_dto_methods() -> TypeDef {
let mut page_layout = method("page_layout", vec![], TypeRef::Named("PageLayout".to_string()), None);
page_layout.has_default_impl = true;
let mut describe = method(
"describe",
vec![param("layout", TypeRef::Named("PageLayout".to_string()))],
TypeRef::String,
None,
);
describe.has_default_impl = true;
let mut is_ready = method("is_ready", vec![], TypeRef::Primitive(PrimitiveType::Bool), None);
is_ready.has_default_impl = true;
make_trait("DocumentSink", vec![page_layout, describe, is_ready])
}
#[test]
fn defaulted_method_named_types_cross_the_boundary_as_json_strings() {
let source = bridge_source(&trait_with_defaulted_dto_methods());
assert!(
source.contains("func pageLayout() -> String"),
"a defaulted method's enum return must be declared as a JSON String, got:\n{source}"
);
assert!(
source.contains("func describe(layout: String) -> String"),
"a defaulted method's enum parameter must be declared as a JSON String, got:\n{source}"
);
assert!(
!source.contains("PageLayout"),
"no DTO type name may appear in a file emitted into RustBridge, got:\n{source}"
);
}
#[test]
fn no_default_method_bodies_are_synthesized() {
let source = bridge_source(&trait_with_defaulted_dto_methods());
assert!(
!source.contains("public extension SwiftDocumentSinkBridge"),
"alef must not ship a default-implementation extension it cannot populate correctly, got:\n{source}"
);
for invented in ["return .", "return true", "return \"{}\"", "return \"\""] {
assert!(
!source.contains(invented),
"invented default body {invented:?} must not be emitted, got:\n{source}"
);
}
}
#[test]
fn defaulted_methods_are_annotated_with_why_no_stub_is_provided() {
let source = bridge_source(&trait_with_defaulted_dto_methods());
assert!(
source.contains("supply the same value the Rust default would have produced"),
"defaulted protocol methods must explain the missing stub, got:\n{source}"
);
}
#[test]
fn methods_without_a_rust_default_carry_no_default_note() {
let source = bridge_source(&make_trait(
"DocumentSink",
vec![method(
"accept",
vec![param("chunk", TypeRef::String)],
TypeRef::Unit,
None,
)],
));
assert!(
!source.contains("supply the same value the Rust default would have produced"),
"a method with no Rust default must not be annotated as defaulted, got:\n{source}"
);
}