use super::*;
use crate::core::ir::TypeRef;
#[test]
fn test_swift_type_name_bool_returns_bool() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::Bool)), "Bool");
}
#[test]
fn test_swift_type_name_usize_returns_uint() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::Usize)), "UInt");
}
#[test]
fn test_swift_type_name_u8_returns_uint8() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::U8)), "UInt8");
}
#[test]
fn test_swift_type_name_u32_returns_uint32() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::U32)), "UInt32");
}
#[test]
fn test_swift_type_name_u64_returns_uint64() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::U64)), "UInt64");
}
#[test]
fn test_swift_type_name_i32_returns_int32() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::I32)), "Int32");
}
#[test]
fn test_swift_type_name_f32_returns_float() {
assert_eq!(swift_type_name(&TypeRef::Primitive(PrimitiveType::F32)), "Float");
}
fn make_function(name: &str, params: Vec<(&str, TypeRef)>, return_type: TypeRef) -> FunctionDef {
FunctionDef {
name: name.to_string(),
rust_path: format!("sample::{name}"),
params: params
.into_iter()
.map(|(pname, ty)| crate::core::ir::ParamDef {
name: pname.to_string(),
ty,
..crate::core::ir::ParamDef::default()
})
.collect(),
return_type,
..FunctionDef::default()
}
}
#[test]
fn skips_forwarder_when_param_type_is_excluded() {
let func = make_function(
"extract_keywords",
vec![("config", TypeRef::Named("KeywordConfig".to_string()))],
TypeRef::Unit,
);
let mut exclude: HashSet<String> = HashSet::new();
exclude.insert("KeywordConfig".to_string());
assert!(function_references_excluded_type(&func, &exclude));
}
#[test]
fn skips_forwarder_when_return_type_is_excluded() {
let func = make_function(
"build_keyword",
vec![("text", TypeRef::String)],
TypeRef::Named("Keyword".to_string()),
);
let mut exclude: HashSet<String> = HashSet::new();
exclude.insert("Keyword".to_string());
assert!(function_references_excluded_type(&func, &exclude));
}
#[test]
fn keeps_forwarder_when_only_primitives_are_used() {
let func = make_function(
"echo_count",
vec![("count", TypeRef::Primitive(PrimitiveType::U32))],
TypeRef::Primitive(PrimitiveType::U32),
);
let mut exclude: HashSet<String> = HashSet::new();
exclude.insert("KeywordConfig".to_string());
exclude.insert("Keyword".to_string());
assert!(!function_references_excluded_type(&func, &exclude));
}
#[test]
fn skips_forwarder_when_vec_named_param_is_excluded() {
let func = make_function(
"score_keywords",
vec![(
"keywords",
TypeRef::Vec(Box::new(TypeRef::Named("Keyword".to_string()))),
)],
TypeRef::Unit,
);
let mut exclude: HashSet<String> = HashSet::new();
exclude.insert("Keyword".to_string());
assert!(function_references_excluded_type(&func, &exclude));
}
#[test]
fn skips_forwarder_when_optional_named_return_is_excluded() {
let func = make_function(
"maybe_yake",
vec![],
TypeRef::Optional(Box::new(TypeRef::Named("YakeParams".to_string()))),
);
let mut exclude: HashSet<String> = HashSet::new();
exclude.insert("YakeParams".to_string());
assert!(function_references_excluded_type(&func, &exclude));
}
#[test]
fn empty_exclude_set_keeps_every_function() {
let func = make_function(
"extract_keywords",
vec![("config", TypeRef::Named("KeywordConfig".to_string()))],
TypeRef::Named("Keyword".to_string()),
);
let exclude: HashSet<String> = HashSet::new();
assert!(!function_references_excluded_type(&func, &exclude));
}
#[test]
fn skips_forwarder_when_map_value_is_excluded_type() {
let func = make_function(
"score_map",
vec![(
"table",
TypeRef::Map(
Box::new(TypeRef::String),
Box::new(TypeRef::Named("Keyword".to_string())),
),
)],
TypeRef::Unit,
);
let mut exclude: HashSet<String> = HashSet::new();
exclude.insert("Keyword".to_string());
assert!(function_references_excluded_type(&func, &exclude));
}
fn make_capsule_fn() -> FunctionDef {
make_function(
"get_language",
vec![("name", TypeRef::String)],
TypeRef::Named("Language".to_string()),
)
}
#[test]
fn capsule_forwarder_emits_opaque_pointer_reconstruction() {
let func = make_capsule_fn();
let cfg = crate::core::config::HostCapsuleTypeConfig {
host_type: "MyLib.Language".to_string(),
package: String::new(),
package_version: String::new(),
construct_expr: "MyLib.Language({ptr})".to_string(),
..Default::default()
};
let mut out = String::new();
emit_capsule_free_function_forwarder(&func, "GetLanguage", &cfg, &mut out);
assert!(
out.contains("OpaquePointer(bitPattern:"),
"capsule forwarder must reconstruct OpaquePointer via bitPattern. Got:\n{out}"
);
assert!(
out.contains("addr != 0"),
"capsule forwarder must check for 0 sentinel. Got:\n{out}"
);
}
#[test]
fn capsule_forwarder_errors_when_construct_expr_empty() {
let func = make_capsule_fn();
let cfg = crate::core::config::HostCapsuleTypeConfig {
host_type: "MyLib.Language".to_string(),
package: String::new(),
package_version: String::new(),
construct_expr: String::new(),
..Default::default()
};
let mut out = String::new();
emit_capsule_free_function_forwarder(&func, "GetLanguage", &cfg, &mut out);
assert!(
out.contains("ALEF ERROR"),
"empty construct_expr must produce ALEF ERROR. Got:\n{out}"
);
assert!(
out.contains("construct_expr"),
"error must name the missing field. Got:\n{out}"
);
}
fn make_async_enum_return_fn() -> FunctionDef {
let mut func = make_function(
"refresh_catalog",
vec![("config", TypeRef::Named("CatalogRefreshConfig".to_string()))],
TypeRef::Named("RefreshOutcome".to_string()),
);
func.is_async = true;
func.error_type = Some("String".to_string());
func
}
#[test]
fn async_forwarder_decodes_unit_enum_return_via_raw_value_not_positional_init() {
let func = make_async_enum_return_fn();
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("RefreshOutcome".to_string());
let enum_names: HashSet<String> = known_dto_names.clone();
let unit_enum_names: HashSet<String> = known_dto_names.clone();
let mut out = String::new();
emit_async_free_function_forwarder(
&func,
"refreshCatalog",
&known_dto_names,
&enum_names,
&unit_enum_names,
"MyLibError",
&mut out,
);
assert!(
!out.contains("RefreshOutcome(_rb_obj)"),
"must not emit the struct-init pattern for an enum return. Got:\n{out}"
);
assert!(
out.contains("RefreshOutcome(rawValue:"),
"must decode the enum via its RawValue initializer. Got:\n{out}"
);
assert!(
out.contains("MyLibError.validation(message: \"Unknown RefreshOutcome variant\""),
"must throw a validation error naming the enum on an unrecognized raw value. Got:\n{out}"
);
}
fn make_sync_enum_return_fn() -> FunctionDef {
let mut func = make_function("current_outcome", vec![], TypeRef::Named("RefreshOutcome".to_string()));
func.error_type = Some("String".to_string());
func
}
#[test]
fn sync_forwarder_decodes_unit_enum_return_via_raw_value_not_positional_init() {
let func = make_sync_enum_return_fn();
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("RefreshOutcome".to_string());
let unit_enum_names: HashSet<String> = known_dto_names.clone();
let client_class_names: HashSet<String> = HashSet::new();
let mut out = String::new();
emit_single_free_function_forwarder(
&func,
"currentOutcome",
&known_dto_names,
&unit_enum_names,
"MyLibError",
&client_class_names,
&mut out,
);
assert!(
!out.contains("RefreshOutcome(_rb)"),
"must not emit the struct-init pattern for an enum return. Got:\n{out}"
);
assert!(
out.contains("RefreshOutcome(rawValue:"),
"must decode the enum via its RawValue initializer. Got:\n{out}"
);
assert!(
out.contains("MyLibError.validation(message: \"Unknown RefreshOutcome variant\""),
"must throw a validation error naming the enum on an unrecognized raw value. Got:\n{out}"
);
}
#[test]
fn string_and_path_params_produce_identical_forwarder_arg_shape() {
let known_dto_names: HashSet<String> = HashSet::new();
let (string_ty, string_arg) = forwarder_param_signature(&TypeRef::String, "path", false, &known_dto_names);
let (path_ty, path_arg) = forwarder_param_signature(&TypeRef::Path, "path", false, &known_dto_names);
assert_eq!(
string_ty, path_ty,
"String and Path params must produce the same Swift parameter type"
);
assert_eq!(
path_arg.setup_line, string_arg.setup_line,
"Path must wrap into RustString exactly like String, not fall through with setup_line: None"
);
assert_eq!(path_arg.arg_expr, string_arg.arg_expr);
assert_eq!(path_ty, "String");
assert_eq!(path_arg.setup_line, Some("let _rb_path = RustString(path)".to_string()));
assert_eq!(path_arg.arg_expr, "_rb_path");
}
#[test]
fn optional_path_param_wraps_like_optional_string() {
let known_dto_names: HashSet<String> = HashSet::new();
let ty = TypeRef::Optional(Box::new(TypeRef::Path));
let (swift_ty, arg) = forwarder_param_signature(&ty, "path", true, &known_dto_names);
assert_eq!(swift_ty, "String?");
assert_eq!(
arg.setup_line,
Some("let _rb_path = path.map { RustString($0) }".to_string())
);
assert_eq!(arg.arg_expr, "_rb_path");
}
#[test]
fn vec_dto_return_throws_when_element_type_is_known_dto() {
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("PatternMatch".to_string());
let ty = TypeRef::Vec(Box::new(TypeRef::Named("PatternMatch".to_string())));
assert!(return_value_conversion_throws(&ty, &known_dto_names));
}
#[test]
fn vec_dto_return_does_not_throw_when_element_type_is_not_a_known_dto() {
let known_dto_names: HashSet<String> = HashSet::new();
let ty = TypeRef::Vec(Box::new(TypeRef::Named("PatternMatch".to_string())));
assert!(!return_value_conversion_throws(&ty, &known_dto_names));
}
#[test]
fn bare_named_dto_return_throws_when_known() {
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("PatternMatch".to_string());
let ty = TypeRef::Named("PatternMatch".to_string());
assert!(return_value_conversion_throws(&ty, &known_dto_names));
}
#[test]
fn optional_named_dto_return_throws_when_known() {
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("PatternMatch".to_string());
let ty = TypeRef::Optional(Box::new(TypeRef::Named("PatternMatch".to_string())));
assert!(return_value_conversion_throws(&ty, &known_dto_names));
}
#[test]
fn vec_string_return_never_throws() {
let known_dto_names: HashSet<String> = HashSet::new();
let ty = TypeRef::Vec(Box::new(TypeRef::String));
assert!(!return_value_conversion_throws(&ty, &known_dto_names));
}
#[test]
fn vec_dto_return_suffix_maps_with_try_and_has_no_leading_try() {
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("PatternMatch".to_string());
let ty = TypeRef::Vec(Box::new(TypeRef::Named("PatternMatch".to_string())));
let suffix = forwarder_return_conversion_suffix_with_throws(&ty, &known_dto_names, true);
assert_eq!(
suffix, ".map { ref in try PatternMatch(ref) }",
"Vec<DTO> suffix must map each element with try inside the closure. Got:\n{suffix}"
);
assert!(
suffix.starts_with(".map"),
"suffix must start with .map. Got:\n{suffix}"
);
assert!(
!suffix.starts_with("try"),
"suffix must not start with a leading try. Got:\n{suffix}"
);
}
#[test]
fn vec_opaque_return_suffix_uses_ptr_based_reconstruction_and_has_no_leading_try() {
let known_dto_names: HashSet<String> = HashSet::new();
let ty = TypeRef::Vec(Box::new(TypeRef::Named("Thing".to_string())));
let suffix = forwarder_return_conversion_suffix_with_throws(&ty, &known_dto_names, true);
assert_eq!(
suffix, ".map { ref in var item = try RustBridge.Thing(ptr: ref.ptr); item.isOwned = false; return item }",
"Vec<opaque> suffix must reconstruct via ptr-based init. Got:\n{suffix}"
);
assert!(
!suffix.starts_with("try"),
"suffix must not start with a leading try. Got:\n{suffix}"
);
}
#[test]
fn capsule_forwarder_errors_when_host_type_empty() {
let func = make_capsule_fn();
let cfg = crate::core::config::HostCapsuleTypeConfig {
host_type: String::new(),
package: String::new(),
package_version: String::new(),
construct_expr: "MyLib.Language({ptr})".to_string(),
..Default::default()
};
let mut out = String::new();
emit_capsule_free_function_forwarder(&func, "GetLanguage", &cfg, &mut out);
assert!(
out.contains("ALEF ERROR"),
"empty host_type must produce ALEF ERROR. Got:\n{out}"
);
assert!(
out.contains("host_type"),
"error must name the missing field. Got:\n{out}"
);
}
fn make_async_enum_param_fn() -> FunctionDef {
let mut func = make_function(
"set_region",
vec![("region_kind", TypeRef::Named("RegionKind".to_string()))],
TypeRef::Unit,
);
func.is_async = true;
func.error_type = Some("String".to_string());
func
}
#[test]
fn async_forwarder_json_encodes_enum_param_and_wraps_in_rust_string() {
let func = make_async_enum_param_fn();
let mut known_dto_names: HashSet<String> = HashSet::new();
known_dto_names.insert("RegionKind".to_string());
let mut enum_names: HashSet<String> = HashSet::new();
enum_names.insert("RegionKind".to_string());
let unit_enum_names: HashSet<String> = HashSet::new();
let mut out = String::new();
emit_async_free_function_forwarder(
&func,
"setRegion",
&known_dto_names,
&enum_names,
&unit_enum_names,
"MyLibError",
&mut out,
);
assert!(
out.contains(
"let _rb_regionKind = RustString(try String(data: JSONEncoder().encode(regionKind), encoding: .utf8) ?? \"null\")"
),
"enum param must be JSON-encoded and the result wrapped in RustString(...). Got:\n{out}"
);
assert!(
!out.contains("regionKind.intoRust()"),
"enum param must not be passed via .intoRust() -- that returns an opaque type, not a String. Got:\n{out}"
);
}
fn make_async_unit_return_fn() -> FunctionDef {
let mut func = make_function("delete_catalog", vec![("catalog_id", TypeRef::String)], TypeRef::Unit);
func.is_async = true;
func.error_type = Some("String".to_string());
func
}
#[test]
fn async_forwarder_void_return_emits_no_result_binding_or_return() {
let func = make_async_unit_return_fn();
let known_dto_names: HashSet<String> = HashSet::new();
let enum_names: HashSet<String> = HashSet::new();
let unit_enum_names: HashSet<String> = HashSet::new();
let mut out = String::new();
emit_async_free_function_forwarder(
&func,
"deleteCatalog",
&known_dto_names,
&enum_names,
&unit_enum_names,
"MyLibError",
&mut out,
);
assert!(
!out.contains("let result ="),
"void async return must not bind an unused `let result`. Got:\n{out}"
);
assert!(
!out.contains("return result"),
"void async return must not emit `return result` -- there is nothing to return. Got:\n{out}"
);
assert!(
out.contains("try RustBridge.deleteCatalog(_rb_catalogId)"),
"void async body must call the bridge directly with no binding. Got:\n{out}"
);
}
fn make_async_string_return_fn() -> FunctionDef {
let mut func = make_function("fetch_status", vec![("catalog_id", TypeRef::String)], TypeRef::String);
func.is_async = true;
func.error_type = Some("String".to_string());
func
}
#[test]
fn async_forwarder_string_return_converts_via_to_string() {
let func = make_async_string_return_fn();
let known_dto_names: HashSet<String> = HashSet::new();
let enum_names: HashSet<String> = HashSet::new();
let unit_enum_names: HashSet<String> = HashSet::new();
let mut out = String::new();
emit_async_free_function_forwarder(
&func,
"fetchStatus",
&known_dto_names,
&enum_names,
&unit_enum_names,
"MyLibError",
&mut out,
);
assert!(
out.contains("let result = try RustBridge.fetchStatus(_rb_catalogId)"),
"String async return must bind the bridge's RustString result. Got:\n{out}"
);
assert!(
out.contains("return result.toString()"),
"String async return must convert the RustString result to a native Swift String via .toString(). Got:\n{out}"
);
}