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::{ToKebabCase, 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("kebab-case") => name.to_kebab_case(),
Some("SCREAMING-KEBAB-CASE") => name.to_kebab_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 {
use crate::template_env::render;
use minijinja::Value;
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);
}
if enum_def.serde_untagged && has_data_variants {
return gen_untagged_wrapper(enum_def, namespace);
}
let rename_all_differs = matches!(
enum_def.serde_rename_all.as_deref(),
Some("kebab-case") | Some("SCREAMING-KEBAB-CASE") | Some("camelCase") | Some("PascalCase")
);
let needs_custom_converter = rename_all_differs
|| 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 variant_list: 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();
let variants: Vec<Value> = 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();
let doc_lines: Vec<String> = if !v.doc.is_empty() {
v.doc.lines().map(|l| l.to_string()).collect()
} else {
vec![]
};
Value::from_serialize(serde_json::json!({
"json_name": json_name,
"pascal_name": pascal_name,
"doc": !v.doc.is_empty(),
"doc_lines": doc_lines,
}))
})
.collect();
let doc_lines: Vec<String> = if !enum_def.doc.is_empty() {
enum_def.doc.lines().map(|l| l.to_string()).collect()
} else {
vec![]
};
let mut out = render(
"enum_header.jinja",
Value::from_serialize(serde_json::json!({
"namespace": namespace,
"enum_pascal": enum_pascal,
"needs_custom_converter": needs_custom_converter,
"doc": !enum_def.doc.is_empty(),
"doc_lines": doc_lines,
"variants": variants,
})),
);
out.push('\n');
if needs_custom_converter {
out.push_str(&render(
"enum_custom_converter.jinja",
Value::from_serialize(serde_json::json!({
"enum_pascal": enum_pascal,
"variants": variant_list.iter().map(|(json_name, pascal_name)| {
serde_json::json!({
"json_name": json_name,
"pascal_name": pascal_name,
})
}).collect::<Vec<_>>(),
})),
));
}
out
}
fn gen_tagged_union(enum_def: &EnumDef, namespace: &str) -> String {
use crate::template_env::render;
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;\n");
out.push_str("using System.IO;\n");
out.push_str("using System.Text.Json;\n");
out.push_str("using System.Text.Json.Serialization;\n\n");
out.push_str(&render("namespace_decl.jinja", minijinja::context! { namespace }));
out.push('\n');
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(&render("doc_line.jinja", minijinja::context! { line }));
}
out.push_str("/// </summary>\n");
}
out.push_str(&render(
"json_converter_attr.jinja",
minijinja::context! { base => enum_pascal },
));
out.push_str(&render(
"abstract_record_header.jinja",
minijinja::context! { enum_pascal },
));
out.push_str("{\n");
for variant in &enum_def.variants {
let pascal = variant.name.to_pascal_case();
if !variant.doc.is_empty() {
out.push_str(" /// <summary>\n");
for line in variant.doc.lines() {
out.push_str(&render("doc_line_indented.jinja", minijinja::context! { line }));
}
out.push_str(" /// </summary>\n");
}
if variant.fields.is_empty() {
out.push_str(&render(
"unit_variant_record.jinja",
minijinja::context! { pascal, enum_pascal },
));
out.push('\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(&render(
"variant_record_body_header.jinja",
minijinja::context! { pascal, enum_pascal },
));
out.push_str(" {\n");
out.push_str(&render(
"required_value_property.jinja",
minijinja::context! { qualified_cs_type },
));
out.push_str(" }\n\n");
} else {
out.push_str(&render(
"variant_record_params_header.jinja",
minijinja::context! { pascal },
));
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(&render(
"variant_field_tuple.jinja",
minijinja::context! { cs_type, comma },
));
} 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(&render(
"variant_field_json_value.jinja",
minijinja::context! { json_name, cs_type, comma },
));
} else {
out.push_str(&render(
"variant_field_json_named.jinja",
minijinja::context! { json_name, cs_type, cs_name, comma },
));
}
}
}
out.push_str(&render(
"variant_record_close.jinja",
minijinja::context! { enum_pascal },
));
out.push('\n');
}
}
}
for variant in &enum_def.variants {
if variant.fields.len() != 1 || !is_tuple_field(&variant.fields[0]) {
continue;
}
let pascal = variant.name.to_pascal_case();
let return_type = csharp_type(&variant.fields[0].ty);
let return_type_nullable = format!("{return_type}?");
out.push_str(&render(
"variant_accessor_summary.jinja",
minijinja::context! { pascal },
));
out.push_str(&render(
"variant_accessor_property.jinja",
minijinja::context! { pascal, return_type_nullable },
));
out.push('\n');
}
out.push_str("}\n");
out.push('\n');
gen_sealed_union_converter(&mut out, namespace, enum_def, tag_field);
out
}
fn gen_sealed_union_converter(out: &mut String, _namespace: &str, enum_def: &EnumDef, tag_field: &str) {
use crate::template_env::render;
use minijinja::Value;
let class_name = enum_def.name.to_pascal_case();
let variants: Vec<Value> = enum_def
.variants
.iter()
.map(|v| {
let pascal = v.name.to_pascal_case();
let discriminator = v
.serde_rename
.clone()
.unwrap_or_else(|| apply_rename_all(&v.name, enum_def.serde_rename_all.as_deref()));
let is_unit = v.fields.is_empty();
let is_tuple = !is_unit && v.fields.len() == 1 && is_tuple_field(&v.fields[0]);
Value::from_serialize(serde_json::json!({
"pascal": pascal,
"pascal_lower": pascal.to_lowercase(),
"discriminator": discriminator,
"is_unit": is_unit,
"is_tuple": is_tuple,
}))
})
.collect();
out.push_str(&render(
"sealed_union_converter.jinja",
Value::from_serialize(serde_json::json!({
"class_name": class_name,
"tag_field": tag_field,
"variants": variants,
})),
));
}
fn gen_untagged_wrapper(enum_def: &EnumDef, namespace: &str) -> String {
use crate::template_env::render;
use minijinja::Value;
let class_name = enum_def.name.to_pascal_case();
let doc_lines: Vec<String> = if !enum_def.doc.is_empty() {
enum_def.doc.lines().map(|l| l.to_string()).collect()
} else {
vec![]
};
render(
"untagged_union_wrapper.jinja",
Value::from_serialize(serde_json::json!({
"namespace": namespace,
"class_name": class_name,
"doc": !enum_def.doc.is_empty(),
"doc_lines": doc_lines,
})),
)
}