use std::collections::HashMap;
use crate::dto::schema::{
Field, FieldType, PrimitiveType, SchemaNode, field_is_optional, node_uses_json,
};
use crate::dto::support::{NameRegistry, collect_types, field_identifier, swift_string_literal};
use crate::dto::{DtoError, DtoLanguage};
pub(in crate::dto) fn render_swift(schema: &SchemaNode, name: &str) -> Result<String, DtoError> {
let mut registry = NameRegistry::new(name);
let mut defs = Vec::new();
collect_types(schema, Vec::new(), &mut registry, &mut defs);
let uses_json = node_uses_json(schema);
let mut out = String::new();
for def in defs {
out.push_str(&format!("struct {}: Codable {{\n", def.name));
let mut used = HashMap::new();
let mut coding_keys = Vec::new();
for field in &def.node.fields {
let ident = field_identifier(DtoLanguage::Swift, &field.key, &mut used);
let rename = ident != field.key;
let optional = field_is_optional(field);
let field_type = swift_type_for_field(field, &def.path, ®istry, optional);
out.push_str(&format!(" let {}: {}\n", ident, field_type));
if rename {
coding_keys.push(format!(
" case {} = {}",
ident,
swift_string_literal(&field.key)
));
}
}
if !coding_keys.is_empty() {
out.push('\n');
out.push_str(" enum CodingKeys: String, CodingKey {\n");
for line in coding_keys {
out.push_str(&format!("{}\n", line));
}
out.push_str(" }\n");
}
out.push_str("}\n\n");
}
if uses_json {
out.push_str(SWIFT_JSON_VALUE);
out.push('\n');
}
Ok(out.trim_end().to_string())
}
fn swift_type_for_field(
field: &Field,
parent_path: &[String],
registry: &NameRegistry,
optional: bool,
) -> String {
let mut path = parent_path.to_vec();
path.push(field.key.clone());
let base = swift_type_for_type(&field.field_type, &path, registry);
if optional && !base.ends_with('?') {
format!("{}?", base)
} else {
base
}
}
fn swift_type_for_type(field_type: &FieldType, path: &[String], registry: &NameRegistry) -> String {
match field_type {
FieldType::Primitive(PrimitiveType::String) => "String".to_string(),
FieldType::Primitive(PrimitiveType::Int) => "Int".to_string(),
FieldType::Primitive(PrimitiveType::Float) => "Double".to_string(),
FieldType::Primitive(PrimitiveType::Bool) => "Bool".to_string(),
FieldType::Array(inner) => format!("[{}]", swift_type_for_type(inner, path, registry)),
FieldType::Map(inner) => {
format!("[String: {}]", swift_type_for_type(inner, path, registry))
}
FieldType::Nullable(inner) => {
let inner_type = swift_type_for_type(inner, path, registry);
if inner_type.ends_with('?') {
inner_type
} else {
format!("{}?", inner_type)
}
}
FieldType::JsonValue => "JSONValue".to_string(),
FieldType::Object(_) => registry
.get(path)
.cloned()
.unwrap_or_else(|| "Record".to_string()),
}
}
const SWIFT_JSON_VALUE: &str = "enum JSONValue: Codable {\n case string(String)\n case number(Double)\n case bool(Bool)\n case object([String: JSONValue])\n case array([JSONValue])\n case null\n\n init(from decoder: Decoder) throws {\n let container = try decoder.singleValueContainer()\n if container.decodeNil() {\n self = .null\n } else if let value = try? container.decode(Bool.self) {\n self = .bool(value)\n } else if let value = try? container.decode(Double.self) {\n self = .number(value)\n } else if let value = try? container.decode(String.self) {\n self = .string(value)\n } else if let value = try? container.decode([String: JSONValue].self) {\n self = .object(value)\n } else if let value = try? container.decode([JSONValue].self) {\n self = .array(value)\n } else {\n throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: \"Unsupported JSON value\"))\n }\n }\n\n func encode(to encoder: Encoder) throws {\n var container = encoder.singleValueContainer()\n switch self {\n case .string(let value):\n try container.encode(value)\n case .number(let value):\n try container.encode(value)\n case .bool(let value):\n try container.encode(value)\n case .object(let value):\n try container.encode(value)\n case .array(let value):\n try container.encode(value)\n case .null:\n try container.encodeNil()\n }\n }\n}\n";