use crate::core::ir::{ErrorDef, ErrorVariant, TypeRef};
use std::collections::HashSet;
pub(crate) fn variant_dispatch_prefix(variant: &ErrorVariant) -> Option<String> {
let template = variant.message_template.as_deref()?;
let prefix_end = template.find('{').unwrap_or(template.len());
let prefix = template[..prefix_end].trim_end().to_string();
if prefix.is_empty() { None } else { Some(prefix) }
}
pub(super) fn compute_variant_dispatch(errors: &[ErrorDef]) -> (bool, String, Vec<String>) {
if errors.is_empty() {
return (false, String::new(), Vec::new());
}
let base_exception_class = format!("{}Exception", errors[0].name);
let mut seen: HashSet<String> = HashSet::new();
let mut variants_with_prefix: Vec<(String, String)> = Vec::new();
for error in errors {
for variant in &error.variants {
let Some(prefix) = variant_dispatch_prefix(variant) else {
continue;
};
let class_name = format!("{}Exception", variant.name);
if seen.insert(class_name.clone()) {
variants_with_prefix.push((class_name, prefix));
}
}
}
variants_with_prefix.sort_by_key(|item| std::cmp::Reverse(item.1.len()));
let dispatch_lines = variants_with_prefix
.into_iter()
.map(|(class, prefix)| {
let escaped_prefix = prefix.replace('\\', "\\\\").replace('"', "\\\"");
format!(" if (message.StartsWith(\"{escaped_prefix}\")) return new {class}(message);")
})
.collect();
(true, base_exception_class, dispatch_lines)
}
pub(super) fn gen_exception_class(namespace: &str, class_name: &str, errors: &[ErrorDef]) -> String {
use crate::backends::csharp::template_env::render;
use minijinja::Value;
let (has_base_error, base_exception_class, variant_dispatch_lines) = compute_variant_dispatch(errors);
render(
"exception_class.jinja",
Value::from_serialize(serde_json::json!({
"namespace": namespace,
"class_name": class_name,
"has_base_error": has_base_error,
"base_exception_class": base_exception_class,
"variant_dispatch_lines": variant_dispatch_lines,
})),
)
}
pub(super) fn compute_handle_returned_types(api: &crate::core::ir::ApiSurface) -> HashSet<String> {
fn inner_named(ty: &crate::core::ir::TypeRef) -> Option<&str> {
match ty {
crate::core::ir::TypeRef::Named(n) => Some(n.as_str()),
crate::core::ir::TypeRef::Optional(inner) | crate::core::ir::TypeRef::Vec(inner) => inner_named(inner),
_ => None,
}
}
let mut type_def_map = std::collections::HashMap::new();
for typ in &api.types {
type_def_map.insert(typ.name.clone(), typ);
}
let mut handle_types = HashSet::new();
for func in &api.functions {
if let Some(name) = inner_named(&func.return_type)
&& let Some(type_def) = type_def_map.get(name)
&& !type_def.has_serde
{
handle_types.insert(name.to_string());
}
}
for typ in &api.types {
for method in &typ.methods {
if let Some(name) = inner_named(&method.return_type)
&& let Some(type_def) = type_def_map.get(name)
&& !type_def.has_serde
{
handle_types.insert(name.to_string());
}
}
}
handle_types
}
pub(super) fn emit_return_statement(out: &mut String, return_type: &TypeRef) {
emit_return_statement_indented(out, return_type, " ");
}
pub(super) fn emit_return_marshalling_indented(
out: &mut String,
return_type: &TypeRef,
indent: &str,
enum_names: &HashSet<String>,
true_opaque_types: &HashSet<String>,
handle_returned_types: &HashSet<String>,
enum_data_variant_names: &HashSet<String>,
) {
use super::{returns_bool_via_int, returns_json_object, returns_string};
use crate::backends::csharp::template_env::render;
use crate::backends::csharp::type_map::csharp_type;
use crate::codegen::naming::csharp_type_name;
if *return_type == TypeRef::Unit {
return;
}
if returns_string(return_type) {
out.push_str(&render("return_string_utf8.jinja", minijinja::context! { indent }));
out.push_str(&render("free_native_string.jinja", minijinja::context! { indent }));
} else if returns_bool_via_int(return_type) {
out.push_str(&render("return_bool_from_int.jinja", minijinja::context! { indent }));
} else if let TypeRef::Named(type_name) = return_type {
let pascal = csharp_type_name(type_name);
if true_opaque_types.contains(type_name)
|| true_opaque_types.contains(&pascal)
|| handle_returned_types.contains(type_name)
|| handle_returned_types.contains(&pascal)
{
out.push_str(&render(
"return_opaque_ctor.jinja",
minijinja::context! { indent, pascal },
));
} else if !enum_names.contains(&pascal) || enum_data_variant_names.contains(&pascal) {
let to_json_method = format!("{pascal}ToJson");
let free_method = format!("{pascal}Free");
let cs_ty = csharp_type(return_type);
out.push_str(&render(
"native_to_json_ptr.jinja",
minijinja::context! { indent, to_json_method },
));
out.push_str(&render(
"json_from_ptr.jinja",
minijinja::context! { indent, ptr_var => "jsonPtr" },
));
out.push_str(&render(
"free_string_ptr.jinja",
minijinja::context! { indent, ptr_var => "jsonPtr" },
));
out.push_str(&render(
"free_native_handle.jinja",
minijinja::context! { indent, free_method },
));
out.push_str(&render(
"deserialize_json.jinja",
minijinja::context! { indent, cs_type => cs_ty },
));
} else {
let cs_ty = csharp_type(return_type);
out.push_str(&render(
"json_from_ptr.jinja",
minijinja::context! { indent, ptr_var => "nativeResult" },
));
out.push_str(&render(
"free_string_ptr.jinja",
minijinja::context! { indent, ptr_var => "nativeResult" },
));
out.push_str(&render(
"deserialize_json.jinja",
minijinja::context! { indent, cs_type => cs_ty },
));
}
} else if returns_json_object(return_type) {
if let TypeRef::Optional(inner) = return_type {
if returns_string(inner) {
out.push_str(&render("return_ptr_as_string.jinja", minijinja::context! { indent }));
out.push_str(&render("free_native_string.jinja", minijinja::context! { indent }));
return;
}
if let TypeRef::Named(type_name) = inner.as_ref() {
let pascal = csharp_type_name(type_name);
if true_opaque_types.contains(type_name)
|| true_opaque_types.contains(&pascal)
|| handle_returned_types.contains(type_name)
|| handle_returned_types.contains(&pascal)
{
out.push_str(&render(
"return_opaque_ctor.jinja",
minijinja::context! { indent, pascal },
));
return;
}
let to_json_method = format!("{pascal}ToJson");
let free_method = format!("{pascal}Free");
let cs_ty = csharp_type(return_type);
out.push_str(&render(
"native_to_json_ptr.jinja",
minijinja::context! { indent, to_json_method },
));
out.push_str(&render(
"json_from_ptr.jinja",
minijinja::context! { indent, ptr_var => "jsonPtr" },
));
out.push_str(&render(
"free_string_ptr.jinja",
minijinja::context! { indent, ptr_var => "jsonPtr" },
));
out.push_str(&render(
"free_native_handle.jinja",
minijinja::context! { indent, free_method },
));
out.push_str(&render(
"deserialize_json.jinja",
minijinja::context! { indent, cs_type => cs_ty },
));
return;
}
}
let cs_ty = csharp_type(return_type);
out.push_str(&render(
"json_from_ptr.jinja",
minijinja::context! { indent, ptr_var => "nativeResult" },
));
out.push_str(&render(
"free_string_ptr.jinja",
minijinja::context! { indent, ptr_var => "nativeResult" },
));
out.push_str(&render(
"deserialize_json.jinja",
minijinja::context! { indent, cs_type => cs_ty },
));
} else {
out.push_str(&render("return_native_result.jinja", minijinja::context! { indent }));
}
}
pub(super) fn emit_return_statement_indented(out: &mut String, return_type: &TypeRef, indent: &str) {
if *return_type != TypeRef::Unit {
out.push_str(&crate::backends::csharp::template_env::render(
"return_value.jinja",
minijinja::context! { indent },
));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn variant(name: &str, message_template: Option<&str>) -> ErrorVariant {
ErrorVariant {
name: name.to_string(),
message_template: message_template.map(str::to_string),
is_unit: true,
..ErrorVariant::default()
}
}
fn error(name: &str, variants: Vec<ErrorVariant>) -> ErrorDef {
ErrorDef {
name: name.to_string(),
rust_path: format!("lib::{name}"),
original_rust_path: String::new(),
variants,
doc: String::new(),
methods: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn variant_dispatch_prefix_takes_the_literal_text_before_the_first_placeholder() {
assert_eq!(
variant_dispatch_prefix(&variant("Authentication", Some("Authentication failed: {reason}"))),
Some("Authentication failed:".to_string())
);
}
#[test]
fn variant_dispatch_prefix_is_none_without_a_template() {
assert_eq!(variant_dispatch_prefix(&variant("Authentication", None)), None);
}
#[test]
fn variant_dispatch_prefix_is_none_when_the_template_opens_on_a_placeholder() {
assert_eq!(variant_dispatch_prefix(&variant("Wrapped", Some("{0}"))), None);
}
#[test]
fn compute_variant_dispatch_covers_every_error_type_not_only_the_first() {
let errors = vec![
error(
"ApiError",
vec![variant("Authentication", Some("Authentication failed: {reason}"))],
),
error(
"StorageError",
vec![variant("Corrupt", Some("Corrupt archive: {path}"))],
),
];
let (has_base_error, base_exception_class, dispatch_lines) = compute_variant_dispatch(&errors);
assert!(has_base_error);
assert_eq!(base_exception_class, "ApiErrorException");
assert_eq!(
dispatch_lines,
vec![
" if (message.StartsWith(\"Authentication failed:\")) return new AuthenticationException(message);"
.to_string(),
" if (message.StartsWith(\"Corrupt archive:\")) return new CorruptException(message);".to_string(),
]
);
}
#[test]
fn compute_variant_dispatch_skips_variants_with_no_dispatchable_prefix() {
let errors = vec![error(
"ApiError",
vec![
variant("Authentication", Some("Authentication failed: {reason}")),
variant("Wrapped", Some("{0}")),
variant("Unknown", None),
],
)];
let (_, _, dispatch_lines) = compute_variant_dispatch(&errors);
assert_eq!(
dispatch_lines,
vec![
" if (message.StartsWith(\"Authentication failed:\")) return new AuthenticationException(message);"
.to_string(),
]
);
}
#[test]
fn compute_variant_dispatch_with_no_errors_disables_dispatch() {
assert_eq!(compute_variant_dispatch(&[]), (false, String::new(), Vec::new()));
}
#[test]
fn gen_exception_class_renders_a_distinct_dispatch_line_per_variant() {
let errors = vec![error(
"ApiError",
vec![
variant("Authentication", Some("Authentication failed: {reason}")),
variant("BadRequest", Some("Bad request: {reason}")),
],
)];
let rendered = gen_exception_class("Sample.Client", "SampleClientException", &errors);
let lines: Vec<&str> = rendered.lines().collect();
let expected: Vec<&str> = vec![
"// This file is auto-generated by alef. DO NOT EDIT.",
"#nullable enable",
"",
"using System;",
"",
"namespace Sample.Client;",
"",
"public class SampleClientException : Exception",
"{",
" public int Code { get; }",
"",
" public SampleClientException(int code, string message) : base(message)",
" {",
" Code = code;",
" }",
"",
" public SampleClientException(string message) : base(message)",
" {",
" Code = 0;",
" }",
"",
" public SampleClientException(string message, Exception innerException) : base(message, innerException)",
" {",
" Code = 0;",
" }",
"",
" /// <summary>",
" /// Builds the concrete exception for the FFI's current thread-local last-error state,",
" /// dispatching to the specific per-variant exception class when the message's prefix",
" /// identifies a known variant. Every throw site across the generated binding funnels",
" /// through here so a variant's identity is never lost to a bypassed dispatch.",
" /// </summary>",
" internal static Exception FromLastError(string fallbackMessage)",
" {",
" var code = NativeMethods.LastErrorCode();",
" var ctxPtr = NativeMethods.LastErrorContext();",
" var message = global::System.Runtime.InteropServices.Marshal.PtrToStringUTF8(ctxPtr) ?? fallbackMessage;",
" if (message.StartsWith(\"Authentication failed:\")) return new AuthenticationException(message);",
" if (message.StartsWith(\"Bad request:\")) return new BadRequestException(message);",
" if (code == 2) return new ApiErrorException(message);",
" return new SampleClientException(code, message);",
" }",
"}",
];
assert_eq!(lines, expected, "got:\n{rendered}");
}
}