use super::{csharp_file_header, is_tuple_field};
use crate::type_map::csharp_type;
use alef_codegen::naming::to_csharp_name;
use alef_core::ir::EnumDef;
use heck::ToPascalCase;
pub(super) fn apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
match rename_all {
Some("snake_case") => name.to_snake_case(),
Some("camelCase") => name.to_lower_camel_case(),
Some("PascalCase") => name.to_pascal_case(),
Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
Some("lowercase") => name.to_lowercase(),
Some("UPPERCASE") => name.to_uppercase(),
_ => name.to_lowercase(),
}
}
pub(super) fn gen_enum(enum_def: &EnumDef, namespace: &str) -> String {
let mut out = csharp_file_header();
out.push_str("using System.Text.Json.Serialization;\n\n");
let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
if enum_def.serde_tag.is_some() && has_data_variants {
return gen_tagged_union(enum_def, namespace);
}
let needs_custom_converter = enum_def.variants.iter().any(|v| {
if let Some(ref rename) = v.serde_rename {
let snake = apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref());
rename != &snake
} else {
false
}
});
let enum_pascal = enum_def.name.to_pascal_case();
let variants: Vec<(String, String)> = enum_def
.variants
.iter()
.map(|v| {
let json_name = v
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
let pascal_name = v.name.to_pascal_case();
(json_name, pascal_name)
})
.collect();
out.push_str("using System;\n");
out.push_str("using System.Text.Json;\n\n");
out.push_str(&format!("namespace {};\n\n", namespace));
if !enum_def.doc.is_empty() {
out.push_str("/// <summary>\n");
for line in enum_def.doc.lines() {
out.push_str(&format!("/// {}\n", line));
}
out.push_str("/// </summary>\n");
}
if needs_custom_converter {
out.push_str(&format!("[JsonConverter(typeof({enum_pascal}JsonConverter))]\n"));
}
out.push_str(&format!("public enum {enum_pascal}\n"));
out.push_str("{\n");
for (json_name, pascal_name) in &variants {
if let Some(v) = enum_def
.variants
.iter()
.find(|v| v.name.to_pascal_case() == *pascal_name)
{
if !v.doc.is_empty() {
out.push_str(" /// <summary>\n");
for line in v.doc.lines() {
out.push_str(&format!(" /// {}\n", line));
}
out.push_str(" /// </summary>\n");
}
}
out.push_str(&format!(" [JsonPropertyName(\"{json_name}\")]\n"));
out.push_str(&format!(" {pascal_name},\n"));
}
out.push_str("}\n");
if needs_custom_converter {
out.push('\n');
out.push_str(&format!(
"/// <summary>Custom JSON converter for <see cref=\"{enum_pascal}\"/> that respects explicit variant names.</summary>\n"
));
out.push_str(&format!(
"internal sealed class {enum_pascal}JsonConverter : JsonConverter<{enum_pascal}>\n"
));
out.push_str("{\n");
out.push_str(&format!(
" public override {enum_pascal} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\n"
));
out.push_str(" {\n");
out.push_str(" var value = reader.GetString();\n");
out.push_str(" return value switch\n");
out.push_str(" {\n");
for (json_name, pascal_name) in &variants {
out.push_str(&format!(
" \"{json_name}\" => {enum_pascal}.{pascal_name},\n"
));
}
out.push_str(&format!(
" _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
));
out.push_str(" };\n");
out.push_str(" }\n\n");
out.push_str(&format!(
" public override void Write(Utf8JsonWriter writer, {enum_pascal} value, JsonSerializerOptions options)\n"
));
out.push_str(" {\n");
out.push_str(" var str = value switch\n");
out.push_str(" {\n");
for (json_name, pascal_name) in &variants {
out.push_str(&format!(
" {enum_pascal}.{pascal_name} => \"{json_name}\",\n"
));
}
out.push_str(&format!(
" _ => throw new JsonException($\"Unknown {enum_pascal} value: {{value}}\")\n"
));
out.push_str(" };\n");
out.push_str(" writer.WriteStringValue(str);\n");
out.push_str(" }\n");
out.push_str("}\n");
}
out
}
fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
let enum_pascal = enum_def.name.to_pascal_case();
let ns = namespace;
let mut out = csharp_file_header();
out.push_str("using System.Text.Json.Serialization;\n\n");
out.push_str(&format!("namespace {};\n\n", namespace));
let variant_names: std::collections::HashSet<String> =
enum_def.variants.iter().map(|v| v.name.to_pascal_case()).collect();
let discriminators: Vec<(String, String)> = enum_def
.variants
.iter()
.map(|v| {
let pascal = v.name.to_pascal_case();
let disc = v
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
(pascal, disc)
})
.collect();
if !enum_def.doc.is_empty() {
out.push_str("/// <summary>\n");
for line in enum_def.doc.lines() {
out.push_str(&format!("/// {}\n", line));
}
out.push_str("/// </summary>\n");
}
out.push_str(&format!(
"[JsonPolymorphic(TypeDiscriminatorPropertyName = \"{tag_field}\")]\n"
));
for (pascal, discriminator) in &discriminators {
out.push_str(&format!("[JsonDerivedType(typeof({pascal}), \"{discriminator}\")]\n"));
}
out.push_str(&format!("public abstract record {enum_pascal}\n"));
out.push_str("{\n");
for variant in &enum_def.variants {
let pascal = variant.name.to_pascal_case();
let discriminator = discriminators
.iter()
.find(|(p, _)| p == &pascal)
.map(|(_, d)| d.as_str())
.unwrap_or("");
if !variant.doc.is_empty() {
out.push_str(" /// <summary>\n");
for line in variant.doc.lines() {
out.push_str(&format!(" /// {}\n", line));
}
out.push_str(" /// </summary>\n");
}
let _ = discriminator;
if variant.fields.is_empty() {
out.push_str(&format!(" public sealed record {pascal}() : {enum_pascal};\n\n"));
} else {
let is_copy_ctor_clash = variant.fields.len() == 1 && {
let field_cs_type = csharp_type(&variant.fields[0].ty);
field_cs_type.as_ref() == pascal
};
if is_copy_ctor_clash {
let cs_type = csharp_type(&variant.fields[0].ty);
let qualified_cs_type = format!("global::{ns}.{cs_type}");
out.push_str(&format!(" public sealed record {pascal} : {enum_pascal}\n"));
out.push_str(" {\n");
out.push_str(&format!(
" public required {qualified_cs_type} Value {{ get; init; }}\n"
));
out.push_str(" }\n\n");
} else {
out.push_str(&format!(" public sealed record {pascal}(\n"));
for (i, field) in variant.fields.iter().enumerate() {
let cs_type = csharp_type(&field.ty);
let cs_type = if field.optional && !cs_type.ends_with('?') {
format!("{cs_type}?")
} else {
cs_type.to_string()
};
let cs_type = if variant_names.iter().any(|vn| cs_type.starts_with(&format!("{vn}<"))) {
cs_type
.replace("List<", "global::System.Collections.Generic.List<")
.replace("Dictionary<", "global::System.Collections.Generic.Dictionary<")
} else {
cs_type
};
let comma = if i < variant.fields.len() - 1 { "," } else { "" };
if is_tuple_field(field) {
out.push_str(&format!(" {cs_type} Value{comma}\n"));
} else {
let json_name = field.name.trim_start_matches('_');
let cs_name = to_csharp_name(json_name);
let clashes = cs_name == pascal || cs_name == cs_type || variant_names.contains(&cs_name);
if clashes {
out.push_str(&format!(
" [property: JsonPropertyName(\"{json_name}\")] {cs_type} Value{comma}\n"
));
} else {
out.push_str(&format!(
" [property: JsonPropertyName(\"{json_name}\")] {cs_type} {cs_name}{comma}\n"
));
}
}
}
out.push_str(&format!(" ) : {enum_pascal};\n\n"));
}
}
}
out.push_str("}\n");
out
}