use crate::e2e::codegen::TestBackendEmission;
use crate::e2e::escape::sanitize_ident;
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::fmt::Write as FmtWrite;
pub(super) fn csharp_type_for_stub(ty: &crate::core::ir::TypeRef) -> String {
use crate::core::ir::{PrimitiveType, TypeRef};
match ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "bool".to_string(),
PrimitiveType::U8 => "byte".to_string(),
PrimitiveType::U16 => "ushort".to_string(),
PrimitiveType::U32 => "uint".to_string(),
PrimitiveType::U64 => "ulong".to_string(),
PrimitiveType::I8 => "sbyte".to_string(),
PrimitiveType::I16 => "short".to_string(),
PrimitiveType::I32 => "int".to_string(),
PrimitiveType::I64 => "long".to_string(),
PrimitiveType::F32 => "float".to_string(),
PrimitiveType::F64 => "double".to_string(),
PrimitiveType::Usize => "ulong".to_string(), PrimitiveType::Isize => "long".to_string(),
},
TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
TypeRef::Bytes => "byte[]".to_string(),
TypeRef::Unit => "void".to_string(),
TypeRef::Optional(inner) => format!("{}?", csharp_type_for_stub(inner)),
TypeRef::Vec(inner) => format!("List<{}>", csharp_type_for_stub(inner)),
TypeRef::Map(k, v) => format!("Dictionary<{}, {}>", csharp_type_for_stub(k), csharp_type_for_stub(v)),
TypeRef::Named(name) => name.clone(),
TypeRef::Json => "object".to_string(),
TypeRef::Duration => "ulong?".to_string(),
}
}
fn csharp_type_for_stub_visible(
ty: &crate::core::ir::TypeRef,
excluded_types: &std::collections::HashSet<&str>,
) -> String {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) => {
if excluded_types.contains(name.as_str()) {
"string".to_string()
} else {
name.clone()
}
}
TypeRef::Optional(inner) => {
let inner_str = csharp_type_for_stub_visible(inner, excluded_types);
format!("{}?", inner_str)
}
TypeRef::Vec(inner) => {
let inner_str = csharp_type_for_stub_visible(inner, excluded_types);
format!("List<{}>", inner_str)
}
TypeRef::Map(k, v) => {
let key_str = csharp_type_for_stub_visible(k, excluded_types);
let val_str = csharp_type_for_stub_visible(v, excluded_types);
format!("Dictionary<{}, {}>", key_str, val_str)
}
_ => csharp_type_for_stub(ty),
}
}
fn emit_csharp_stub_default(
original_type: &crate::core::ir::TypeRef,
visible_type: &str,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
excluded_types: &std::collections::HashSet<&str>,
) -> String {
use crate::core::ir::TypeRef;
fn contains_non_visible(ty: &TypeRef, excluded_types: &std::collections::HashSet<&str>) -> bool {
match ty {
TypeRef::Named(name) => excluded_types.contains(name.as_str()),
TypeRef::Optional(inner) => contains_non_visible(inner, excluded_types),
TypeRef::Vec(inner) => contains_non_visible(inner, excluded_types),
TypeRef::Map(k, v) => contains_non_visible(k, excluded_types) || contains_non_visible(v, excluded_types),
_ => false,
}
}
if contains_non_visible(original_type, excluded_types) {
if visible_type.contains("?") {
"null".to_string()
} else {
"\"\"".to_string()
}
} else if matches!(original_type, TypeRef::Named(_)) {
format!("default({visible_type})")
} else {
defaults.emit_default(original_type)
}
}
fn extract_fixture_default(method_name: &str, fixture: &crate::e2e::fixture::Fixture) -> Option<String> {
let backend_input = fixture.input.get("backend").and_then(|v| v.as_object())?;
let snake_name = method_name.to_snake_case();
let val = backend_input
.get(&snake_name)
.or_else(|| backend_input.get(method_name))?;
Some(match val {
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
if i == 0 { "1".to_string() } else { i.to_string() }
} else if let Some(u) = n.as_u64() {
if u == 0 { "1".to_string() } else { u.to_string() }
} else {
n.to_string()
}
}
serde_json::Value::String(s) => format!("\"{}\"", s),
serde_json::Value::Bool(b) => b.to_string(),
_ => return None, })
}
fn emit_csharp_stub_method(
out: &mut String,
method_cs: &str,
method: &crate::core::ir::MethodDef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
excluded_types: &std::collections::HashSet<&str>,
fixture: &crate::e2e::fixture::Fixture,
) {
use crate::core::ir::TypeRef;
let ret_ty = csharp_type_for_stub_visible(&method.return_type, excluded_types);
let default_val = extract_fixture_default(&method.name, fixture).unwrap_or_else(|| {
if method.params.is_empty()
&& matches!(
method.return_type,
TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize | crate::core::ir::PrimitiveType::U64)
)
{
match method.name.to_lowercase().as_str() {
"dimensions" | "embedding_dimensions" | "model_dimensions" => "1".to_string(),
_ => emit_csharp_stub_default(&method.return_type, &ret_ty, defaults, excluded_types),
}
} else {
emit_csharp_stub_default(&method.return_type, &ret_ty, defaults, excluded_types)
}
});
let params: Vec<String> = method
.params
.iter()
.map(|p| {
format!(
"{} {}",
csharp_type_for_stub_visible(&p.ty, excluded_types),
p.name.to_lower_camel_case()
)
})
.collect();
let param_list = params.join(", ");
if matches!(method.return_type, TypeRef::Unit) {
let _ = writeln!(out, " public void {method_cs}({param_list}) {{ }}");
} else if method.params.is_empty() {
let _ = writeln!(out, " public {ret_ty} {method_cs} {{ get; }} = {default_val};");
} else {
let _ = writeln!(out, " public {ret_ty} {method_cs}({param_list})");
let _ = writeln!(out, " => {default_val};");
}
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> TestBackendEmission {
emit_test_backend_with_class_name(
trait_bridge,
methods,
fixture,
"GeneratedBinding",
&std::collections::HashSet::new(),
)
}
pub(super) fn emit_test_backend_with_class_name(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
class_name: &str,
excluded_types: &std::collections::HashSet<&str>,
) -> TestBackendEmission {
use crate::codegen::defaults::language_defaults;
let defaults = language_defaults("csharp");
let stub_class = format!("TestStub_{}", sanitize_ident(&fixture.id).to_upper_camel_case());
let trait_pascal = trait_bridge.trait_name.to_upper_camel_case();
let iface_name = format!("I{trait_pascal}");
let plugin_name = fixture
.input
.get("name")
.and_then(|v| v.as_str())
.unwrap_or(&fixture.id)
.to_string();
let mut setup = String::new();
let _ = writeln!(setup, " private class {stub_class} : {iface_name}");
let _ = writeln!(setup, " {{");
let mut emitted_methods = std::collections::HashSet::new();
if let Some(super_trait) = trait_bridge.super_trait.as_deref() {
let _ = writeln!(setup, " public string Name => \"{plugin_name}\";");
let _ = writeln!(setup, " public string Version => \"1.0.0\";");
let _ = writeln!(setup);
emitted_methods.insert("name".to_string());
emitted_methods.insert("version".to_string());
for method in methods
.iter()
.filter(|m| m.trait_source.as_deref() == Some(super_trait))
{
let method_cs = method.name.to_upper_camel_case();
emit_csharp_stub_method(&mut setup, &method_cs, method, &*defaults, excluded_types, fixture);
emitted_methods.insert(method.name.clone());
}
}
for method in methods.iter() {
if emitted_methods.contains(&method.name) {
continue;
}
let method_cs = method.name.to_upper_camel_case();
emit_csharp_stub_method(&mut setup, &method_cs, method, &*defaults, excluded_types, fixture);
}
let _ = writeln!(setup, " }}");
let arg_expr = format!("{}Bridge.Register(new {}())", trait_pascal, stub_class);
let escaped_plugin_name = plugin_name.replace('\\', "\\\\").replace('"', "\\\"");
let teardown_block = format!("{class_name}.Unregister{trait_pascal}(\"{escaped_plugin_name}\");");
TestBackendEmission {
setup_block: setup,
arg_expr,
type_imports: Vec::new(),
teardown_block,
}
}