use crate::backends::swift::naming::{swift_rust_shim_ident as swift_ident, swift_source_ident as swift_case_ident};
use crate::backends::swift::type_map::SwiftMapper;
use crate::codegen::shared::binding_fields;
use crate::codegen::type_mapper::TypeMapper;
use crate::core::ir::{DefaultValue, FieldDef, MethodDef, PrimitiveType, TypeDef, TypeRef};
use heck::{AsSnakeCase, ToLowerCamelCase, ToSnakeCase};
use std::collections::HashSet;
pub(super) fn can_emit_first_class_struct(
ty: &TypeDef,
_mapper: &SwiftMapper,
_exclude_fields: &HashSet<String>,
known_dto_names: &HashSet<String>,
) -> bool {
!ty.is_opaque
&& ty.has_serde
&& !ty.fields.is_empty()
&& binding_fields(&ty.fields).all(|field| first_class_field_supported(&field.ty, known_dto_names))
&& !ty.fields.iter().all(|f| f.binding_excluded)
}
pub(super) fn first_class_field_supported(ty: &TypeRef, known_dto_names: &HashSet<String>) -> bool {
match ty {
TypeRef::Primitive(_) | TypeRef::String => true,
TypeRef::Named(name) => known_dto_names.contains(name),
TypeRef::Vec(inner) => first_class_field_supported(inner, known_dto_names),
TypeRef::Optional(inner) => first_class_field_supported(inner, known_dto_names),
_ => false,
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_first_class_struct(
ty: &TypeDef,
mapper: &SwiftMapper,
exclude_fields: &HashSet<String>,
known_dto_names: &HashSet<String>,
unit_enum_names: &HashSet<String>,
untagged_enum_names: &HashSet<String>,
error_type_name: &str,
out: &mut String,
) {
let type_name = &ty.name;
let type_snake = AsSnakeCase(type_name.as_str()).to_string();
let visible_fields: Vec<_> = binding_fields(&ty.fields).collect();
let mut properties = String::new();
for field in &visible_fields {
super::client::emit_doc_comment(&field.doc, " ", &mut properties);
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let swift_ty = mapper.map_type(&field.ty);
let property_type = if field.optional && !already_optional {
format!("{swift_ty}?")
} else {
swift_ty
};
properties.push_str(&crate::backends::swift::template_env::render(
"swift_struct_property.swift.jinja",
minijinja::context! {
name => camel,
ty => property_type,
},
));
}
let params: Vec<String> = visible_fields
.iter()
.map(|field| {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let swift_ty = mapper.map_type(&field.ty);
if field.optional && !already_optional {
format!("{camel}: {swift_ty}? = nil")
} else if already_optional {
format!("{camel}: {swift_ty} = nil")
} else {
format!("{camel}: {swift_ty}")
}
})
.collect();
let init_params = params.join(", ");
let mut init_assignments = String::new();
for field in &visible_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
init_assignments.push_str(&crate::backends::swift::template_env::render(
"swift_self_assignment.swift.jinja",
minijinja::context! {
field => camel,
expr => camel,
},
));
}
let needs_coding_keys = ty.has_default
|| visible_fields.iter().any(|field| {
let camel = field.name.to_lower_camel_case();
camel != field.name
});
let mut coding_keys = String::new();
if needs_coding_keys {
for field in &visible_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let wire_key = &field.name;
coding_keys.push_str(&crate::backends::swift::template_env::render(
"swift_coding_key.swift.jinja",
minijinja::context! {
name => camel,
wire_key => wire_key,
},
));
}
}
let mut decoder_init = String::new();
if ty.has_default {
emit_decoder_init(mapper, &visible_fields, &mut decoder_init);
}
let mut ffi_init_assignments = String::new();
for field in &visible_fields {
let swift_field = swift_case_ident(&field.name.to_lower_camel_case());
let rust_accessor = swift_ident(&field.name.to_lower_camel_case());
let is_optional = field.optional || matches!(&field.ty, TypeRef::Optional(_));
let expr = if is_field_unbridgeable_for_init(ty, field, exclude_fields, known_dto_names) {
if is_optional {
"nil".to_string()
} else {
let swift_ty = mapper.map_type(&field.ty);
format!("try JSONDecoder().decode({swift_ty}.self, from: Data(\"null\".utf8))")
}
} else if is_untagged_enum_type(&field.ty, untagged_enum_names) {
let swift_ty = mapper.map_type(&field.ty);
let swift_ty_with_opt = if is_optional && !matches!(&field.ty, TypeRef::Optional(_)) {
format!("{swift_ty}?")
} else {
swift_ty
};
let accessor_with_chain = if is_optional {
format!("rb.{rust_accessor}()?.toString() ?? \"null\"")
} else {
format!("rb.{rust_accessor}().toString()")
};
format!(
"try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
(({accessor_with_chain}).data(using: .utf8) ?? Data(\"null\".utf8)))"
)
} else if needs_json_bridge_for_swift(&field.ty) {
let swift_ty = mapper.map_type(&field.ty);
let swift_ty_with_opt = if is_optional && !matches!(&field.ty, TypeRef::Optional(_)) {
format!("{swift_ty}?")
} else {
swift_ty
};
let accessor_with_chain = format!("rb.{rust_accessor}().toString()");
format!(
"try JSONDecoder().decode({swift_ty_with_opt}.self, from: \
(({accessor_with_chain}).data(using: .utf8) ?? Data(\"null\".utf8)))"
)
} else {
swift_ffi_read_expr(
&field.ty,
is_optional,
&rust_accessor,
known_dto_names,
unit_enum_names,
untagged_enum_names,
error_type_name,
)
};
ffi_init_assignments.push_str(&crate::backends::swift::template_env::render(
"swift_self_assignment.swift.jinja",
minijinja::context! {
field => swift_field,
expr => expr,
},
));
}
let mut into_rust_body = String::new();
let direct_call = emit_into_rust_direct_call(ty, mapper, exclude_fields, type_name);
match direct_call {
Some(call) => into_rust_body.push_str(&call),
None => {
let from_json_fn = format!("{type_snake}_from_json").to_lower_camel_case();
into_rust_body.push_str(" let data = try JSONEncoder().encode(self)\n");
into_rust_body.push_str(" let json = String(data: data, encoding: .utf8) ?? \"{}\"\n");
into_rust_body.push_str(&crate::backends::swift::template_env::render(
"swift_into_rust_json_return.swift.jinja",
minijinja::context! {
from_json_fn => from_json_fn,
},
));
}
}
let (instance_methods, _static_methods) = crate::codegen::shared::partition_methods(&ty.methods);
let mut methods_source = String::new();
for method in instance_methods {
if method.sanitized || method.is_static {
continue;
}
emit_instance_method_for_first_class_struct(method, type_name, mapper, &mut methods_source);
}
out.push_str(&crate::backends::swift::template_env::render(
"first_class_struct.swift.jinja",
minijinja::context! {
type_name => type_name,
properties => properties,
init_params => init_params,
init_assignments => init_assignments,
coding_keys => coding_keys,
decoder_init => decoder_init,
ffi_init_assignments => ffi_init_assignments,
into_rust_body => into_rust_body,
methods => methods_source,
},
));
}
pub(super) fn swift_typed_default_literal(dv: &DefaultValue) -> Option<String> {
match dv {
DefaultValue::BoolLiteral(true) => Some("true".to_string()),
DefaultValue::BoolLiteral(false) => Some("false".to_string()),
DefaultValue::IntLiteral(n) => Some(n.to_string()),
DefaultValue::FloatLiteral(f) => {
if f.is_nan() || f.is_infinite() {
None
} else {
let s = if f.fract() == 0.0 {
format!("{f:.1}")
} else {
f.to_string()
};
Some(s)
}
}
DefaultValue::StringLiteral(s) => {
let mut escaped = String::with_capacity(s.len() + 2);
escaped.push('"');
for ch in s.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c => escaped.push(c),
}
}
escaped.push('"');
Some(escaped)
}
DefaultValue::EnumVariant(_) => None,
DefaultValue::Empty | DefaultValue::None => None,
}
}
pub(super) fn swift_type_based_default(ty: &TypeRef) -> Option<String> {
match ty {
TypeRef::Primitive(prim) => match prim {
PrimitiveType::Bool => Some("false".to_string()),
PrimitiveType::U8
| PrimitiveType::I8
| PrimitiveType::U16
| PrimitiveType::I16
| PrimitiveType::U32
| PrimitiveType::I32
| PrimitiveType::U64
| PrimitiveType::I64
| PrimitiveType::Usize
| PrimitiveType::Isize => Some("0".to_string()),
PrimitiveType::F32 | PrimitiveType::F64 => Some("0".to_string()),
},
TypeRef::String => Some("\"\"".to_string()),
TypeRef::Vec(_) => Some("[]".to_string()),
TypeRef::Map(_, _) => Some("[:]".to_string()),
TypeRef::Optional(_) => Some("nil".to_string()),
_ => None,
}
}
pub(super) fn emit_decoder_init(mapper: &SwiftMapper, visible_fields: &[&FieldDef], out: &mut String) {
out.push_str(" public init(from decoder: any Decoder) throws {\n");
out.push_str(" let container = try decoder.container(keyedBy: CodingKeys.self)\n");
for field in visible_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let already_optional = matches!(&field.ty, TypeRef::Optional(_));
let is_optional = field.optional || already_optional;
let swift_ty = mapper.map_type(&field.ty);
if is_optional {
let inner_ty = swift_ty.strip_suffix('?').unwrap_or(&swift_ty);
out.push_str(&crate::backends::swift::template_env::render(
"swift_decode_optional_assignment.swift.jinja",
minijinja::context! {
field => camel,
ty => inner_ty,
},
));
continue;
}
let fallback = field
.typed_default
.as_ref()
.and_then(swift_typed_default_literal)
.or_else(|| swift_type_based_default(&field.ty));
match fallback {
Some(fb) => {
out.push_str(&crate::backends::swift::template_env::render(
"swift_decode_default_assignment.swift.jinja",
minijinja::context! {
field => camel,
ty => swift_ty,
fallback => fb,
},
));
}
None => {
out.push_str(&crate::backends::swift::template_env::render(
"swift_decode_required_assignment.swift.jinja",
minijinja::context! {
field => camel,
ty => swift_ty,
},
));
}
}
}
out.push_str(" }\n");
}
pub(super) fn emit_into_rust_direct_call(
ty: &TypeDef,
_mapper: &SwiftMapper,
exclude_fields: &HashSet<String>,
type_name: &str,
) -> Option<String> {
use crate::backends::swift::gen_rust_crate::extern_block::{constructor_fields, has_constructor_extern};
if !has_constructor_extern(ty, exclude_fields) {
return None;
}
let ctor_fields = constructor_fields(ty, exclude_fields);
let visible_count = ty.fields.iter().filter(|f| !f.binding_excluded).count();
if ctor_fields.len() != visible_count {
return None;
}
let mut prelude = String::new();
let mut args: Vec<String> = Vec::with_capacity(ctor_fields.len());
for field in &ctor_fields {
let camel = swift_case_ident(&field.name.to_lower_camel_case());
let local = format!("__{}", field.name.to_lower_camel_case());
match field_intorust_arg(&field.ty, field.optional, &camel, &local) {
Some(FieldArg::Direct(expr)) => args.push(expr),
Some(FieldArg::WithPrelude { prelude: p, arg }) => {
prelude.push_str(&p);
args.push(arg);
}
None => return None,
}
}
let mut body = String::new();
body.push_str(&prelude);
body.push_str(&crate::backends::swift::template_env::render(
"swift_bridge_constructor_return.swift.jinja",
minijinja::context! {
type_name => type_name,
args => args.join(", "),
},
));
Some(body)
}
pub(super) enum FieldArg {
Direct(String),
WithPrelude { prelude: String, arg: String },
}
pub(super) fn field_intorust_arg(
ty: &TypeRef,
field_optional: bool,
self_property: &str,
local: &str,
) -> Option<FieldArg> {
let (inner_ty, is_optional) = match ty {
TypeRef::Optional(inner) => (inner.as_ref(), true),
_ => (ty, field_optional),
};
match inner_ty {
TypeRef::Primitive(_) => Some(FieldArg::Direct(format!("self.{self_property}"))),
TypeRef::String if !is_optional => Some(FieldArg::Direct(format!("RustString(self.{self_property})"))),
TypeRef::String => Some(FieldArg::Direct(format!("self.{self_property}.map(RustString.init)"))),
TypeRef::Named(_) if !is_optional => Some(FieldArg::Direct(format!("try self.{self_property}.intoRust()"))),
TypeRef::Vec(elem) if !is_optional => emit_vec_arg(elem, self_property, local),
_ => None,
}
}
pub(super) fn emit_vec_arg(elem: &TypeRef, self_property: &str, local: &str) -> Option<FieldArg> {
let (rust_vec_param, elem_expr): (String, String) = match elem {
TypeRef::Primitive(prim) => {
let swift_prim = SwiftMapper.primitive(prim).into_owned();
(swift_prim, "__elem".to_string())
}
TypeRef::String => ("RustString".to_string(), "RustString(__elem)".to_string()),
TypeRef::Named(name) => (format!("RustBridge.{name}"), "try __elem.intoRust()".to_string()),
_ => return None,
};
let prelude = format!(
" let {local} = RustVec<{rust_vec_param}>()\n \
for __elem in self.{self_property} {{ {local}.push(value: {elem_expr}) }}\n",
);
Some(FieldArg::WithPrelude {
prelude,
arg: local.to_string(),
})
}
pub(super) fn swift_ffi_read_expr(
ty: &TypeRef,
field_optional: bool,
accessor: &str,
known_dto_names: &HashSet<String>,
unit_enum_names: &HashSet<String>,
untagged_enum_names: &HashSet<String>,
error_type_name: &str,
) -> String {
let opt = field_optional && !matches!(ty, TypeRef::Optional(_));
match ty {
TypeRef::String if opt => format!("rb.{accessor}()?.toString()"),
TypeRef::String => format!("rb.{accessor}().toString()"),
TypeRef::Named(name) if unit_enum_names.contains(name) && opt => {
format!("rb.{accessor}().flatMap {{ {name}(rawValue: $0.toString()) }}")
}
TypeRef::Named(name) if unit_enum_names.contains(name) => {
format!(
"try {{ let rawValue = rb.{accessor}().toString(); \
guard let value = {name}(rawValue: rawValue) else {{ \
throw {error_type_name}.validation(message: \"Unknown {name} variant\", source: rawValue) \
}}; return value }}()"
)
}
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) && opt => {
format!("try rb.{accessor}().map {{ try {name}($0) }}")
}
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
format!("try {name}(rb.{accessor}())")
}
TypeRef::Vec(inner) if opt => {
match inner.as_ref() {
TypeRef::Primitive(_) => format!("rb.{accessor}().map {{ Array($0) }}"),
TypeRef::String => format!("rb.{accessor}()?.map {{ $0.as_str().toString() }}"),
TypeRef::Named(name) if untagged_enum_names.contains(name) => {
format!(
"try rb.{accessor}()?.map {{ (s: RustStringRef) -> {name} in \
let d = s.as_str().toString().data(using: .utf8) ?? Data(); \
return try JSONDecoder().decode({name}.self, from: d) }}"
)
}
TypeRef::Named(name) if known_dto_names.contains(name) => {
format!("try rb.{accessor}()?.map {{ try {name}($0) }}")
}
_ => {
let map_expr = vec_elem_convert_expr(inner, known_dto_names);
format!("rb.{accessor}()?.map {{ {map_expr} }}")
}
}
}
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Primitive(_) => format!("Array(rb.{accessor}())"),
TypeRef::String => format!("rb.{accessor}().map {{ $0.as_str().toString() }}"),
TypeRef::Named(name) if untagged_enum_names.contains(name) => {
format!(
"try rb.{accessor}().map {{ (s: RustStringRef) -> {name} in \
let d = s.as_str().toString().data(using: .utf8) ?? Data(); \
return try JSONDecoder().decode({name}.self, from: d) }}"
)
}
TypeRef::Named(name) if known_dto_names.contains(name) => {
format!("try rb.{accessor}().map {{ try {name}($0) }}")
}
_ => format!("rb.{accessor}()"),
},
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::String => format!("rb.{accessor}()?.toString()"),
TypeRef::Named(name) if unit_enum_names.contains(name) => {
format!("rb.{accessor}().flatMap {{ {name}(rawValue: $0.toString()) }}")
}
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
format!("try rb.{accessor}().map {{ try {name}($0) }}")
}
TypeRef::Vec(elem) => match elem.as_ref() {
TypeRef::String => format!("rb.{accessor}()?.map {{ $0.as_str().toString() }}"),
TypeRef::Named(name) if known_dto_names.contains(name) && !untagged_enum_names.contains(name) => {
format!("try rb.{accessor}()?.map {{ try {name}($0) }}")
}
TypeRef::Primitive(_) => format!("rb.{accessor}().map {{ Array($0) }}"),
_ => format!("rb.{accessor}()"),
},
_ => format!("rb.{accessor}()"),
},
_ => format!("rb.{accessor}()"),
}
}
pub(super) fn vec_elem_convert_expr(inner: &TypeRef, known_dto_names: &HashSet<String>) -> String {
match inner {
TypeRef::String => "$0.as_str().toString()".to_string(),
TypeRef::Named(name) if known_dto_names.contains(name) => format!("try {name}($0)"),
TypeRef::Primitive(_) => "$0".to_string(),
_ => "$0".to_string(),
}
}
fn is_untagged_enum_type(ty: &TypeRef, untagged_enum_names: &HashSet<String>) -> bool {
match ty {
TypeRef::Named(n) => untagged_enum_names.contains(n),
TypeRef::Optional(inner) => is_untagged_enum_type(inner, untagged_enum_names),
_ => false,
}
}
fn needs_json_bridge_for_swift(ty: &TypeRef) -> bool {
fn is_leaf(ty: &TypeRef) -> bool {
matches!(
ty,
TypeRef::Primitive(_)
| TypeRef::String
| TypeRef::Char
| TypeRef::Path
| TypeRef::Json
| TypeRef::Unit
| TypeRef::Duration
| TypeRef::Bytes
| TypeRef::Named(_),
)
}
match ty {
TypeRef::Map(_, _) => true,
TypeRef::Vec(inner) => !is_leaf(inner),
TypeRef::Optional(inner) => needs_json_bridge_for_swift(inner),
_ => false,
}
}
fn is_field_unbridgeable_for_init(
ty: &TypeDef,
field: &FieldDef,
exclude_fields: &HashSet<String>,
known_dto_names: &HashSet<String>,
) -> bool {
let name = field.name.to_snake_case();
let field_key = format!("{}.{}", ty.name, name);
if field.binding_excluded || exclude_fields.contains(&field_key) {
return true;
}
if let TypeRef::Vec(inner) = &field.ty
&& field.sanitized
&& !matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::Bytes)
{
return true;
}
if !ty.has_serde
&& let TypeRef::Vec(inner) = &field.ty
&& !matches!(inner.as_ref(), TypeRef::Primitive(_) | TypeRef::Bytes)
{
return true;
}
if needs_json_bridge_for_swift(&field.ty) {
let inner_named = match &field.ty {
TypeRef::Optional(inner) | TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(n) => Some(n.as_str()),
_ => None,
},
TypeRef::Named(n) => Some(n.as_str()),
_ => None,
};
if let Some(n) = inner_named
&& !known_dto_names.contains(n)
{
return true;
}
}
false
}
fn emit_instance_method_for_first_class_struct(
method: &MethodDef,
type_name: &str,
mapper: &SwiftMapper,
out: &mut String,
) {
if method.is_static {
return;
}
let method_name = swift_case_ident(&method.name.to_lower_camel_case());
let extern_fn_name = format!("{}_{}", AsSnakeCase(type_name), method.name.to_snake_case());
let mut param_strs: Vec<String> = Vec::new();
for param in &method.params {
if param.sanitized {
continue;
}
let param_name = swift_ident(¶m.name.to_snake_case());
let param_type = mapper.map_type(¶m.ty);
param_strs.push(format!("{param_name}: {param_type}"));
}
let param_list = param_strs.join(", ");
let return_type = mapper.map_type(&method.return_type);
let method_sig = if param_list.is_empty() {
format!(" public func {method_name}() -> {return_type}")
} else {
format!(" public func {method_name}({param_list}) -> {return_type}")
};
out.push_str(&method_sig);
out.push_str(" {\n");
out.push_str(" fatalError(\"Not yet implemented: ");
out.push_str(&extern_fn_name);
out.push_str("\")\n");
out.push_str(" }\n\n");
}