use crate::backends::swift::gen_bindings::dto::emit_decoder_init;
use crate::backends::swift::gen_bindings::zero_arg_default::compute_zero_arg_constructible_names;
use crate::backends::swift::type_map::SwiftMapper;
use crate::core::ir::{ApiSurface, DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
use std::collections::HashSet;
fn decode_body(name: &str, ty: TypeRef, typed_default: DefaultValue) -> String {
let field = FieldDef {
name: name.to_string(),
ty,
typed_default: Some(typed_default),
..Default::default()
};
let mut out = String::new();
emit_decoder_init(&SwiftMapper, &[&field], &HashSet::new(), &mut out);
out
}
#[test]
fn a_named_serde_default_on_a_vec_defers_to_rust_instead_of_decoding_an_empty_array() {
let out = decode_body(
"scheme_allowlist",
TypeRef::Vec(Box::new(TypeRef::String)),
DefaultValue::FunctionCall("default_scheme_allowlist".to_string()),
);
assert!(
!out.contains("?? []"),
"alef never evaluates default_scheme_allowlist(); `?? []` decodes an absent key into an \
empty allow-list that permits nothing:\n{out}"
);
assert!(
out.contains("try container.decode([String].self, forKey: .schemeAllowlist)"),
"an unreadable default must leave the key required so an absent key is a visible \
DecodingError:\n{out}"
);
}
#[test]
fn a_named_serde_default_on_a_map_defers_to_rust_instead_of_decoding_an_empty_dictionary() {
let out = decode_body(
"header_overrides",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
DefaultValue::PublicFunctionCall("sample_crate::Policy::header_overrides".to_string()),
);
assert!(
!out.contains("?? [:]"),
"a resolved function-call default is still a value alef has not read:\n{out}"
);
}
#[test]
fn an_empty_default_still_decodes_to_the_swift_collection_zero() {
let vec_out = decode_body(
"scheme_allowlist",
TypeRef::Vec(Box::new(TypeRef::String)),
DefaultValue::Empty,
);
assert!(
vec_out.contains("?? []"),
"`Empty` is the type's own default and keeps the empty-array fallback:\n{vec_out}"
);
let map_out = decode_body(
"header_overrides",
TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::String)),
DefaultValue::Empty,
);
assert!(
map_out.contains("?? [:]"),
"`Empty` keeps the empty-dictionary fallback:\n{map_out}"
);
let scalar_out = decode_body(
"redirect_limit",
TypeRef::Primitive(PrimitiveType::U32),
DefaultValue::Empty,
);
assert!(
scalar_out.contains("?? 0"),
"`Empty` keeps the scalar zero fallback:\n{scalar_out}"
);
}
#[test]
fn a_named_field_defaulting_to_type_default_uses_the_zero_arg_initializer() {
let field = FieldDef {
name: "ngram_range".to_string(),
ty: TypeRef::Named("NgramRange".to_string()),
typed_default: Some(DefaultValue::Empty),
..Default::default()
};
let zero_arg_constructible_names: HashSet<String> = ["NgramRange".to_string()].into_iter().collect();
let mut out = String::new();
emit_decoder_init(&SwiftMapper, &[&field], &zero_arg_constructible_names, &mut out);
assert_eq!(
out,
" public init(from decoder: any Decoder) throws {\n \
let container = try decoder.container(keyedBy: CodingKeys.self)\n \
self.ngramRange = try container.decodeIfPresent(NgramRange.self, forKey: .ngramRange) ?? NgramRange()\n }\n",
"an absent key must fall back to the type's own zero-arg initializer:\n{out}"
);
}
#[test]
fn an_enum_field_defaulting_to_type_default_stays_required() {
let field = FieldDef {
name: "algorithm".to_string(),
ty: TypeRef::Named("KeywordAlgorithm".to_string()),
typed_default: Some(DefaultValue::Empty),
..Default::default()
};
let mut out = String::new();
emit_decoder_init(&SwiftMapper, &[&field], &HashSet::new(), &mut out);
assert!(
out.contains("self.algorithm = try container.decode(KeywordAlgorithm.self, forKey: .algorithm)"),
"an enum with no zero-arg initializer must stay a required decode:\n{out}"
);
assert!(
!out.contains("decodeIfPresent"),
"there is no safe fallback value to guess for a feature-gated enum default:\n{out}"
);
}
#[test]
fn zero_arg_constructible_names_admits_fully_defaulted_structs_only() {
let api = ApiSurface {
types: vec![
TypeDef {
name: "NgramRange".to_string(),
rust_path: "demo::NgramRange".to_string(),
has_serde: true,
fields: vec![
FieldDef {
name: "min".to_string(),
ty: TypeRef::Primitive(PrimitiveType::Usize),
typed_default: Some(DefaultValue::IntLiteral(1)),
..Default::default()
},
FieldDef {
name: "max".to_string(),
ty: TypeRef::Primitive(PrimitiveType::Usize),
typed_default: Some(DefaultValue::IntLiteral(3)),
..Default::default()
},
],
..Default::default()
},
TypeDef {
name: "YakeParams".to_string(),
rust_path: "demo::YakeParams".to_string(),
has_serde: true,
fields: vec![FieldDef {
name: "window_size".to_string(),
ty: TypeRef::Primitive(PrimitiveType::Usize),
..Default::default()
}],
..Default::default()
},
],
..Default::default()
};
let known_dto_names: HashSet<String> = ["NgramRange".to_string(), "YakeParams".to_string()]
.into_iter()
.collect();
let admitted = compute_zero_arg_constructible_names(&api, &known_dto_names);
assert!(
admitted.contains("NgramRange"),
"every field defaults, so the memberwise init takes zero arguments: {admitted:?}"
);
assert!(
!admitted.contains("YakeParams"),
"`window_size` has no default, so `YakeParams()` would not compile: {admitted:?}"
);
}