use crate::backends::swift::naming::swift_source_ident as swift_case_ident;
use crate::backends::swift::type_map::SwiftMapper;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{EnumDef, EnumVariant, TypeRef};
use heck::{AsSnakeCase, ToLowerCamelCase};
pub(super) fn emit_serde_tagged_codable(en: &EnumDef, out: &mut String, mapper: &SwiftMapper) {
let tag_key = en.serde_tag.as_deref().unwrap_or("type");
let mut field_keys = std::collections::BTreeSet::new();
for variant in &en.variants {
for (idx, field) in variant.fields.iter().enumerate() {
let swift_name = swift_associated_label(&field.name, idx);
let rust_name = field.serde_rename.as_deref().unwrap_or(&field.name);
field_keys.insert((swift_name, rust_name.to_string()));
}
}
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_case_ident(&variant.name.to_lower_camel_case());
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 {
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_case_ident(&variant.name.to_lower_camel_case());
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_key,
case_name => &case_name,
},
));
} 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_key,
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_key => tag_key,
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_case_ident(&variant.name.to_lower_camel_case());
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_case_ident(&variant.name.to_lower_camel_case());
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>,
) {
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; let mut cases = String::new();
for variant in &en.variants {
super::client::emit_doc_comment(&variant.doc, " ", &mut cases);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
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,
},
));
}
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_raw_decl.swift.jinja",
minijinja::context! {
name => &en.name,
cases => cases,
},
));
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 has_serde_tag = en.serde_tag.is_some() && !en.serde_untagged;
let is_serde_untagged = en.serde_untagged
&& en.variants.iter().any(|v| !v.fields.is_empty())
&& en
.variants
.iter()
.filter(|v| !v.fields.is_empty())
.all(|v| v.fields.len() == 1);
if has_serde_tag {
let mut variants = String::new();
for variant in &en.variants {
emit_variant_with_data(variant, &mut variants, mapper);
}
let mut codable_body = String::new();
emit_serde_tagged_codable(en, &mut codable_body, mapper);
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_decl.swift.jinja",
minijinja::context! {
name => &en.name,
variants => variants,
codable_body => codable_body,
},
));
} else if is_serde_untagged {
let mut variants = String::new();
for variant in &en.variants {
emit_variant_with_data(variant, &mut variants, mapper);
}
let mut codable_body = String::new();
emit_serde_untagged_codable(en, &mut codable_body, mapper);
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_decl.swift.jinja",
minijinja::context! {
name => &en.name,
variants => variants,
codable_body => codable_body,
},
));
} else {
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 => "",
},
));
}
emit_enum_into_rust_extension(&en.name, out);
}
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>,
) {
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;
let mut cases = String::new();
for variant in &en.variants {
super::client::emit_doc_comment(&variant.doc, " ", &mut cases);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
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,
},
));
}
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_enum_raw_decl.swift.jinja",
minijinja::context! {
name => &en.name,
cases => cases,
},
));
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 => "",
},
));
} 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_case_ident(&variant.name.to_lower_camel_case());
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())
}