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, ErrorVariant, FieldDef, PrimitiveType, TypeRef};
use heck::ToLowerCamelCase;
use std::collections::BTreeSet;
pub(super) fn swift_error_name(error: &ErrorDef, module_name: &str) -> String {
if error.name == "Error" {
format!("{module_name}Error")
} else {
error.name.clone()
}
}
fn has_status_code_method(error: &ErrorDef) -> bool {
error.methods.iter().any(|method| {
method.name == "status_code"
&& !method.is_static
&& !method.is_async
&& method.params.is_empty()
&& matches!(method.return_type, TypeRef::Primitive(PrimitiveType::U16))
})
}
fn is_status_field(field: &FieldDef) -> bool {
field.name == "status" || field.name == "status_code" || field.name.to_lower_camel_case() == "statusCode"
}
fn swift_view(error: &ErrorDef) -> ErrorDef {
let mut view = error.clone();
let has_status = has_status_code_method(error);
for variant in &mut view.variants {
let has_source = variant.has_source;
variant.fields.retain_mut(|field| {
if !field.binding_excluded {
return true;
}
if has_source
&& field.name == "source"
&& (field.optional || matches!(field.ty, TypeRef::Optional(_)))
&& !field.sensitive
{
field.ty = TypeRef::String;
field.optional = true;
return true;
}
false
});
if !has_status || variant.fields.iter().any(is_status_field) {
continue;
}
variant.is_unit = false;
variant.fields.push(FieldDef {
name: "status".to_string(),
ty: TypeRef::Primitive(PrimitiveType::U16),
..FieldDef::default()
});
}
view
}
struct AssociatedValue<'a> {
label: String,
swift_type: String,
field: Option<&'a FieldDef>,
}
fn associated_values<'a>(variant: &'a ErrorVariant, mapper: &SwiftMapper) -> Vec<AssociatedValue<'a>> {
let mut values: Vec<AssociatedValue<'a>> = 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() {
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);
while labels.contains(&label) {
label = format!("{label}{idx}");
}
labels.insert(label.clone());
if label == "message" {
seen_message = true;
}
values.push(AssociatedValue {
label,
swift_type: ty_with_opt,
field: Some(f),
});
}
if !seen_message {
values.insert(
0,
AssociatedValue {
label: "message".to_string(),
swift_type: "String".to_string(),
field: None,
},
);
}
values
}
pub(super) fn emit_error(error: &ErrorDef, module_name: &str, out: &mut String, mapper: &SwiftMapper) {
let name = swift_error_name(error, module_name);
let error = &swift_view(error);
super::client::emit_doc_comment(&error.doc, "", out);
out.push_str(&crate::backends::swift::template_env::render(
"error_enum_header.jinja",
crate::alef_context! {
name => &name,
},
));
for variant in &error.variants {
super::client::emit_doc_comment(&variant.doc, " ", out);
let case_name = super::enums::swift_variant_case(&variant.name);
if variant.is_unit || variant.fields.is_empty() {
out.push_str(&crate::backends::swift::template_env::render(
"error_case.jinja",
crate::alef_context! {
case_name => &case_name,
},
));
} else {
let assoc: Vec<String> = associated_values(variant, mapper)
.iter()
.map(|value| format!("{}: {}", value.label, value.swift_type))
.collect();
out.push_str(&crate::backends::swift::template_env::render(
"error_case_with_data.jinja",
crate::alef_context! {
case_name => &case_name,
associated_values => assoc.join(", "),
},
));
}
}
let has_validation_variant = error
.variants
.iter()
.any(|v| super::enums::swift_variant_case(&v.name) == "validation");
if !has_validation_variant {
out.push_str(" /// Synthetic case raised when a Rust serde unit-enum raw value cannot\n");
out.push_str(" /// be mapped to a known Swift case during DTO unmarshaling.\n");
out.push_str(" case validation(message: String, source: String)\n");
}
out.push_str("}\n");
description::emit(error, &name, mapper, out);
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 = super::enums::swift_variant_case(&variant.name);
let field_match = variant.fields.iter().find(|f| {
let camel = f.name.to_lower_camel_case();
let prop_snake = method.name.as_str();
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
&& fm.name == f.name
{
return format!("{label}: let matched");
}
format!("{label}: _")
})
.collect();
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",
crate::alef_context! {
case_name => &case_name,
wildcard => &wildcard,
return_expression => &ret_expr,
},
));
}
if !has_validation_variant {
cases.push_str(&crate::backends::swift::template_env::render(
"swift_error_property_case.swift.jinja",
crate::alef_context! {
case_name => "validation",
wildcard => "(message: _, source: _)",
return_expression => &default_val,
},
));
}
properties.push_str(&crate::backends::swift::template_env::render(
"swift_error_property.swift.jinja",
crate::alef_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",
crate::alef_context! {
name => &name,
properties => properties,
},
));
}
}
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(),
}
}
pub(crate) fn bridge_payload_supported(ty: &TypeRef) -> bool {
match ty {
TypeRef::Primitive(_) | TypeRef::String | TypeRef::Path | TypeRef::Char => true,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => bridge_payload_supported(inner),
TypeRef::Map(key, value) => matches!(key.as_ref(), TypeRef::String) && bridge_payload_supported(value),
_ => false,
}
}
fn bridge_value_expression(value: &AssociatedValue<'_>, error: &ErrorDef) -> Option<String> {
let Some(field) = value.field else {
return Some("message".to_string());
};
if field.binding_excluded {
return Some("nil".to_string());
}
if is_status_field(field) && has_status_code_method(error) {
let (inner, optional) = match &field.ty {
TypeRef::Optional(inner) => (inner.as_ref(), true),
ty => (ty, field.optional),
};
if !matches!(inner, TypeRef::Primitive(p) if !matches!(p, PrimitiveType::Bool | PrimitiveType::F32 | PrimitiveType::F64))
{
return None;
}
let integer = SwiftMapper.map_type(inner);
return Some(if optional {
format!("status < 0 ? nil : {integer}(clamping: status)")
} else {
format!("{integer}(clamping: status)")
});
}
if field.name == "retry_after"
&& error.methods.iter().any(|method| {
method.name == "retry_after" && !method.is_static && !method.is_async && method.params.is_empty()
})
{
match crate::codegen::error_gen::duration_shape(&field.ty) {
Some(crate::codegen::error_gen::DurationShape::Optional) => {
return Some("retryAfterMs < 0 ? nil : Duration.milliseconds(retryAfterMs)".to_string());
}
Some(crate::codegen::error_gen::DurationShape::Bare) if field.optional => {
return Some("retryAfterMs < 0 ? nil : Duration.milliseconds(retryAfterMs)".to_string());
}
Some(crate::codegen::error_gen::DurationShape::Bare) => {
return Some("Duration.milliseconds(max(retryAfterMs, 0))".to_string());
}
None => {}
}
}
bridge_payload_supported(&field.ty).then(|| {
let key = serde_json::to_string(&field.name).expect("field names are strings");
format!(
"try alefBridgeErrorField(error, {key}, as: ({}).self)",
value.swift_type
)
})
}
fn bridge_case_expression(variant: &ErrorVariant, mapper: &SwiftMapper, error: &ErrorDef) -> Option<String> {
let case_name = super::enums::swift_variant_case(&variant.name);
if variant.is_unit || variant.fields.is_empty() {
return Some(format!(".{case_name}"));
}
let mut arguments: Vec<String> = Vec::new();
for value in associated_values(variant, mapper) {
arguments.push(format!("{}: {}", value.label, bridge_value_expression(&value, error)?));
}
Some(format!(".{case_name}({})", arguments.join(", ")))
}
pub(crate) fn emit_bridge_conversions(errors: &[ErrorDef], module_name: &str, out: &mut String, mapper: &SwiftMapper) {
#[derive(serde::Serialize)]
struct ErrorName {
rust: String,
swift: String,
}
let converted: Vec<&ErrorDef> = errors.iter().filter(|error| !error.binding_excluded).collect();
let mut names: Vec<ErrorName> = Vec::new();
for error in &converted {
let name = swift_error_name(error, module_name);
let view = swift_view(error);
let declared_validation = view
.variants
.iter()
.find(|v| super::enums::swift_variant_case(&v.name) == "validation");
#[derive(serde::Serialize)]
struct BridgeCase {
name: String,
case_name: String,
expression: Option<String>,
}
let cases: Vec<BridgeCase> = view
.variants
.iter()
.map(|variant| BridgeCase {
name: variant.name.clone(),
case_name: super::enums::swift_variant_case(&variant.name),
expression: bridge_case_expression(variant, mapper, error),
})
.collect();
out.push_str(&crate::backends::swift::template_env::render(
"swift_error_bridge_conversion.swift.jinja",
crate::alef_context! {
name => &name,
has_throwing_case => cases.iter().any(|case| case.expression.as_deref().is_some_and(|expression| expression.contains("try "))),
cases => &cases,
has_validation_variant => declared_validation.is_some(),
has_kind_method => error.methods.iter().any(|method| method.name == "kind"),
},
));
out.push('\n');
names.push(ErrorName {
rust: error.name.clone(),
swift: name,
});
}
out.push_str(&crate::backends::swift::template_env::render(
"swift_error_bridge_helpers.swift.jinja",
crate::alef_context! {
errors => &names,
},
));
out.push('\n');
}
pub(super) fn emit_errors(errors: &[ErrorDef], module_name: &str, out: &mut String, mapper: &SwiftMapper) {
for error in errors {
emit_error(error, module_name, out, mapper);
out.push('\n');
}
emit_bridge_conversions(errors, module_name, out, mapper);
}
#[cfg(test)]
#[path = "errors_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "errors_duration_tests.rs"]
mod duration_tests;
#[path = "errors_description.rs"]
mod description;