use crate::backends::swift::gen_bindings::adjacent_codable::{emit_serde_adjacent_codable, is_newtype_variant};
use crate::backends::swift::naming::swift_source_ident as swift_case_ident;
use crate::backends::swift::type_map::SwiftMapper;
use crate::codegen::serde_enum_repr::{SerdeEnumRepr, serde_enum_repr};
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{EnumDef, EnumVariant, TypeRef};
use heck::{AsSnakeCase, ToLowerCamelCase};
pub(super) fn swift_variant_case(name: &str) -> String {
swift_case_ident(&crate::codegen::naming::pascal_to_camel(name))
}
pub(super) struct EnumDeclarationCfg<'a> {
source_crate: String,
configured_features: &'a [String],
}
impl<'a> EnumDeclarationCfg<'a> {
pub(super) fn new(crate_name: &str, configured_features: &'a [String]) -> Self {
Self {
source_crate: crate_name.replace('-', "_"),
configured_features,
}
}
}
fn unit_enum_cases(en: &EnumDef, cfg: &EnumDeclarationCfg<'_>) -> String {
let declared = crate::backends::swift::gen_rust_crate::enums::declared_variants(
en,
&cfg.source_crate,
Some(cfg.configured_features),
);
let mut cases = String::new();
for variant in declared {
super::client::emit_doc_comment(&variant.doc, " ", &mut cases);
let case_name = swift_variant_case(&variant.name);
let raw_value = unit_enum_wire_value(variant, en.serde_rename_all.as_deref());
if raw_value == case_name.trim_matches('`') {
cases.push_str(&crate::backends::swift::template_env::render(
"enum_case_unit.jinja",
minijinja::context! {
case_name => &case_name,
},
));
} else {
cases.push_str(&crate::backends::swift::template_env::render(
"enum_case_raw_value.swift.jinja",
minijinja::context! {
case_name => &case_name,
raw_value => &raw_value,
},
));
}
}
cases
}
fn serde_codable_body(en: &EnumDef, repr: &SerdeEnumRepr, mapper: &SwiftMapper) -> String {
let mut body = String::new();
match repr {
SerdeEnumRepr::Internal { .. } => emit_serde_tagged_codable(en, &mut body, mapper),
SerdeEnumRepr::Adjacent { tag, content } => {
emit_serde_adjacent_codable(en, tag, content, &mut body, mapper);
}
SerdeEnumRepr::Untagged if has_untagged_single_payloads(en) => {
emit_serde_untagged_codable(en, &mut body, mapper);
}
SerdeEnumRepr::External => emit_serde_external_codable(en, &mut body, mapper),
SerdeEnumRepr::Untagged => {}
}
body
}
pub(super) fn emit_serde_external_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
let wire = |variant: &EnumVariant| {
crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
en.serde_rename_all.as_deref(),
)
};
let esc = |value: &str| value.replace('\\', "\\\\").replace('"', "\\\"");
let mut unit_decode = String::new();
let mut keyed_decode = String::new();
let mut encode_cases = String::new();
let mut untagged_attempts = String::new();
for variant in &en.variants {
let case_name = swift_variant_case(&variant.name);
let tag = esc(&wire(variant));
if variant.serde_untagged && variant.fields.len() == 1 {
let payload_ty = mapper.map_type(&variant.fields[0].ty);
let label = swift_associated_label(&variant.fields[0].name, 0);
untagged_attempts.push_str(&format!(
" if let single = try? decoder.singleValueContainer(), let value = try? single.decode({payload_ty}.self) {{\n self = .{case_name}({label}: value)\n return\n }}\n"
));
encode_cases.push_str(&format!(
" case .{case_name}(let value):\n var single = encoder.singleValueContainer()\n try single.encode(value)\n"
));
continue;
}
if variant.fields.is_empty() {
unit_decode.push_str(&format!(
" case \"{tag}\":\n self = .{case_name}\n return\n"
));
encode_cases.push_str(&format!(
" case .{case_name}:\n var single = encoder.singleValueContainer()\n try single.encode(\"{tag}\")\n"
));
continue;
}
if variant.is_tuple && variant.fields.len() == 1 {
let label = swift_associated_label(&variant.fields[0].name, 0);
let (decode_fn, encode_fn, payload_ty) = match &variant.fields[0].ty {
TypeRef::Optional(inner) => ("decodeIfPresent", "encodeIfPresent", mapper.map_type(inner)),
other if variant.fields[0].optional => ("decodeIfPresent", "encodeIfPresent", mapper.map_type(other)),
other => ("decode", "encode", mapper.map_type(other)),
};
keyed_decode.push_str(&format!(
" case \"{tag}\":\n self = .{case_name}({label}: try container.{decode_fn}({payload_ty}.self, forKey: key))\n"
));
encode_cases.push_str(&format!(
" case .{case_name}(let value):\n var container = encoder.container(keyedBy: __AlefExternalTagKey.self)\n try container.{encode_fn}(value, forKey: __AlefExternalTagKey(\"{tag}\"))\n"
));
continue;
}
let mut binds = Vec::new();
let mut reads = String::new();
let mut writes = String::new();
for (index, field) in variant.fields.iter().enumerate() {
let label = swift_associated_label(&field.name, index);
let field_ty = mapper.map_type(&field.ty);
let field_wire = esc(&crate::codegen::naming::wire_field_name(
&field.name,
field.serde_rename.as_deref(),
None,
));
binds.push(format!("let {label}"));
let (decode_fn, encode_fn, decoded_ty) = match &field.ty {
TypeRef::Optional(inner) => ("decodeIfPresent", "encodeIfPresent", mapper.map_type(inner)),
other if field.optional => ("decodeIfPresent", "encodeIfPresent", mapper.map_type(other)),
_ => ("decode", "encode", field_ty.clone()),
};
reads.push_str(&format!(
" {label}: try nested.{decode_fn}({decoded_ty}.self, forKey: __AlefExternalTagKey(\"{field_wire}\")),\n"
));
writes.push_str(&format!(
" try nested.{encode_fn}({label}, forKey: __AlefExternalTagKey(\"{field_wire}\"))\n"
));
}
let reads = reads.trim_end().trim_end_matches(',').to_string();
keyed_decode.push_str(&format!(
" case \"{tag}\":\n let nested = try container.nestedContainer(keyedBy: __AlefExternalTagKey.self, forKey: key)\n self = .{case_name}(\n{reads}\n )\n"
));
encode_cases.push_str(&format!(
" case .{case_name}({}):\n var container = encoder.container(keyedBy: __AlefExternalTagKey.self)\n var nested = container.nestedContainer(keyedBy: __AlefExternalTagKey.self, forKey: __AlefExternalTagKey(\"{tag}\"))\n{writes}",
binds.join(", ")
));
}
let unknown_tag_arm = if untagged_attempts.is_empty() {
" throw DecodingError.dataCorrupted(\n .init(codingPath: decoder.codingPath, debugDescription: \"unknown variant tag \\(raw)\"))".to_string()
} else {
" break".to_string()
};
out.push_str(&crate::backends::swift::template_env::render(
"swift_external_codable.swift.jinja",
minijinja::context! {
unit_decode => unit_decode,
keyed_decode => keyed_decode,
encode_cases => encode_cases,
untagged_attempts => untagged_attempts,
unknown_tag_arm => unknown_tag_arm,
},
));
}
fn has_untagged_single_payloads(en: &EnumDef) -> bool {
en.variants.iter().any(|v| !v.fields.is_empty())
&& en
.variants
.iter()
.filter(|v| !v.fields.is_empty())
.all(|v| v.fields.len() == 1)
}
fn is_flattened_newtype_variant(variant: &EnumVariant) -> bool {
if !is_newtype_variant(variant) {
return false;
}
let field = &variant.fields[0];
!field.optional && !matches!(&field.ty, TypeRef::Optional(_))
}
pub(super) fn emit_serde_tagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
let tag_wire = crate::codegen::serde_enum_repr::tagged_object_tag_key(en);
let tag_ident = swift_case_ident(&tag_wire.to_lower_camel_case());
let mut field_keys = std::collections::BTreeSet::new();
for variant in &en.variants {
if is_flattened_newtype_variant(variant) {
continue;
}
for (idx, field) in variant.fields.iter().enumerate() {
let swift_name = swift_associated_label(&field.name, idx);
let rust_name = crate::codegen::naming::wire_field_name(
&field.name,
field.serde_rename.as_deref(),
en.rename_all_fields.as_deref(),
);
field_keys.insert((swift_name, rust_name));
}
}
let mut coding_key_cases = String::new();
for (swift_name, rust_name) in field_keys {
coding_key_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_coding_key_case.swift.jinja",
minijinja::context! {
swift_name => &swift_name,
rust_name => &rust_name,
has_custom_wire_name => swift_name != rust_name,
},
));
}
let mut decode_cases = String::new();
for variant in &en.variants {
let variant_tag = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
en.serde_rename_all.as_deref(),
);
let case_name = swift_variant_case(&variant.name);
if variant.fields.is_empty() {
decode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_decode_unit_case.swift.jinja",
minijinja::context! {
variant_tag => &variant_tag,
case_name => &case_name,
},
));
} else if is_flattened_newtype_variant(variant) {
let field = &variant.fields[0];
let label = swift_associated_label(&field.name, 0);
let payload_ty = mapper.map_type(&field.ty);
decode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_decode_payload_case.swift.jinja",
minijinja::context! {
variant_tag => &variant_tag,
case_name => &case_name,
field_decoders => format!("{label}: try {payload_ty}(from: decoder)"),
},
));
} else {
let mut field_decoders = Vec::with_capacity(variant.fields.len());
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);
let decode_method = if is_optional { "decodeIfPresent" } else { "decode" };
field_decoders.push(format!(
"{label}: try container.{decode_method}({ty}.self, forKey: .{label})"
));
}
decode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_decode_payload_case.swift.jinja",
minijinja::context! {
variant_tag => &variant_tag,
case_name => &case_name,
field_decoders => field_decoders.join(", "),
},
));
}
}
let mut encode_cases = String::new();
for variant in &en.variants {
let variant_tag = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
en.serde_rename_all.as_deref(),
);
let case_name = swift_variant_case(&variant.name);
if variant.fields.is_empty() {
encode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_encode_unit_case.swift.jinja",
minijinja::context! {
variant_tag => &variant_tag,
tag_key => &tag_ident,
case_name => &case_name,
},
));
} else if is_flattened_newtype_variant(variant) {
let label = swift_associated_label(&variant.fields[0].name, 0);
encode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_encode_payload_case.swift.jinja",
minijinja::context! {
variant_tag => &variant_tag,
tag_key => &tag_ident,
case_name => &case_name,
bindings => format!("let {label}"),
field_encoders => format!(" try {label}.encode(to: encoder)\n"),
},
));
} 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));
}
let mut field_encoders = String::new();
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 encode_method = if is_optional { "encodeIfPresent" } else { "encode" };
field_encoders.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_encode_field.swift.jinja",
minijinja::context! {
encode_method => encode_method,
label => &label,
},
));
}
encode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_encode_payload_case.swift.jinja",
minijinja::context! {
variant_tag => &variant_tag,
tag_key => &tag_ident,
case_name => &case_name,
bindings => bindings.join(", "),
field_encoders => field_encoders,
},
));
}
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_tagged_codable.swift.jinja",
minijinja::context! {
enum_name => &en.name,
tag_ident => &tag_ident,
tag_wire => tag_wire,
coding_key_cases => coding_key_cases,
decode_cases => decode_cases,
encode_cases => encode_cases,
},
));
}
pub(super) fn emit_serde_untagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
let mut decode_attempts = String::new();
for variant in &en.variants {
if variant.fields.len() != 1 {
continue;
}
let case_name = swift_variant_case(&variant.name);
let payload_ty = mapper.map_type(&variant.fields[0].ty);
let label = swift_associated_label(&variant.fields[0].name, 0);
decode_attempts.push_str(&crate::backends::swift::template_env::render(
"swift_untagged_decode_attempt.swift.jinja",
minijinja::context! {
payload_type => &payload_ty,
case_name => &case_name,
label => &label,
},
));
}
let mut encode_cases = String::new();
for variant in &en.variants {
if variant.fields.len() != 1 {
continue;
}
let case_name = swift_variant_case(&variant.name);
let label = swift_associated_label(&variant.fields[0].name, 0);
encode_cases.push_str(&crate::backends::swift::template_env::render(
"swift_untagged_encode_case.swift.jinja",
minijinja::context! {
case_name => &case_name,
label => &label,
},
));
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_untagged_codable.swift.jinja",
minijinja::context! {
decode_attempts => decode_attempts,
encode_cases => encode_cases,
},
));
}
pub(super) fn emit_enum(
en: &EnumDef,
out: &mut String,
mapper: &SwiftMapper,
known_dto_names: &std::collections::HashSet<String>,
text_types: &[String],
cfg: &EnumDeclarationCfg<'_>,
) {
super::client::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(&crate::backends::swift::template_env::render(
"swift_enum_raw_decl.swift.jinja",
minijinja::context! {
name => &en.name,
cases => unit_enum_cases(en, cfg),
},
));
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 repr = serde_enum_repr(en);
let is_serde_untagged = matches!(repr, SerdeEnumRepr::Untagged) && has_untagged_single_payloads(en);
let mut variants = String::new();
for variant in &en.variants {
emit_variant_with_data(variant, &mut variants, mapper);
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_decl.swift.jinja",
minijinja::context! {
name => &en.name,
variants => variants,
codable_body => serde_codable_body(en, &repr, mapper),
},
));
if is_serde_untagged && text_types.iter().any(|t| t == &en.name) {
emit_swift_text_accessor(en, out);
}
emit_swift_wire_tag_accessor(en, out);
emit_enum_into_rust_extension(&en.name, out);
}
pub(super) fn emit_swift_wire_tag_accessor(en: &EnumDef, out: &mut String) {
out.push_str("extension ");
out.push_str(&en.name);
out.push_str(" {\n");
out.push_str(" /// Returns the serde wire tag identifying this variant -- the same value\n");
out.push_str(" /// the pre-promotion opaque binding's `to_string()` returned -- dropping\n");
out.push_str(" /// any associated payload.\n");
out.push_str(" public func toString() -> String {\n");
out.push_str(" switch self {\n");
for variant in &en.variants {
let case_name = swift_variant_case(&variant.name);
let wire = crate::codegen::naming::wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
en.serde_rename_all.as_deref(),
);
out.push_str(" case .");
out.push_str(&case_name);
out.push_str(":\n return \"");
out.push_str(&wire.replace('\\', "\\\\").replace('"', "\\\""));
out.push_str("\"\n");
}
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n");
}
pub(super) fn emit_swift_text_accessor(en: &EnumDef, out: &mut String) {
let name = &en.name;
out.push_str("extension ");
out.push_str(name);
out.push_str(" {\n");
out.push_str(" /// Returns the plain-text display value of this content.\n");
out.push_str(" ///\n");
out.push_str(" /// - If the value is a string, it is returned verbatim.\n");
out.push_str(" /// - If the value is an array, the `text` field of every element whose\n");
out.push_str(" /// `type` equals `\"text\"` is concatenated in order; non-text parts\n");
out.push_str(" /// (images, audio, refusals, etc.) are skipped.\n");
out.push_str(" /// - Otherwise returns an empty string.\n");
out.push_str(" public func text() -> String {\n");
out.push_str(" switch self {\n");
for variant in &en.variants {
let case_name = swift_variant_case(&variant.name);
if variant.fields.len() == 1 && is_tuple_variant_field(&variant.fields[0].name) {
let label = swift_associated_label(&variant.fields[0].name, 0);
match &variant.fields[0].ty {
TypeRef::String => {
out.push_str(" case .");
out.push_str(&case_name);
out.push_str("(let ");
out.push_str(&label);
out.push_str("):\n return ");
out.push_str(&label);
out.push('\n');
}
TypeRef::Vec(elem_ty) if matches!(**elem_ty, TypeRef::Named(_) | TypeRef::Json) => {
out.push_str(" case .");
out.push_str(&case_name);
out.push_str("(let ");
out.push_str(&label);
out.push_str("):\n");
out.push_str(" var result = \"\"\n");
out.push_str(" for part in ");
out.push_str(&label);
out.push_str(" {\n");
out.push_str(" guard let data = try? JSONEncoder().encode(part),\n");
out.push_str(
" let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],\n",
);
out.push_str(" object[\"type\"] as? String == \"text\",\n");
out.push_str(" let textValue = object[\"text\"] as? String\n");
out.push_str(" else { continue }\n");
out.push_str(" result += textValue\n");
out.push_str(" }\n");
out.push_str(" return result\n");
}
_ => emit_swift_text_empty_case(out, &case_name),
}
} else if variant.fields.is_empty() {
emit_swift_text_empty_case(out, &case_name);
} else {
out.push_str(" case .");
out.push_str(&case_name);
out.push_str(":\n return \"\"\n");
}
}
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n");
}
fn emit_swift_text_empty_case(out: &mut String, case_name: &str) {
out.push_str(" case .");
out.push_str(case_name);
out.push_str(":\n return \"\"\n");
}
fn is_tuple_variant_field(name: &str) -> bool {
let stripped = name.strip_prefix('_').unwrap_or(name);
!stripped.is_empty() && stripped.bytes().all(|b| b.is_ascii_digit())
}
pub(super) fn all_variants_codable_safe(en: &EnumDef, known_dto_names: &std::collections::HashSet<String>) -> bool {
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))
}
pub(super) fn emit_enum_without_into_rust(
en: &EnumDef,
out: &mut String,
mapper: &SwiftMapper,
known_dto_names: &std::collections::HashSet<String>,
cfg: &EnumDeclarationCfg<'_>,
) {
super::client::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(&crate::backends::swift::template_env::render(
"swift_enum_raw_decl.swift.jinja",
minijinja::context! {
name => &en.name,
cases => unit_enum_cases(en, cfg),
},
));
return;
}
if all_variants_codable_safe(en, known_dto_names) {
let mut variants = String::new();
for variant in &en.variants {
emit_variant_with_data(variant, &mut variants, mapper);
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_decl.swift.jinja",
minijinja::context! {
name => &en.name,
variants => variants,
codable_body => serde_codable_body(en, &serde_enum_repr(en), mapper),
},
));
} else {
out.push_str(&crate::backends::swift::template_env::render(
"typealias.jinja",
minijinja::context! {
name => &en.name,
},
));
}
}
pub(super) 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(&crate::backends::swift::template_env::render(
"swift_enum_into_rust.swift.jinja",
minijinja::context! {
name => name,
from_json_fn => from_json_fn,
},
));
}
pub(super) fn unit_enum_wire_value(variant: &crate::core::ir::EnumVariant, rename_all: Option<&str>) -> String {
crate::codegen::naming::wire_variant_value(&variant.name, variant.serde_rename.as_deref(), rename_all)
}
pub(super) fn emit_variant_with_data(variant: &EnumVariant, out: &mut String, mapper: &SwiftMapper) {
super::client::emit_doc_comment(&variant.doc, " ", out);
let case_name = swift_variant_case(&variant.name);
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(", "),
},
));
}
}
pub(super) 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())
}
#[cfg(test)]
mod tagged_codable_tests {
use super::*;
use crate::core::ir::FieldDef;
fn render_tagged(en: &EnumDef) -> String {
let mut out = String::new();
emit_serde_tagged_codable(en, &mut out, &SwiftMapper);
out
}
fn field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..FieldDef::default()
}
}
fn newtype_variant(name: &str, ty: TypeRef) -> EnumVariant {
EnumVariant {
name: name.to_string(),
is_tuple: true,
fields: vec![field("0", ty)],
..EnumVariant::default()
}
}
fn struct_variant(name: &str, fields: Vec<FieldDef>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
fields,
..EnumVariant::default()
}
}
fn tagged_enum(tag: &str, variants: Vec<EnumVariant>) -> EnumDef {
EnumDef {
name: "FormatMetadata".to_string(),
has_serde: true,
serde_tag: Some(tag.to_string()),
serde_rename_all: Some("snake_case".to_string()),
variants,
..EnumDef::default()
}
}
#[test]
fn should_decode_an_internally_tagged_newtype_payload_from_the_same_decoder() {
let en = tagged_enum(
"format_type",
vec![newtype_variant("Excel", TypeRef::Named("ExcelMetadata".to_string()))],
);
let out = render_tagged(&en);
assert!(
out.contains("self = .excel(field0: try ExcelMetadata(from: decoder))"),
"{out}"
);
}
#[test]
fn should_encode_an_internally_tagged_newtype_payload_into_the_tag_container() {
let en = tagged_enum(
"format_type",
vec![newtype_variant("Excel", TypeRef::Named("ExcelMetadata".to_string()))],
);
let out = render_tagged(&en);
assert!(out.contains("case .excel(let field0):"), "{out}");
assert!(
out.contains("try container.encode(\"excel\", forKey: .formatType)"),
"{out}"
);
assert!(out.contains("try field0.encode(to: encoder)"), "{out}");
}
#[test]
fn should_not_give_an_internally_tagged_newtype_payload_a_positional_coding_key() {
let en = tagged_enum(
"format_type",
vec![newtype_variant("Excel", TypeRef::Named("ExcelMetadata".to_string()))],
);
let out = render_tagged(&en);
assert!(
!out.contains("field0 = \"0\""),
"serde flattens the payload; there is no \"0\" key on the wire: {out}"
);
assert!(
!out.contains("forKey: .field0"),
"serde flattens the payload; there is no \"0\" key on the wire: {out}"
);
}
#[test]
fn should_keep_the_keyed_form_for_an_internally_tagged_struct_variant() {
let en = tagged_enum(
"type",
vec![struct_variant(
"Basic",
vec![field("username", TypeRef::String), field("password", TypeRef::String)],
)],
);
let out = render_tagged(&en);
assert!(out.contains("case username"), "{out}");
assert!(out.contains("case password"), "{out}");
assert!(!out.contains("case username = "), "{out}");
assert!(
out.contains(
"self = .basic(username: try container.decode(String.self, forKey: .username), \
password: try container.decode(String.self, forKey: .password))"
),
"{out}"
);
assert!(
out.contains("try container.encode(username, forKey: .username)"),
"{out}"
);
assert!(
!out.contains("(from: decoder)"),
"a named-field variant is not a flattened newtype: {out}"
);
}
#[test]
fn should_keep_the_keyed_form_for_an_optional_newtype_payload() {
let en = tagged_enum(
"format_type",
vec![newtype_variant(
"Excel",
TypeRef::Optional(Box::new(TypeRef::Named("ExcelMetadata".to_string()))),
)],
);
let out = render_tagged(&en);
assert!(
out.contains("field0: try container.decodeIfPresent("),
"`T?(from:)` is not expressible in Swift, so the keyed path is kept: {out}"
);
assert!(out.contains("forKey: .field0"), "{out}");
assert!(out.contains("case field0 = \"0\""), "{out}");
assert!(!out.contains("(from: decoder)"), "{out}");
}
#[test]
fn should_alias_a_snake_case_tag_key_onto_a_camel_case_swift_identifier() {
let en = tagged_enum("format_type", vec![struct_variant("Excel", vec![])]);
let out = render_tagged(&en);
assert!(out.contains("case formatType = \"format_type\""), "{out}");
assert!(out.contains("forKey: .formatType"), "{out}");
assert!(!out.contains("case format_type"), "{out}");
assert!(!out.contains("forKey: .format_type"), "{out}");
}
#[test]
fn should_alias_a_hyphenated_tag_key_onto_a_legal_swift_identifier() {
let en = tagged_enum("format-type", vec![struct_variant("Excel", vec![])]);
let out = render_tagged(&en);
assert!(out.contains("case formatType = \"format-type\""), "{out}");
assert!(out.contains("forKey: .formatType"), "{out}");
assert!(!out.contains("case format-type"), "{out}");
}
#[test]
fn should_backtick_escape_a_tag_key_that_collides_with_a_swift_keyword() {
let en = tagged_enum("protocol", vec![struct_variant("Excel", vec![])]);
let out = render_tagged(&en);
assert!(out.contains("case `protocol` = \"protocol\""), "{out}");
assert!(out.contains("forKey: .`protocol`"), "{out}");
}
#[test]
fn should_give_the_conventional_type_tag_key_an_explicit_raw_value() {
let en = tagged_enum("type", vec![struct_variant("Excel", vec![])]);
let out = render_tagged(&en);
assert!(out.contains("case type = \"type\""), "{out}");
assert!(out.contains("forKey: .type"), "{out}");
}
}
#[cfg(test)]
mod wire_tag_accessor_tests {
use super::*;
use crate::core::ir::FieldDef;
use std::collections::HashSet;
fn payload_enum() -> EnumDef {
EnumDef {
name: "FormatMetadata".to_string(),
has_serde: true,
serde_rename_all: Some("snake_case".to_string()),
variants: vec![
EnumVariant {
name: "Pdf".to_string(),
fields: vec![FieldDef {
name: "_0".to_string(),
ty: TypeRef::String,
..FieldDef::default()
}],
is_tuple: true,
..EnumVariant::default()
},
EnumVariant {
name: "Excel".to_string(),
fields: vec![FieldDef {
name: "_0".to_string(),
ty: TypeRef::String,
..FieldDef::default()
}],
is_tuple: true,
..EnumVariant::default()
},
],
..EnumDef::default()
}
}
#[test]
fn payload_carrying_enum_declares_a_to_string_wire_tag_accessor() {
let en = payload_enum();
let mapper = SwiftMapper;
let known: HashSet<String> = HashSet::new();
let cfg = EnumDeclarationCfg::new("sample", &[]);
let mut out = String::new();
emit_enum(&en, &mut out, &mapper, &known, &[], &cfg);
assert!(
out.contains("public func toString() -> String {"),
"expected a `toString()` wire-tag accessor, got:\n{out}"
);
assert!(out.contains("case .pdf:\n return \"pdf\""), "got:\n{out}");
assert!(
out.contains("case .excel:\n return \"excel\""),
"got:\n{out}"
);
assert!(out.contains("case pdf(field0: String)"), "got:\n{out}");
assert!(out.contains("case excel(field0: String)"), "got:\n{out}");
}
}
#[cfg(test)]
mod external_codable_tests {
use super::*;
use crate::core::ir::FieldDef;
use std::collections::HashSet;
fn external_enum() -> EnumDef {
EnumDef {
name: "OutputFormat".to_string(),
has_serde: true,
serde_rename_all: Some("snake_case".to_string()),
variants: vec![
EnumVariant {
name: "Plain".to_string(),
..EnumVariant::default()
},
EnumVariant {
name: "Custom".to_string(),
fields: vec![FieldDef {
name: "_0".to_string(),
ty: TypeRef::String,
..FieldDef::default()
}],
is_tuple: true,
..EnumVariant::default()
},
],
..EnumDef::default()
}
}
fn render(en: &EnumDef) -> String {
let mut out = String::new();
emit_enum(
en,
&mut out,
&SwiftMapper,
&HashSet::new(),
&[],
&EnumDeclarationCfg::new("sample", &[]),
);
out
}
#[test]
fn externally_tagged_enum_declares_a_custom_codable_body() {
let out = render(&external_enum());
assert!(
out.contains("public init(from decoder: Decoder) throws {"),
"got:\n{out}"
);
assert!(out.contains("struct __AlefExternalTagKey: CodingKey"), "got:\n{out}");
}
#[test]
fn a_unit_variant_decodes_from_a_bare_string_not_a_keyed_object() {
let out = render(&external_enum());
assert!(out.contains("try? single.decode(String.self)"), "got:\n{out}");
assert!(out.contains("case \"plain\":"), "got:\n{out}");
assert!(out.contains("try single.encode(\"plain\")"), "got:\n{out}");
}
#[test]
fn a_newtype_variant_decodes_the_payload_directly_under_its_tag_key() {
let out = render(&external_enum());
assert!(
out.contains("self = .custom(field0: try container.decode(String.self, forKey: key))"),
"got:\n{out}"
);
assert!(
out.contains("try container.encode(value, forKey: __AlefExternalTagKey(\"custom\"))"),
"got:\n{out}"
);
assert!(
!out.contains("\"field0\""),
"payload must not be wrapped in a field0 key:\n{out}"
);
}
}