1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
//! Emits the swift-bridge mirror enum wrapper and its `From` conversion.
//!
//! Only unit variants are exposed in the bridge enum. Data variants are
//! absorbed by a catch-all `Unknown` variant when present.
use crate::codegen::generators::type_paths::resolve_type_path;
use crate::core::ir::EnumDef;
use std::collections::HashMap;
pub(crate) fn emit_enum_wrapper(en: &EnumDef, source_crate: &str, type_paths: &HashMap<String, String>) -> String {
let mut out = String::new();
let source_path = resolve_type_path(&en.name, source_crate, type_paths);
// Emit all variants (both unit and data-bearing) as unit-only variants in the bridge enum.
// This preserves the variant tag for all source enum variants, allowing consumers to
// distinguish them without collapsing data-bearing variants to Unknown.
// Bridge enum variant names: use the raw Rust identifier from the IR.
// Do NOT apply to_upper_camel_case() — heck transforms acronyms like "EasyOCR"
// to "EasyOcr" and "RDFa" to "RdFa", creating names that don't match the source.
// The bridge enum uses the same names as the source enum so the From impl
// match arms are valid Rust identifiers on both sides.
out.push_str(&crate::backends::swift::template_env::render(
"enum_unit_header.jinja",
minijinja::context! {
name => &en.name,
},
));
for variant in &en.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");
// From conversion: match all source variants and map to the corresponding bridge variant.
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");
let has_cfg_variants = en.variants.iter().any(|v| v.cfg.is_some());
for variant in &en.variants {
let pattern = if variant.fields.is_empty() {
variant.name.clone()
} else if variant.is_tuple {
format!("{}(..)", variant.name)
} else {
format!("{} {{ .. }}", variant.name)
};
// Mirror the dart enum_conversions emitter: variants gated by upstream `#[cfg(...)]`
// (e.g. `Heif` under `#[cfg(feature = "heic")]`) must carry that same gate on the
// From-impl arm so cross-compiling against feature sets that drop the variant (iOS
// uses `android-target` which excludes `heic`) compiles cleanly.
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,
},
));
}
// Emit a catch-all wildcard arm whenever either of these conditions holds:
//
// 1. `excluded_variants` is non-empty: source variants live outside `variants`;
// the match arms only cover `variants`, making it non-exhaustive (E0004) under
// `--all-features`.
//
// 2. Any `variants` entry carries a `#[cfg(feature = "X")]` gate: when that feature
// is inactive the arm is compiled out, also making the match non-exhaustive.
//
// `#![allow(unreachable_patterns)]` at the crate root suppresses the redundant-arm
// warning when all variants are in fact covered.
if !en.excluded_variants.is_empty() || has_cfg_variants {
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");
// `to_string` impl — returns the serialized (serde) name of the variant so that
// swift-bridge can expose it as a `toString() -> RustString` Swift method.
// This lets e2e tests do `linkType().toString().toString()` to get "anchor" etc.
// instead of relying on `String(describing:)` which yields the opaque class description.
let mut variants = String::new();
for variant in &en.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,
},
));
out
}
/// Compute the serde-serialized name for a unit enum variant.
///
/// Priority order:
/// 1. Explicit `#[serde(rename = "...")]` on the variant.
/// 2. `rename_all` transformation applied to the Rust identifier.
/// 3. Raw Rust identifier (no transformation).
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()
}
}
/// When any variant in the primary list carries a `#[cfg(...)]` gate the
/// From-impl match must emit a `_ => unreachable!()` catch-all arm so it
/// remains exhaustive when that feature is inactive (E0004 guard).
#[test]
fn cfg_gated_variant_emits_catch_all_in_from_impl() {
let en = EnumDef {
name: "ImageOutputFormat".to_string(),
variants: vec![
make_unit_variant("Jpeg", None),
make_unit_variant("Heif", Some(r#"feature = "heic""#)),
],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths);
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}"
);
}
/// When no variant is cfg-gated and `excluded_variants` is empty, no catch-all
/// should be emitted (the match is statically exhaustive without it).
#[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)],
excluded_variants: vec![],
..Default::default()
};
let type_paths = std::collections::HashMap::new();
let out = emit_enum_wrapper(&en, "mylib", &type_paths);
assert!(
!out.contains("_ => unreachable!"),
"unexpected catch-all arm in From impl for fully-covered enum:\n{out}"
);
}
/// `excluded_variants` alone (no inline cfg gates) must still trigger the catch-all.
#[test]
fn excluded_variants_alone_emits_catch_all() {
let en = EnumDef {
name: "ImageOutputFormat".to_string(),
variants: vec![make_unit_variant("Jpeg", None)],
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);
assert!(
out.contains("_ => unreachable!"),
"expected catch-all arm when excluded_variants is non-empty, got:\n{out}"
);
}
}