#[cfg(test)]
mod class_scope_tests;
#[cfg(test)]
mod named_return_tests;
#[cfg(test)]
mod nullable_return_tests;
use crate::backends::csharp::trait_bridge::csharp_type_visible_pub;
use crate::codegen::naming::{csharp_type_name, to_csharp_name};
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 collect_named_types<'a>(ty: &'a crate::core::ir::TypeRef, out: &mut std::collections::HashSet<&'a str>) {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) => {
out.insert(name.as_str());
}
TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_named_types(inner, out),
TypeRef::Map(k, v) => {
collect_named_types(k, out);
collect_named_types(v, out);
}
_ => {}
}
}
fn emit_csharp_stub_default(
original_type: &crate::core::ir::TypeRef,
visible_type: &str,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
visible_type_names: &std::collections::HashSet<&str>,
) -> String {
use crate::core::ir::TypeRef;
fn contains_non_visible(ty: &TypeRef, visible_type_names: &std::collections::HashSet<&str>) -> bool {
match ty {
TypeRef::Named(name) => !visible_type_names.contains(name.as_str()),
TypeRef::Optional(inner) => contains_non_visible(inner, visible_type_names),
TypeRef::Vec(inner) => contains_non_visible(inner, visible_type_names),
TypeRef::Map(k, v) => {
contains_non_visible(k, visible_type_names) || contains_non_visible(v, visible_type_names)
}
_ => false,
}
}
if contains_non_visible(original_type, visible_type_names) {
if visible_type.contains("?") {
"null".to_string()
} else {
"\"\"".to_string()
}
} else if matches!(original_type, TypeRef::Named(_)) {
format!("new {visible_type}()")
} else {
defaults.emit_default(original_type)
}
}
fn non_null_stub_default(default_val: String, visible_type: &str) -> String {
if default_val != "null" || visible_type.ends_with('?') {
return default_val;
}
if visible_type == "string" {
"\"\"".to_string()
} else {
format!("new {visible_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,
visible_type_names: &std::collections::HashSet<&str>,
fixture: &crate::e2e::fixture::Fixture,
) {
use crate::core::ir::TypeRef;
let ret_ty = csharp_type_visible_pub(&method.return_type, visible_type_names);
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, visible_type_names),
}
} else {
emit_csharp_stub_default(&method.return_type, &ret_ty, defaults, visible_type_names)
}
});
let default_val = non_null_stub_default(default_val, &ret_ty);
let params: Vec<String> = method
.params
.iter()
.map(|p| {
format!(
"{} {}",
csharp_type_visible_pub(&p.ty, visible_type_names),
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 = csharp_type_name(&trait_bridge.trait_name);
let iface_name = format!("I{trait_pascal}");
let mut referenced_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
for method in methods.iter() {
collect_named_types(&method.return_type, &mut referenced_names);
for param in &method.params {
collect_named_types(¶m.ty, &mut referenced_names);
}
}
let visible_type_names: std::collections::HashSet<&str> = referenced_names
.into_iter()
.filter(|name| !excluded_types.contains(name))
.collect();
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, " 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 = to_csharp_name(&method.name);
emit_csharp_stub_method(&mut setup, &method_cs, method, &*defaults, &visible_type_names, fixture);
emitted_methods.insert(method.name.clone());
}
}
for method in methods.iter() {
if emitted_methods.contains(&method.name) {
continue;
}
let method_cs = to_csharp_name(&method.name);
emit_csharp_stub_method(&mut setup, &method_cs, method, &*defaults, &visible_type_names, 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,
}
}