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()
}
#[cfg(test)]
pub(crate) fn emit_enum_wrapper(
en: &EnumDef,
source_crate: &str,
type_paths: &HashMap<String, String>,
configured_features: Option<&[String]>,
) -> String {
emit_enum_wrapper_with_defaults(en, source_crate, type_paths, configured_features, &HashSet::new())
}
pub(crate) fn emit_enum_wrapper_with_defaults(
en: &EnumDef,
source_crate: &str,
type_paths: &HashMap<String, String>,
configured_features: Option<&[String]>,
defaultable_names: &HashSet<&str>,
) -> 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);
if uses_lossless_payload_carrier(en) {
return crate::backends::swift::template_env::render(
"rust_enum_payload_wrapper.rs.jinja",
crate::alef_context! {
enum_name => &en.name,
source_path => &source_path,
fn_name => enum_from_string_fn_name(&en.name),
},
);
}
let declared_variants = declared_variants(en, source_crate, configured_features);
out.push_str(&crate::backends::swift::template_env::render(
"enum_unit_header.jinja",
crate::alef_context! {
name => &en.name,
},
));
for variant in &declared_variants {
out.push_str(&crate::backends::swift::template_env::render(
"enum_unit_variant.jinja",
crate::alef_context! {
variant_name => &variant.name,
},
));
}
out.push_str("}\n\n");
out.push_str(&crate::backends::swift::template_env::render(
"enum_from_impl_header.jinja",
crate::alef_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",
crate::alef_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",
crate::alef_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",
crate::alef_context! {
enum_name => &en.name,
variants => variants,
},
));
let mut from_string_variants = String::new();
if can_reconstruct_legacy_variant(en, defaultable_names) {
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");
}
let expression = default_variant_expression(&source_path, variant);
from_string_variants.push_str(&crate::backends::swift::template_env::render(
"rust_enum_from_string_variant.rs.jinja",
crate::alef_context! {
serde_name => &serde_name,
expression => expression,
},
));
}
out.push_str(&crate::backends::swift::template_env::render(
"rust_enum_from_string_impl.rs.jinja",
crate::alef_context! {
fn_name => enum_from_string_fn_name(&en.name),
enum_name => &en.name,
source_path => &source_path,
variants => from_string_variants,
},
));
}
out
}
fn default_variant_expression(source_path: &str, variant: &EnumVariant) -> String {
if variant.fields.is_empty() {
return format!("{source_path}::{}", variant.name);
}
if variant.is_tuple {
let values = vec!["::std::default::Default::default()"; variant.fields.len()].join(", ");
return format!("{source_path}::{}({values})", variant.name);
}
let fields = variant
.fields
.iter()
.map(|field| {
format!(
"{}: ::std::default::Default::default()",
crate::core::keywords::rust_raw_ident(&field.name)
)
})
.collect::<Vec<_>>()
.join(", ");
format!("{source_path}::{} {{ {fields} }}", variant.name)
}
pub(crate) fn can_reconstruct_legacy_variant(en: &EnumDef, defaultable_names: &HashSet<&str>) -> bool {
if uses_lossless_payload_carrier(en) {
return true;
}
en.variants.iter().all(|variant| {
variant
.fields
.iter()
.all(|field| type_is_defaultable(&field.ty, defaultable_names))
})
}
fn uses_lossless_payload_carrier(en: &EnumDef) -> bool {
en.has_serde && en.variants.iter().any(|variant| !variant.fields.is_empty())
}
fn type_is_defaultable(ty: &crate::core::ir::TypeRef, defaultable_names: &HashSet<&str>) -> bool {
match ty {
crate::core::ir::TypeRef::Named(name) => defaultable_names.contains(name.as_str()),
crate::core::ir::TypeRef::Optional(_)
| crate::core::ir::TypeRef::Vec(_)
| crate::core::ir::TypeRef::Map(_, _) => true,
_ => true,
}
}
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}"
);
assert!(
out.contains("pub enum Mode"),
"an all-unit enum must retain its fieldless swift-bridge mirror:\n{out}"
);
}
#[test]
fn fielded_serde_enum_preserves_and_round_trips_the_complete_payload() {
let en = EnumDef {
name: "AuthHeaderFormat".to_string(),
has_serde: true,
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("pub struct AuthHeaderFormat(pub mylib::AuthHeaderFormat);"),
"a data-carrying enum mirror must retain the complete source value:\n{out}"
);
assert!(
out.contains("Self(val)"),
"the From conversion must store the complete source value:\n{out}"
);
assert!(
out.contains("serde_json::to_string(&self.0)"),
"the opaque carrier must serialize the complete source enum:\n{out}"
);
assert!(
out.contains("serde_json::from_str::<mylib::AuthHeaderFormat>(value)"),
"the compatibility helper must deserialize the complete source enum:\n{out}"
);
assert!(
!out.contains("::std::default::Default::default()"),
"a data-carrying serde enum must never reconstruct a synthetic default payload:\n{out}"
);
}
#[test]
fn struct_variant_reconstruction_escapes_reserved_field_names() {
let mut variant = make_struct_variant("OpenDal");
variant.fields[0].name = "type".to_string();
let en = EnumDef {
name: "CacheBackend".to_string(),
variants: vec![make_unit_variant("Memory", None), variant],
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("CacheBackend::OpenDal { r#type: ::std::default::Default::default() }"),
"struct payload defaults must use valid Rust identifiers:\n{out}"
);
}
#[test]
fn non_serde_non_default_payload_type_disables_the_compatibility_helper() {
let mut variant = make_struct_variant("External");
variant.fields[0].ty = crate::core::ir::TypeRef::Named("NoDefault".to_string());
let en = EnumDef {
name: "Backend".to_string(),
variants: vec![variant],
..Default::default()
};
let out = emit_enum_wrapper(&en, "mylib", &HashMap::new(), None);
assert!(!out.contains("__alef_backend_from_swift_string"), "{out}");
}
#[test]
fn serde_payload_type_does_not_require_default_to_be_lossless() {
let mut variant = make_struct_variant("External");
variant.fields[0].ty = crate::core::ir::TypeRef::Named("NoDefault".to_string());
let en = EnumDef {
name: "Backend".to_string(),
has_serde: true,
variants: vec![variant],
..Default::default()
};
let out = emit_enum_wrapper(&en, "mylib", &HashMap::new(), None);
assert!(out.contains("serde_json::from_str::<mylib::Backend>(value)"), "{out}");
assert!(!out.contains("Default::default"), "{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}"
);
}
}