use std::borrow::Cow;
use specta::{
Types,
datatype::{DataType, Enum, Fields, GenericReference, Primitive, Reference, Variant},
};
use crate::error::{Error, Result};
use crate::swift::Swift;
fn enum_string_raw_value(variant: &Variant) -> Option<&str> {
let Fields::Unnamed(fields) = variant.fields() else {
return None;
};
let [field] = fields.fields() else {
return None;
};
let DataType::Enum(literal_enum) = field.ty()? else {
return None;
};
let [(raw_value, literal_variant)] = literal_enum.variants() else {
return None;
};
matches!(literal_variant.fields(), Fields::Unit).then_some(raw_value.as_ref())
}
fn resolved_string_enum(e: &Enum) -> Option<Vec<(&str, &str)>> {
e.variants()
.iter()
.map(|(variant_name, variant)| {
enum_string_raw_value(variant).map(|raw| (variant_name.as_ref(), raw))
})
.collect()
}
pub fn export_type(
swift: &Swift,
types: &Types,
ndt: &specta::datatype::NamedDataType,
) -> Result<String> {
if !matches!(ndt.ty(), DataType::Struct(_) | DataType::Enum(_)) {
return Ok(String::new());
}
let mut result = String::new();
if !ndt.docs().is_empty() {
let docs = ndt.docs();
for line in docs.lines() {
result.push_str("/// ");
result.push_str(line.trim_start());
result.push('\n');
}
}
if let Some(deprecated) = ndt.deprecated() {
let message = deprecated
.note()
.filter(|note| !note.trim().is_empty())
.map(ToString::to_string)
.unwrap_or_else(|| "This type is deprecated".to_string());
result.push_str(&format!(
"@available(*, deprecated, message: \"{}\")\n",
message
));
}
let generic_scope = ndt.generics().to_vec();
match ndt.ty() {
DataType::Struct(_) => {
let type_def =
datatype_to_swift(swift, types, ndt.ty(), generic_scope.clone(), false, None)?;
let name = swift.naming.convert(ndt.name());
let generics = if ndt.generics().is_empty() {
String::new()
} else {
format!(
"<{}>",
ndt.generics()
.iter()
.map(|(_, g)| g.as_ref().to_string())
.collect::<Vec<_>>()
.join(", ")
)
};
result.push_str(&format!("public struct {}{}: Codable {{\n", name, generics));
result.push_str(&type_def);
result.push('}');
}
DataType::Enum(e) => {
let name = swift.naming.convert(ndt.name());
let generics = if ndt.generics().is_empty() {
String::new()
} else {
format!(
"<{}>",
ndt.generics()
.iter()
.map(|(_, g)| g.as_ref().to_string())
.collect::<Vec<_>>()
.join(", ")
)
};
let is_string_enum_val = resolved_string_enum(e).is_some();
let has_struct_variants = e.variants().iter().any(|(_, variant)| {
matches!(variant.fields(), specta::datatype::Fields::Named(fields) if !fields.fields().is_empty())
});
let protocols = if is_string_enum_val {
if has_struct_variants {
"String" } else {
"String, Codable"
}
} else if has_struct_variants {
"" } else {
"Codable"
};
let protocol_part = if protocols.is_empty() {
String::new()
} else {
format!(": {}", protocols)
};
result.push_str(&format!(
"public enum {}{}{} {{\n",
name, generics, protocol_part
));
let enum_body = enum_to_swift(
swift,
types,
e,
generic_scope.clone(),
false,
None,
Some(&name),
)?;
result.push_str(&enum_body);
result.push('}');
let struct_definitions =
generate_enum_structs(swift, types, e, generic_scope, false, None, &name)?;
result.push_str(&struct_definitions);
if has_struct_variants {
let codable_impl = generate_enum_codable_impl(swift, e, &name)?;
result.push_str(&codable_impl);
}
}
_ => {
return Ok(String::new());
}
}
Ok(result)
}
pub fn datatype_to_swift(
swift: &Swift,
types: &Types,
dt: &DataType,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
is_export: bool,
reference: Option<&specta::datatype::Reference>,
) -> Result<String> {
if let Some(special_type) = is_special_std_type(types, reference) {
return Ok(special_type);
}
match dt {
DataType::Primitive(p) => primitive_to_swift(p),
DataType::List(l) => list_to_swift(swift, types, l, generic_scope.clone()),
DataType::Map(m) => map_to_swift(swift, types, m, generic_scope.clone()),
DataType::Nullable(def) => {
let inner = datatype_to_swift(swift, types, def, generic_scope, is_export, None)?;
Ok(match swift.optionals {
crate::swift::OptionalStyle::QuestionMark => format!("{}?", inner),
crate::swift::OptionalStyle::Optional => format!("Optional<{}>", inner),
})
}
DataType::Struct(s) => {
if is_duration_struct(s) {
return Ok("RustDuration".to_string());
}
struct_to_swift(swift, types, s, generic_scope, is_export, None)
}
DataType::Enum(e) => enum_to_swift(swift, types, e, generic_scope, is_export, None, None),
DataType::Tuple(t) => tuple_to_swift(swift, types, t, generic_scope.clone()),
DataType::Reference(r) => reference_to_swift(swift, types, r, &generic_scope),
}
}
pub fn is_duration_struct(s: &specta::datatype::Struct) -> bool {
match s.fields() {
specta::datatype::Fields::Named(fields) => {
let field_names: Vec<String> = fields
.fields()
.iter()
.map(|(name, _)| name.to_string())
.collect();
field_names.len() == 2
&& field_names.contains(&"secs".to_string())
&& field_names.contains(&"nanos".to_string())
}
_ => false,
}
}
fn is_special_std_type(
types: &Types,
reference: Option<&specta::datatype::Reference>,
) -> Option<String> {
if let Some(Reference::Named(r)) = reference
&& let Some(ndt) = r.get(types)
{
if ndt.name() == "Duration" {
return Some("RustDuration".to_string());
}
if ndt.name() == "SystemTime" {
return Some("Date".to_string());
}
}
None
}
fn primitive_to_swift(primitive: &Primitive) -> Result<String> {
Ok(match primitive {
Primitive::i8 => "Int8".to_string(),
Primitive::i16 => "Int16".to_string(),
Primitive::i32 => "Int32".to_string(),
Primitive::i64 => "Int64".to_string(),
Primitive::isize => "Int".to_string(),
Primitive::u8 => "UInt8".to_string(),
Primitive::u16 => "UInt16".to_string(),
Primitive::u32 => "UInt32".to_string(),
Primitive::u64 => "UInt64".to_string(),
Primitive::usize => "UInt".to_string(),
Primitive::f32 => "Float".to_string(),
Primitive::f64 => "Double".to_string(),
Primitive::bool => "Bool".to_string(),
Primitive::char => "Character".to_string(),
Primitive::str => "String".to_string(),
Primitive::i128 | Primitive::u128 => {
return Err(Error::UnsupportedType(
"Swift does not support 128-bit integers".to_string(),
));
}
Primitive::f16 => {
return Err(Error::UnsupportedType(
"Swift does not support f16".to_string(),
));
}
Primitive::f128 => {
return Err(Error::UnsupportedType(
"Swift does not support f128".to_string(),
));
}
})
}
fn list_to_swift(
swift: &Swift,
types: &Types,
list: &specta::datatype::List,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
) -> Result<String> {
let element_type = datatype_to_swift(swift, types, list.ty(), generic_scope, false, None)?;
Ok(format!("[{}]", element_type))
}
fn map_to_swift(
swift: &Swift,
types: &Types,
map: &specta::datatype::Map,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
) -> Result<String> {
let key_type = datatype_to_swift(
swift,
types,
map.key_ty(),
generic_scope.clone(),
false,
None,
)?;
let value_type = datatype_to_swift(swift, types, map.value_ty(), generic_scope, false, None)?;
Ok(format!("[{}: {}]", key_type, value_type))
}
fn struct_to_swift(
swift: &Swift,
types: &Types,
s: &specta::datatype::Struct,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
is_export: bool,
_reference: Option<&specta::datatype::Reference>,
) -> Result<String> {
match s.fields() {
specta::datatype::Fields::Unit => Ok("Void".to_string()),
specta::datatype::Fields::Unnamed(fields) => {
if fields.fields().is_empty() {
Ok("Void".to_string())
} else if fields.fields().len() == 1 {
let field_type = datatype_to_swift(
swift,
types,
fields.fields()[0]
.ty()
.expect("tuple field should have a type"),
generic_scope,
is_export,
None,
)?;
Ok(format!(" let value: {}\n", field_type))
} else {
let mut result = String::new();
for (i, field) in fields.fields().iter().enumerate() {
let field_type = datatype_to_swift(
swift,
types,
field.ty().expect("tuple field should have a type"),
generic_scope.clone(),
is_export,
None,
)?;
result.push_str(&format!(" public let field{}: {}\n", i, field_type));
}
Ok(result)
}
}
specta::datatype::Fields::Named(fields) => {
let mut result = String::new();
let mut field_mappings = Vec::new();
for (original_field_name, field) in fields.fields() {
let field_type = if let Some(ty) = field.ty() {
datatype_to_swift(swift, types, ty, generic_scope.clone(), is_export, None)?
} else {
continue;
};
let optional_marker = if field.optional() { "?" } else { "" };
let swift_field_name = swift.naming.convert_field(original_field_name);
result.push_str(&format!(
" public let {}: {}{}\n",
swift_field_name, field_type, optional_marker
));
field_mappings.push((swift_field_name, original_field_name.to_string()));
}
let needs_custom_coding_keys = field_mappings
.iter()
.any(|(swift_name, rust_name)| swift_name != rust_name);
if needs_custom_coding_keys {
result.push_str("\n private enum CodingKeys: String, CodingKey {\n");
for (swift_name, rust_name) in &field_mappings {
result.push_str(&format!(
" case {} = \"{}\"\n",
swift_name, rust_name
));
}
result.push_str(" }\n");
}
Ok(result)
}
}
}
fn enum_to_swift(
swift: &Swift,
types: &Types,
e: &specta::datatype::Enum,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
is_export: bool,
_reference: Option<&specta::datatype::Reference>,
enum_name: Option<&str>,
) -> Result<String> {
let mut result = String::new();
let is_string_enum = resolved_string_enum(e).is_some();
for (original_variant_name, variant) in e.variants() {
if variant.skip() {
continue;
}
let variant_name = swift.naming.convert_enum_case(original_variant_name);
match variant.fields() {
specta::datatype::Fields::Unit => {
if is_string_enum {
let raw_value = enum_string_raw_value(variant)
.expect("string enum variants should have string literal payloads");
result.push_str(&format!(" case {} = \"{}\"\n", variant_name, raw_value));
} else {
result.push_str(&format!(" case {}\n", variant_name));
}
}
specta::datatype::Fields::Unnamed(fields) => {
if is_string_enum && let Some(raw_value) = enum_string_raw_value(variant) {
result.push_str(&format!(" case {} = \"{}\"\n", variant_name, raw_value));
} else if fields.fields().is_empty() {
result.push_str(&format!(" case {}\n", variant_name));
} else {
let types_str = fields
.fields()
.iter()
.map(|f| {
datatype_to_swift(
swift,
types,
f.ty().expect("enum variant field should have a type"),
generic_scope.clone(),
is_export,
None,
)
})
.collect::<std::result::Result<Vec<_>, _>>()?
.join(", ");
result.push_str(&format!(" case {}({})\n", variant_name, types_str));
}
}
specta::datatype::Fields::Named(fields) => {
if fields.fields().is_empty() {
result.push_str(&format!(" case {}\n", variant_name));
} else {
let pascal_variant_name = to_pascal_case(original_variant_name);
let struct_name = if let Some(enum_name) = enum_name {
format!("{}{}Data", enum_name, pascal_variant_name)
} else {
format!("{}Data", pascal_variant_name)
};
result.push_str(&format!(" case {}({})\n", variant_name, struct_name));
}
}
}
}
Ok(result)
}
fn generate_enum_structs(
swift: &Swift,
types: &Types,
e: &specta::datatype::Enum,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
is_export: bool,
_reference: Option<&specta::datatype::Reference>,
enum_name: &str,
) -> Result<String> {
let mut result = String::new();
for (original_variant_name, variant) in e.variants() {
if variant.skip() {
continue;
}
if let specta::datatype::Fields::Named(fields) = variant.fields()
&& !fields.fields().is_empty()
{
let pascal_variant_name = to_pascal_case(original_variant_name);
let struct_name = format!("{}{}Data", enum_name, pascal_variant_name);
result.push_str(&format!("\npublic struct {}: Codable {{\n", struct_name));
let mut field_mappings = Vec::new();
for (original_field_name, field) in fields.fields() {
if let Some(ty) = field.ty() {
let field_type = datatype_to_swift(
swift,
types,
ty,
generic_scope.clone(),
is_export,
None,
)?;
let optional_marker = if field.optional() { "?" } else { "" };
let swift_field_name = swift.naming.convert_field(original_field_name);
result.push_str(&format!(
" public let {}: {}{}\n",
swift_field_name, field_type, optional_marker
));
field_mappings.push((swift_field_name, original_field_name.to_string()));
}
}
let needs_custom_coding_keys = field_mappings
.iter()
.any(|(swift_name, rust_name)| swift_name != rust_name);
if needs_custom_coding_keys {
result.push_str("\n private enum CodingKeys: String, CodingKey {\n");
for (swift_name, rust_name) in &field_mappings {
result.push_str(&format!(
" case {} = \"{}\"\n",
swift_name, rust_name
));
}
result.push_str(" }\n");
}
result.push_str("}\n");
}
}
Ok(result)
}
fn to_pascal_case(s: &str) -> String {
if s.chars().next().is_some_and(|c| c.is_uppercase()) {
return s.to_string();
}
let mut result = String::new();
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' {
capitalize_next = true;
} else if capitalize_next {
result.push(c.to_uppercase().next().unwrap_or(c));
capitalize_next = false;
} else {
result.push(c.to_lowercase().next().unwrap_or(c));
}
}
result
}
fn tuple_to_swift(
swift: &Swift,
types: &Types,
t: &specta::datatype::Tuple,
generic_scope: Vec<(GenericReference, Cow<'static, str>)>,
) -> Result<String> {
if t.elements().is_empty() {
Ok("Void".to_string())
} else if t.elements().len() == 1 {
datatype_to_swift(swift, types, &t.elements()[0], generic_scope, false, None)
} else {
let types_str = t
.elements()
.iter()
.map(|e| datatype_to_swift(swift, types, e, generic_scope.clone(), false, None))
.collect::<std::result::Result<Vec<_>, _>>()?
.join(", ");
Ok(format!("({})", types_str))
}
}
fn reference_to_swift(
swift: &Swift,
types: &Types,
r: &specta::datatype::Reference,
generic_scope: &[(GenericReference, Cow<'static, str>)],
) -> Result<String> {
match r {
Reference::Named(r) => {
let Some(ndt) = r.get(types) else {
return Err(Error::InvalidIdentifier(
"Reference to unknown type".to_string(),
));
};
if ndt.name() == "String" {
return Ok("String".to_string());
}
if matches!(ndt.name().as_ref(), "Uuid" | "DateTime" | "NaiveDateTime") {
return Ok("String".to_string());
}
if ndt.name() == "Vec"
&& let Some((_, inner_ty)) = r.generics().first()
{
let inner =
datatype_to_swift(swift, types, inner_ty, generic_scope.to_vec(), false, None)?;
return Ok(format!("[{inner}]"));
}
let name = swift.naming.convert(ndt.name());
if r.generics().is_empty() {
Ok(name)
} else {
let generics = r
.generics()
.iter()
.map(|(_, t)| {
datatype_to_swift(swift, types, t, generic_scope.to_vec(), false, None)
})
.collect::<std::result::Result<Vec<_>, _>>()?
.join(", ");
Ok(format!("{}<{}>", name, generics))
}
}
Reference::Opaque(_) => Err(Error::UnsupportedType(
"Opaque references are not supported by Swift exporter".to_string(),
)),
Reference::Generic(g) => generic_to_swift(swift, g, generic_scope),
}
}
fn generic_to_swift(
_swift: &Swift,
g: &specta::datatype::GenericReference,
generic_scope: &[(GenericReference, Cow<'static, str>)],
) -> Result<String> {
generic_scope
.iter()
.find_map(|(candidate, name)| (candidate == g).then(|| name.to_string()))
.ok_or_else(|| Error::GenericConstraint(format!("Unresolved generic reference: {g:?}")))
}
fn generate_enum_codable_impl(
swift: &Swift,
e: &specta::datatype::Enum,
enum_name: &str,
) -> Result<String> {
let mut result = String::new();
result.push_str(&format!(
"\n// MARK: - {} Codable Implementation\n",
enum_name
));
result.push_str(&format!("extension {}: Codable {{\n", enum_name));
result.push_str(" private enum CodingKeys: String, CodingKey {\n");
for (original_variant_name, variant) in e.variants() {
if variant.skip() {
continue;
}
let swift_case_name = swift.naming.convert_enum_case(original_variant_name);
result.push_str(&format!(
" case {} = \"{}\"\n",
swift_case_name, original_variant_name
));
}
result.push_str(" }\n\n");
result.push_str(" public init(from decoder: Decoder) throws {\n");
result.push_str(" let container = try decoder.container(keyedBy: CodingKeys.self)\n");
result.push_str(" \n");
result.push_str(" if container.allKeys.count != 1 {\n");
result.push_str(" throw DecodingError.dataCorrupted(\n");
result.push_str(" DecodingError.Context(codingPath: decoder.codingPath, debugDescription: \"Invalid number of keys found, expected one.\")\n");
result.push_str(" )\n");
result.push_str(" }\n\n");
result.push_str(" let key = container.allKeys.first!\n");
result.push_str(" switch key {\n");
for (original_variant_name, variant) in e.variants() {
if variant.skip() {
continue;
}
let swift_case_name = swift.naming.convert_enum_case(original_variant_name);
match variant.fields() {
specta::datatype::Fields::Unit => {
result.push_str(&format!(" case .{}:\n", swift_case_name));
result.push_str(&format!(" self = .{}\n", swift_case_name));
}
specta::datatype::Fields::Unnamed(fields) => {
if fields.fields().is_empty() {
result.push_str(&format!(" case .{}:\n", swift_case_name));
result.push_str(&format!(" self = .{}\n", swift_case_name));
} else {
result.push_str(&format!(" case .{}:\n", swift_case_name));
result.push_str(&format!(
" // TODO: Implement tuple variant decoding for {}\n",
swift_case_name
));
result.push_str(
" fatalError(\"Tuple variant decoding not implemented\")\n",
);
}
}
specta::datatype::Fields::Named(_) => {
let pascal_variant_name = to_pascal_case(original_variant_name);
let struct_name = format!("{}{}Data", enum_name, pascal_variant_name);
result.push_str(&format!(" case .{}:\n", swift_case_name));
result.push_str(&format!(
" let data = try container.decode({}.self, forKey: .{})\n",
struct_name, swift_case_name
));
result.push_str(&format!(" self = .{}(data)\n", swift_case_name));
}
}
}
result.push_str(" }\n");
result.push_str(" }\n\n");
result.push_str(" public func encode(to encoder: Encoder) throws {\n");
result.push_str(" var container = encoder.container(keyedBy: CodingKeys.self)\n");
result.push_str(" \n");
result.push_str(" switch self {\n");
for (original_variant_name, variant) in e.variants() {
if variant.skip() {
continue;
}
let swift_case_name = swift.naming.convert_enum_case(original_variant_name);
match variant.fields() {
specta::datatype::Fields::Unit => {
result.push_str(&format!(" case .{}:\n", swift_case_name));
result.push_str(&format!(
" try container.encodeNil(forKey: .{})\n",
swift_case_name
));
}
specta::datatype::Fields::Unnamed(_) => {
result.push_str(&format!(" case .{}:\n", swift_case_name));
result.push_str(&format!(
" // TODO: Implement tuple variant encoding for {}\n",
swift_case_name
));
result.push_str(
" fatalError(\"Tuple variant encoding not implemented\")\n",
);
}
specta::datatype::Fields::Named(_) => {
result.push_str(&format!(" case .{}(let data):\n", swift_case_name));
result.push_str(&format!(
" try container.encode(data, forKey: .{})\n",
swift_case_name
));
}
}
}
result.push_str(" }\n");
result.push_str(" }\n");
result.push_str("}\n");
Ok(result)
}