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
use crate::backends::swift::naming::swift_rust_shim_ident as swift_case_ident;
use crate::backends::swift::type_map::SwiftMapper;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{ErrorDef, TypeRef};
use heck::ToLowerCamelCase;
use std::collections::BTreeSet;
/// Emits a Swift `Swift.Error`-conforming `public enum` for the given `ErrorDef`.
///
/// When the Rust error type is named `Error`, Swift would parse `public enum Error: Error`
/// as a circular raw-type binding rather than protocol conformance. In that case the enum
/// is renamed to `{module_name}Error` (e.g. `SampleLanguagePackError`) to avoid the
/// clash. The protocol reference is always qualified as `Swift.Error` for clarity.
pub(super) fn emit_error(error: &ErrorDef, module_name: &str, out: &mut String, mapper: &SwiftMapper) {
// Rename bare `Error` to `{ModuleName}Error` to avoid the Swift parser ambiguity
// where `public enum Error: Error` is interpreted as a circular raw-type binding
// instead of protocol conformance.
let name = if error.name == "Error" {
format!("{module_name}Error")
} else {
error.name.clone()
};
super::client::emit_doc_comment(&error.doc, "", out);
out.push_str(&crate::backends::swift::template_env::render(
"error_enum_header.jinja",
minijinja::context! {
name => &name,
},
));
for variant in &error.variants {
super::client::emit_doc_comment(&variant.doc, " ", out);
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
if variant.is_unit || variant.fields.is_empty() {
out.push_str(&crate::backends::swift::template_env::render(
"error_case.jinja",
minijinja::context! {
case_name => &case_name,
},
));
} else {
let mut assoc: Vec<String> = Vec::with_capacity(variant.fields.len() + 1);
let mut seen_message = false;
let mut labels: BTreeSet<String> = BTreeSet::new();
for (idx, f) in variant.fields.iter().enumerate() {
// Honor field.optional (extractor-unwrapped form) in addition to
// TypeRef::Optional(inner) — both encode "nullable" in the IR.
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 mut label = super::enums::swift_associated_label(&f.name, idx);
// Disambiguate duplicate labels by suffixing the index.
while labels.contains(&label) {
label = format!("{label}{idx}");
}
labels.insert(label.clone());
if label == "message" {
seen_message = true;
}
assoc.push(format!("{label}: {ty_with_opt}"));
}
if !seen_message {
assoc.insert(0, "message: String".to_string());
}
out.push_str(&crate::backends::swift::template_env::render(
"error_case_with_data.jinja",
minijinja::context! {
case_name => &case_name,
associated_values => assoc.join(", "),
},
));
}
}
out.push_str("}\n");
// Emit a public extension with computed properties for each whitelisted
// introspection method (e.g. `status_code`, `is_transient`, `error_type`).
// Each property switches over `self` and delegates to the per-variant
// associated values or returns a sensible default when the variant carries
// no such field. Backends that wire a swift-bridge free function can
// replace these stubs in a subsequent code-generation pass.
if !error.methods.is_empty() {
out.push('\n');
let mut properties = String::new();
for method in &error.methods {
let prop_name = swift_case_ident(&method.name.to_lower_camel_case());
let return_ty = super::overloads::swift_type_name(&method.return_type);
let default_val = swift_default_for_type(&method.return_type);
let mut cases = String::new();
for variant in &error.variants {
let case_name = swift_case_ident(&variant.name.to_lower_camel_case());
// Check whether this variant carries an associated value whose
// name matches the method (e.g. `status_code` ↔ `status`).
let field_match = variant.fields.iter().find(|f| {
let camel = f.name.to_lower_camel_case();
let prop_snake = method.name.as_str();
// Exact match or common abbreviation (status_code → status).
camel == prop_name
|| f.name == prop_snake
|| (prop_snake == "status_code" && (f.name == "status" || camel == "status"))
});
let wildcard = if variant.is_unit || variant.fields.is_empty() {
String::new()
} else {
let mut args: Vec<String> = variant
.fields
.iter()
.enumerate()
.map(|(i, f)| {
let label = super::enums::swift_associated_label(&f.name, i);
if let Some(fm) = &field_match {
if fm.name == f.name {
return format!("{label}: let matched");
}
}
format!("{label}: _")
})
.collect();
// The case declaration above synthesizes a leading
// `message: String` parameter when none of the original
// fields is named `message`. The switch pattern must
// include the same synthetic label or the tuple lengths
// will mismatch.
let has_message_field = variant.fields.iter().any(|f| f.name == "message");
if !has_message_field {
args.insert(0, "message: _".to_string());
}
format!("({})", args.join(", "))
};
let ret_expr = if field_match.is_some() && !variant.is_unit && !variant.fields.is_empty() {
"matched".to_string()
} else {
default_val.clone()
};
cases.push_str(&crate::backends::swift::template_env::render(
"swift_error_property_case.swift.jinja",
minijinja::context! {
case_name => &case_name,
wildcard => &wildcard,
return_expression => &ret_expr,
},
));
}
properties.push_str(&crate::backends::swift::template_env::render(
"swift_error_property.swift.jinja",
minijinja::context! {
property_name => &prop_name,
return_type => &return_ty,
cases => cases,
},
));
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_error_extension.swift.jinja",
minijinja::context! {
name => &name,
properties => properties,
},
));
}
}
/// Returns the Swift zero/default literal for a given `TypeRef`.
pub(super) fn swift_default_for_type(ty: &TypeRef) -> String {
match ty {
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "false".to_string(),
_ => "0".to_string(),
}
}
TypeRef::String => "\"\"".to_string(),
TypeRef::Optional(_) => "nil".to_string(),
_ => "nil".to_string(),
}
}