use alef::backends::swift::SwiftBackend;
use alef::core::backend::Backend;
use alef::core::config::{ResolvedCrateConfig, new_config::NewAlefConfig};
use alef::core::ir::{
ApiSurface, CoreWrapper, DefaultValue, EnumDef, EnumVariant, FieldDef, PrimitiveType, TypeDef, TypeRef,
};
fn make_field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
version: Default::default(),
name: name.to_string(),
ty,
optional,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: CoreWrapper::None,
vec_inner_core_wrapper: CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
serde_with: None,
serde_skip_serializing_if: false,
serde_skip: false,
sensitive: false,
binding_excluded: false,
binding_exclusion_reason: None,
original_type: None,
}
}
fn make_type(name: &str, fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("demo::{name}"),
original_rust_path: String::new(),
fields,
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
doc: String::new(),
cfg: None,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
serde_container_default: false,
serde_container_conversion: Default::default(),
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
has_private_fields: false,
version: Default::default(),
}
}
fn make_config() -> ResolvedCrateConfig {
let toml = r#"
[workspace]
languages = ["swift"]
[[crates]]
name = "demo-crate"
sources = ["src/lib.rs"]
"#;
let cfg: NewAlefConfig = toml::from_str(toml).expect("test config must parse");
cfg.resolve().expect("test config must resolve").remove(0)
}
fn api_with_type(ty: TypeDef) -> ApiSurface {
ApiSurface {
unresolved_modules: Vec::new(),
crate_name: "demo".into(),
version: "0.1.0".into(),
types: vec![ty],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::BTreeMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
}
}
fn host_matcher_enum() -> EnumDef {
EnumDef {
name: "HostMatcher".to_string(),
rust_path: "demo::HostMatcher".to_string(),
variants: vec![EnumVariant {
name: "Exact".to_string(),
..Default::default()
}],
has_serde: true,
..Default::default()
}
}
#[test]
fn struct_with_serde_default_bool_field_emits_custom_decoder_with_false_fallback() {
let mut field = make_field("enabled", TypeRef::Primitive(PrimitiveType::Bool), false);
field.typed_default = Some(DefaultValue::BoolLiteral(false));
let mut ty = make_type("Config", vec![field]);
ty.has_serde = true;
ty.has_default = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(ty), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(from decoder: any Decoder) throws"),
"expected custom Codable decoder when has_default is true:\n{content}"
);
assert!(
content.contains("try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? false"),
"expected `?? false` fallback for BoolLiteral(false) typed default:\n{content}"
);
assert!(
content.contains("private enum CodingKeys: String, CodingKey"),
"expected CodingKeys enum to be emitted alongside custom decoder:\n{content}"
);
}
#[test]
fn struct_with_serde_default_bool_field_emits_custom_decoder_with_true_fallback() {
let mut field = make_field("structure", TypeRef::Primitive(PrimitiveType::Bool), false);
field.typed_default = Some(DefaultValue::BoolLiteral(true));
let mut ty = make_type("Config", vec![field]);
ty.has_serde = true;
ty.has_default = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(ty), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(from decoder: any Decoder) throws"),
"expected custom Codable decoder when has_default is true:\n{content}"
);
assert!(
content.contains("try container.decodeIfPresent(Bool.self, forKey: .structure) ?? true"),
"expected `?? true` fallback for BoolLiteral(true) typed default:\n{content}"
);
}
#[test]
fn struct_with_vec_field_and_default_impl_defaults_to_empty_array() {
let mut field = make_field("items", TypeRef::Vec(Box::new(TypeRef::String)), false);
field.typed_default = Some(DefaultValue::Empty);
let mut ty = make_type("Bag", vec![field]);
ty.has_serde = true;
ty.has_default = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(ty), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(from decoder: any Decoder) throws"),
"expected custom Codable decoder when has_default is true:\n{content}"
);
assert!(
content.contains("try container.decodeIfPresent([String].self, forKey: .items) ?? []"),
"expected `?? []` fallback for Vec<String> with Empty typed default:\n{content}"
);
}
#[test]
fn struct_without_default_impl_does_not_emit_custom_decoder() {
let mut ty = make_type(
"Strict",
vec![make_field("value", TypeRef::Primitive(PrimitiveType::I32), false)],
);
ty.has_serde = true;
ty.has_default = false;
let files = SwiftBackend
.generate_bindings(&api_with_type(ty), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public struct Strict: Codable, Sendable, Hashable"),
"must still emit first-class Codable struct:\n{content}"
);
assert!(
!content.contains("public init(from decoder: any Decoder)"),
"must NOT emit custom decoder for structs without Default impl:\n{content}"
);
}
#[test]
fn struct_with_optional_field_decodes_to_nil_fallback() {
let mut field = make_field("chunk_max_size", TypeRef::Primitive(PrimitiveType::Usize), true);
field.typed_default = Some(DefaultValue::None);
let mut ty = make_type("Config", vec![field]);
ty.has_serde = true;
ty.has_default = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(ty), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(from decoder: any Decoder) throws"),
"expected custom Codable decoder for has_default struct with optional field:\n{content}"
);
assert!(
content.contains("try container.decodeIfPresent(UInt.self, forKey: .chunkMaxSize) ?? nil"),
"expected `?? nil` fallback for optional field:\n{content}"
);
}
#[test]
fn struct_with_string_literal_default_uses_quoted_swift_literal() {
let mut field = make_field("language", TypeRef::String, false);
field.typed_default = Some(DefaultValue::StringLiteral("python".to_string()));
let mut ty = make_type("Config", vec![field]);
ty.has_serde = true;
ty.has_default = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(ty), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("try container.decodeIfPresent(String.self, forKey: .language) ?? \"python\""),
"expected quoted Swift string literal fallback:\n{content}"
);
}
#[test]
fn memberwise_initializer_defaults_an_empty_named_collection() {
let mut denylist = make_field(
"denylist",
TypeRef::Vec(Box::new(TypeRef::Named("HostMatcher".to_string()))),
false,
);
denylist.default = Some("/* serde(default) */".to_string());
denylist.typed_default = Some(DefaultValue::Empty);
let mut policy = make_type("SsrfPolicy", vec![denylist]);
policy.has_serde = true;
policy.has_default = true;
let mut api = api_with_type(policy);
api.enums.push(host_matcher_enum());
let files = SwiftBackend
.generate_bindings(&api, &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(denylist: [HostMatcher] = [])"),
"a newly added empty collection must not become a required Swift argument:\n{content}"
);
}
#[test]
fn memberwise_initializer_uses_bare_serde_default_for_an_empty_map() {
let mut headers = make_field(
"headers",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
false,
);
headers.default = Some("/* serde(default) */".to_string());
let mut config = make_type("Config", vec![headers]);
config.has_serde = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(config), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(headers: [String: String] = [:])"),
"bare serde(default) metadata guarantees the collection zero:\n{content}"
);
}
#[test]
fn memberwise_initializer_does_not_replace_an_unresolved_collection_default_with_empty() {
let mut values = make_field("values", TypeRef::Vec(Box::new(TypeRef::String)), false);
values.default = Some("/* serde(default) */".to_string());
values.typed_default = Some(DefaultValue::Unresolved("Config::default_values".to_string()));
let mut config = make_type("Config", vec![values]);
config.has_serde = true;
let files = SwiftBackend
.generate_bindings(&api_with_type(config), &make_config())
.expect("generate must succeed");
let content = &files[0].content;
assert!(
content.contains("public init(values: [String])"),
"an unresolved Rust default must remain required instead of becoming empty:\n{content}"
);
}