use crate::backends::swift::gen_rust_crate::type_bridge::enum_from_string_fn_name;
use crate::codegen::cfg::is_host_owned_rust_path;
use crate::codegen::conversions::{VariantDeclaration, enum_variant_declaration};
use crate::codegen::generators::type_paths::resolve_type_path;
use crate::core::ir::{EnumDef, EnumVariant};
use std::collections::{HashMap, HashSet};
pub(crate) fn declared_variants<'a>(
en: &'a EnumDef,
source_crate: &str,
configured_features: Option<&[String]>,
) -> Vec<&'a EnumVariant> {
let is_host_enum = is_host_owned_rust_path(source_crate, &en.rust_path);
let configured_features_set: Option<HashSet<&str>> =
configured_features.map(|features| features.iter().map(String::as_str).collect());
en.variants
.iter()
.filter(|variant| {
!matches!(
enum_variant_declaration(variant, is_host_enum, configured_features_set.as_ref()),
VariantDeclaration::Drop
)
})
.collect()
}
pub(crate) fn emit_enum_wrapper(
en: &EnumDef,
source_crate: &str,
type_paths: &HashMap<String, String>,
configured_features: Option<&[String]>,
) -> String {
let mut out = String::new();
let source_path = resolve_type_path(&en.name, source_crate, type_paths);
let is_host_enum = is_host_owned_rust_path(source_crate, &en.rust_path);
let declared_variants = declared_variants(en, source_crate, configured_features);
out.push_str(&crate::backends::swift::template_env::render(
"enum_unit_header.jinja",
minijinja::context! {
name => &en.name,
},
));
for variant in &declared_variants {
out.push_str(&crate::backends::swift::template_env::render(
"enum_unit_variant.jinja",
minijinja::context! {
variant_name => &variant.name,
},
));
}
out.push_str("}\n\n");
out.push_str(&crate::backends::swift::template_env::render(
"enum_from_impl_header.jinja",
minijinja::context! {
source_path => &source_path,
name => &en.name,
},
));
out.push_str(" match val {\n");
for variant in &en.variants {
if variant.cfg.is_some() && !is_host_enum {
tracing::debug!(
enum_name = %en.name,
enum_rust_path = %en.rust_path,
variant_name = %variant.name,
cfg = variant.cfg.as_deref().unwrap_or_default(),
"dropping Swift bridge From-impl arm for a foreign-crate enum variant behind a \
#[cfg(...)] this crate cannot declare as a Cargo feature; the variant is \
unreachable from this conversion"
);
continue;
}
let pattern = if variant.fields.is_empty() {
variant.name.clone()
} else if variant.is_tuple {
format!("{}(..)", variant.name)
} else {
format!("{} {{ .. }}", variant.name)
};
if let Some(condition) = variant.cfg.as_deref() {
out.push_str(" #[cfg(");
out.push_str(condition);
out.push_str(")]\n");
}
out.push_str(&crate::backends::swift::template_env::render(
"enum_from_variant.jinja",
minijinja::context! {
source_path => &source_path,
variant_name => &variant.name,
pattern => pattern,
},
));
}
if crate::codegen::conversions::enum_conversion_needs_catch_all_for_features(
en,
is_host_enum,
!en.excluded_variants.is_empty(),
configured_features,
true,
) {
out.push_str(&format!(
" _ => unreachable!(\"bridge enum variant of {} not exposed in binding\"),\n",
en.name
));
}
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
let mut variants = String::new();
for variant in &declared_variants {
let serde_name = serde_variant_wire_name(variant, en.serde_rename_all.as_deref());
variants.push_str(&crate::backends::swift::template_env::render(
"rust_enum_to_string_variant.rs.jinja",
minijinja::context! {
variant_name => &variant.name,
serde_name => &serde_name,
},
));
}
out.push_str(&crate::backends::swift::template_env::render(
"rust_enum_to_string_impl.rs.jinja",
minijinja::context! {
enum_name => &en.name,
variants => variants,
},
));
let is_unit_enum = en.variants.iter().all(|v| v.fields.is_empty());
if is_unit_enum {
let mut from_string_variants = String::new();
for variant in &en.variants {
if variant.cfg.is_some() && !is_host_enum {
tracing::debug!(
enum_name = %en.name,
enum_rust_path = %en.rust_path,
variant_name = %variant.name,
cfg = variant.cfg.as_deref().unwrap_or_default(),
"dropping Swift bridge from-string reconstruction arm for a foreign-crate \
enum variant behind a #[cfg(...)] this crate cannot declare as a Cargo \
feature; the variant is unreachable from this helper"
);
continue;
}
let serde_name = serde_variant_wire_name(variant, en.serde_rename_all.as_deref());
if let Some(condition) = variant.cfg.as_deref() {
from_string_variants.push_str(" #[cfg(");
from_string_variants.push_str(condition);
from_string_variants.push_str(")]\n");
}
from_string_variants.push_str(&crate::backends::swift::template_env::render(
"rust_enum_from_string_variant.rs.jinja",
minijinja::context! {
variant_name => &variant.name,
serde_name => &serde_name,
source_path => &source_path,
},
));
}
out.push_str(&crate::backends::swift::template_env::render(
"rust_enum_from_string_impl.rs.jinja",
minijinja::context! {
fn_name => enum_from_string_fn_name(&en.name),
enum_name => &en.name,
source_path => &source_path,
variants => from_string_variants,
},
));
}
out
}
fn serde_variant_wire_name(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)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{EnumDef, EnumVariant};
fn make_unit_variant(name: &str, cfg: Option<&str>) -> EnumVariant {
EnumVariant {
name: name.to_string(),
cfg: cfg.map(str::to_string),
..Default::default()
}
}
fn make_tuple_variant(name: &str) -> EnumVariant {
EnumVariant {
name: name.to_string(),
fields: vec![crate::core::ir::FieldDef {
name: "0".to_string(),
ty: crate::core::ir::TypeRef::String,
..Default::default()
}],
is_tuple: true,
..Default::default()
}
}
fn make_struct_variant(name: &str) -> EnumVariant {
EnumVariant {
name: name.to_string(),
fields: vec![crate::core::ir::FieldDef {
name: "value".to_string(),
ty: crate::core::ir::TypeRef::String,
..Default::default()
}],
is_tuple: false,
..Default::default()
}
}
#[test]
fn fieldless_enum_still_emits_from_string_helper() {
let en = EnumDef {
name: "Mode".to_string(),
variants: vec![make_unit_variant("Fast", None), make_unit_variant("Thorough", None)],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
out.contains("fn __alef_mode_from_swift_string"),
"expected the from-string helper for a fieldless enum, got:\n{out}"
);
assert!(
out.contains("\"Fast\" => Ok(mylib::Mode::Fast),"),
"expected an `Ok`-wrapped unit-variant arm now that the helper is fallible \
(unknown wire values used to panic across the FFI boundary), got:\n{out}"
);
assert!(
out.contains("Result<mylib::Mode, String>"),
"the helper must return a Result so an unrecognised wire string can be reported \
as a real error instead of unwinding a panic across the FFI boundary, got:\n{out}"
);
assert!(
!out.contains("panic!"),
"an unrecognised enum wire string must no longer panic across the FFI boundary, got:\n{out}"
);
}
#[test]
fn fielded_enum_omits_from_string_helper_entirely() {
let en = EnumDef {
name: "AuthHeaderFormat".to_string(),
variants: vec![make_unit_variant("None", None), make_tuple_variant("ApiKey")],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
!out.contains("__alef_auth_header_format_from_swift_string"),
"expected no from-string helper for an enum with a tuple variant, got:\n{out}"
);
assert!(
!out.contains("fn __alef_"),
"expected no from-string helper of any name for an enum with a tuple variant, got:\n{out}"
);
}
#[test]
fn struct_variant_enum_omits_from_string_helper_entirely() {
let en = EnumDef {
name: "CacheBackend".to_string(),
variants: vec![make_unit_variant("Memory", None), make_struct_variant("OpenDal")],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
!out.contains("fn __alef_"),
"expected no from-string helper for an enum with a struct variant, got:\n{out}"
);
}
#[test]
fn cfg_gated_variant_emits_catch_all_in_from_impl() {
let en = EnumDef {
name: "ImageOutputFormat".to_string(),
rust_path: "dep_crate::ImageOutputFormat".to_string(),
variants: vec![
make_unit_variant("Jpeg", None),
make_unit_variant("Heif", Some(r#"feature = "heic""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
out.contains("_ => unreachable!"),
"expected catch-all `_ => unreachable!` arm when cfg-gated variant present, got:\n{out}"
);
assert!(
out.contains("ImageOutputFormat"),
"catch-all message must include the enum name, got:\n{out}"
);
}
#[test]
fn no_cfg_or_excluded_variants_does_not_emit_catch_all() {
let en = EnumDef {
name: "SimpleEnum".to_string(),
variants: vec![make_unit_variant("A", None), make_unit_variant("B", None)],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
!out.contains("_ => unreachable!"),
"unexpected catch-all arm in From impl for fully-covered enum:\n{out}"
);
}
#[test]
fn excluded_variants_alone_emits_catch_all() {
let en = EnumDef {
name: "ImageOutputFormat".to_string(),
variants: vec![make_unit_variant("Jpeg", None)],
methods: vec![],
excluded_variants: vec![make_unit_variant("ExcludedVariant", None)],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
out.contains("_ => unreachable!"),
"expected catch-all arm when excluded_variants is non-empty, got:\n{out}"
);
}
#[test]
fn foreign_cfg_variant_arm_is_dropped_not_gated_in_from_impl() {
let en = EnumDef {
name: "TierStrategy".to_string(),
rust_path: "dep_crate::TierStrategy".to_string(),
variants: vec![
make_unit_variant("Auto", None),
make_unit_variant("Tier1", Some(r#"feature = "testkit""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
!out.contains("#[cfg(feature = \"testkit\")]"),
"no invalid #[cfg] naming an undeclared feature may be emitted, got:\n{out}"
);
assert!(
!out.contains("dep_crate::TierStrategy::Tier1 =>"),
"a foreign-crate cfg-gated variant must not be referenced in the From-impl match, got:\n{out}"
);
assert!(
out.contains("_ => unreachable!"),
"dropping the arm must still leave the match exhaustive via the catch-all, got:\n{out}"
);
}
#[test]
fn foreign_cfg_variant_arm_is_dropped_from_from_string_helper() {
let en = EnumDef {
name: "TierStrategy".to_string(),
rust_path: "dep_crate::TierStrategy".to_string(),
variants: vec![
make_unit_variant("Auto", None),
make_unit_variant("Tier1", Some(r#"feature = "testkit""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
out.contains("fn __alef_tier_strategy_from_swift_string"),
"the helper is still emitted for the enum's remaining unit variants, got:\n{out}"
);
assert!(
!out.contains("dep_crate::TierStrategy::Tier1"),
"a foreign-crate cfg-gated variant must not be referenced in the from-string helper, got:\n{out}"
);
}
#[test]
fn host_cfg_variant_keeps_its_arm_and_gains_a_cfg_guard_in_from_string_helper() {
let en = EnumDef {
name: "ImageOutputFormat".to_string(),
rust_path: "mylib::ImageOutputFormat".to_string(),
variants: vec![
make_unit_variant("Jpeg", None),
make_unit_variant("Heif", Some(r#"feature = "heic""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths, None);
assert!(
out.contains("mylib::ImageOutputFormat::Heif => Self::Heif,"),
"the host-owned variant's From-impl arm must still be emitted, got:\n{out}"
);
assert!(
out.contains("\"Heif\" => Ok(mylib::ImageOutputFormat::Heif),"),
"the host-owned variant's from-string arm must still be emitted, got:\n{out}"
);
assert_eq!(
out.matches("#[cfg(feature = \"heic\")]").count(),
2,
"both the From-impl arm and the from-string arm must carry the #[cfg] guard, got:\n{out}"
);
assert!(
!out.contains("_ => unreachable!"),
"a host-owned cfg-gated variant alone must not trigger a catch-all, got:\n{out}"
);
}
#[test]
fn foreign_variant_proven_unreachable_is_absent_from_mirror_declaration() {
let en = EnumDef {
name: "SampleMode".to_string(),
rust_path: "dep_crate::SampleMode".to_string(),
variants: vec![
make_unit_variant("Base", None),
make_unit_variant("Extra", Some(r#"feature = "testkit""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let configured_features: Vec<String> = vec![];
let out = emit_enum_wrapper(&en, "hostlib", &type_paths, Some(&configured_features));
assert!(
out.contains("pub enum SampleMode {\n Base,\n}\n\n"),
"the mirror enum must declare only the reachable variant, got:\n{out}"
);
assert!(
!out.contains("Extra,"),
"a proven-unreachable foreign variant must not be declared on the mirror enum, got:\n{out}"
);
assert!(
!out.contains("Self::Extra"),
"the to-string match must not reference a variant the mirror enum no longer declares, got:\n{out}"
);
}
#[test]
fn foreign_variant_not_proven_unreachable_stays_on_mirror_declaration() {
let en = EnumDef {
name: "SampleMode".to_string(),
rust_path: "dep_crate::SampleMode".to_string(),
variants: vec![
make_unit_variant("Base", None),
make_unit_variant("Extra", Some(r#"feature = "testkit""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "hostlib", &type_paths, None);
assert!(
out.contains("pub enum SampleMode {\n Base,\n Extra,\n}\n\n"),
"an unproven foreign variant must stay on the mirror declaration, got:\n{out}"
);
assert!(
out.contains("Self::Extra => \"Extra\".to_string(),"),
"the to-string match must still cover it, got:\n{out}"
);
}
#[test]
fn host_owned_cfg_gated_variant_stays_on_mirror_declaration_regardless_of_configured_features() {
let en = EnumDef {
name: "SampleMode".to_string(),
rust_path: "hostlib::SampleMode".to_string(),
variants: vec![
make_unit_variant("Base", None),
make_unit_variant("Extra", Some(r#"feature = "extra_feature""#)),
],
methods: vec![],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let configured_features: Vec<String> = vec![];
let out = emit_enum_wrapper(&en, "hostlib", &type_paths, Some(&configured_features));
assert!(
out.contains("pub enum SampleMode {\n Base,\n Extra,\n}\n\n"),
"a host-owned cfg-gated variant must stay declared, got:\n{out}"
);
}
}