use crate::codegen::naming::go_param_name;
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::fmt::Write as FmtWrite;
fn uses_json_type(ty: &crate::core::ir::TypeRef) -> bool {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Json => true,
TypeRef::Optional(inner) => uses_json_type(inner),
TypeRef::Vec(inner) => uses_json_type(inner),
TypeRef::Map(k, v) => uses_json_type(k) || uses_json_type(v),
_ => false,
}
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
) -> super::super::TestBackendEmission {
emit_test_backend_with_context(
trait_bridge,
methods,
fixture,
&std::collections::HashSet::new(),
"",
&std::collections::HashSet::new(),
&[],
)
}
pub fn emit_test_backend_with_context(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
excluded_types: &std::collections::HashSet<&str>,
import_alias: &str,
enum_names: &std::collections::HashSet<&str>,
enums: &[crate::core::ir::EnumDef],
) -> super::super::TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use crate::e2e::escape::sanitize_ident;
let defaults = language_defaults("go");
let safe_id = sanitize_ident(&fixture.id);
let struct_name = format!("testStub_{safe_id}");
let mut setup = String::new();
let _ = writeln!(setup, "type {struct_name} struct{{}}");
setup.push('\n');
if let Some(super_trait) = trait_bridge.super_trait.as_deref() {
let super_methods: Vec<_> = methods
.iter()
.filter(|m| m.trait_source.as_deref() == Some(super_trait))
.collect();
for method in &super_methods {
let go_method = method_to_camel(&method.name);
if method.name == "name" {
let _ = writeln!(
setup,
"func ({struct_name}) {go_method}() string {{ return \"{safe_id}\" }}"
);
} else {
emit_go_stub_method_body(
&mut setup,
&struct_name,
&go_method,
method,
&*defaults,
excluded_types,
import_alias,
enum_names,
fixture,
enums,
);
}
}
if !super_methods.is_empty() {
setup.push('\n');
}
}
for method in methods.iter() {
if trait_bridge
.super_trait
.as_deref()
.is_some_and(|st| method.trait_source.as_deref() == Some(st))
{
continue;
}
if should_skip_method_with_type(&method.return_type, excluded_types, method.error_type.is_some()) {
continue;
}
let go_method = method_to_camel(&method.name);
if method.name == "name" {
let _ = writeln!(
setup,
"func ({struct_name}) {go_method}() string {{ return \"{safe_id}\" }}"
);
continue;
}
emit_go_stub_method_body(
&mut setup,
&struct_name,
&go_method,
method,
&*defaults,
excluded_types,
import_alias,
enum_names,
fixture,
enums,
);
}
let uses_json_with_context = |ty: &crate::core::ir::TypeRef| -> bool {
uses_json_type(ty) || {
use crate::core::ir::TypeRef;
matches!(ty, TypeRef::Named(n) if excluded_types.contains(n.as_str()))
}
};
let needs_json = methods
.iter()
.any(|m| uses_json_with_context(&m.return_type) || m.params.iter().any(|p| uses_json_with_context(&p.ty)));
let mut type_imports = Vec::new();
if needs_json {
type_imports.push("encoding/json".to_string());
}
super::super::TestBackendEmission {
setup_block: setup,
arg_expr: format!("{struct_name}{{}}"),
type_imports,
teardown_block: String::new(),
}
}
fn go_stub_default_with_context(
ty: &crate::core::ir::TypeRef,
enum_names: &std::collections::HashSet<&str>,
excluded_types: &std::collections::HashSet<&str>,
import_alias: &str,
enums: &[crate::core::ir::EnumDef],
) -> String {
use crate::backends::go::type_map::go_zero_value;
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) if excluded_types.contains(name.as_str()) && enum_names.contains(name.as_str()) => {
match enums.iter().find(|e| e.name == *name) {
Some(enum_def) if !enum_def.variants.is_empty() => {
go_enum_variant_default_expression(name, enum_def, import_alias)
}
_ => "nil".to_string(),
}
}
TypeRef::Named(name) if excluded_types.contains(name.as_str()) => "nil".to_string(),
TypeRef::Named(name) if enum_names.contains(name.as_str()) => match enums.iter().find(|e| e.name == *name) {
Some(enum_def) if !enum_def.variants.is_empty() => {
go_enum_variant_default_expression(name, enum_def, import_alias)
}
_ => "\"\"".to_string(),
},
TypeRef::Named(name) if !import_alias.is_empty() => {
let go_name = crate::codegen::naming::go_type_name(name);
format!("{import_alias}.{go_name}{{}}")
}
TypeRef::Named(name) => {
let go_name = crate::codegen::naming::go_type_name(name);
format!("{go_name}{{}}")
}
_ => go_zero_value(ty),
}
}
fn go_enum_variant_default_expression(name: &str, enum_def: &crate::core::ir::EnumDef, import_alias: &str) -> String {
use crate::backends::go::{GoEnumRepresentation, go_data_enum_variant_struct, go_enum_representation};
let qualify = |type_name: String| {
if import_alias.is_empty() {
type_name
} else {
format!("{import_alias}.{type_name}")
}
};
let first_variant = &enum_def.variants[0];
let representation = go_enum_representation(enum_def);
if representation.has_named_constants() {
let go_name = crate::codegen::naming::go_type_name(name);
let variant_name = crate::codegen::naming::go_type_name(&first_variant.name);
return qualify(format!("{go_name}{variant_name}"));
}
match representation {
GoEnumRepresentation::DataInterface => {
let struct_name = go_data_enum_variant_struct(enum_def, first_variant);
qualify(format!("{struct_name}{{}}"))
}
GoEnumRepresentation::AdjacentTaggedStruct
| GoEnumRepresentation::TupleTaggedStruct
| GoEnumRepresentation::ExternallyTaggedStruct
| GoEnumRepresentation::RawMessage => {
let go_name = crate::codegen::naming::go_type_name(name);
qualify(format!("{go_name}{{}}"))
}
GoEnumRepresentation::UnitString | GoEnumRepresentation::NewtypeTupleString => unreachable!(
"has_named_constants() already returned true for UnitString/NewtypeTupleString and returned above"
),
}
}
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 should_skip_method_with_type(
ty: &crate::core::ir::TypeRef,
excluded_types: &std::collections::HashSet<&str>,
_is_result_return: bool,
) -> bool {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Optional(inner) => {
matches!(inner.as_ref(), TypeRef::Named(name) if excluded_types.contains(name.as_str()))
}
_ => false,
}
}
pub(super) fn stub_go_type_with_context(
ty: &crate::core::ir::TypeRef,
excluded_types: &std::collections::HashSet<&str>,
import_alias: &str,
enum_names: &std::collections::HashSet<&str>,
) -> String {
use crate::backends::go::type_map::go_type;
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) if !excluded_types.is_empty() && excluded_types.contains(name.as_str()) => {
if !enum_names.is_empty() && enum_names.contains(name.as_str()) {
let go_name = crate::codegen::naming::go_type_name(name);
if !import_alias.is_empty() {
format!("{import_alias}.{go_name}")
} else {
go_name
}
} else {
"json.RawMessage".to_string()
}
}
TypeRef::Named(name) if !import_alias.is_empty() => {
let go_name = crate::codegen::naming::go_type_name(name);
format!("{import_alias}.{go_name}")
}
TypeRef::Optional(inner) => {
let inner_str = stub_go_type_with_context(inner, excluded_types, import_alias, enum_names);
if inner_str == "json.RawMessage" {
inner_str
} else {
format!("*{inner_str}")
}
}
TypeRef::Vec(inner) => {
let inner_str = stub_go_type_with_context(inner, excluded_types, import_alias, enum_names);
format!("[]{inner_str}")
}
TypeRef::Map(k, v) => {
let k_str = stub_go_type_with_context(k, excluded_types, import_alias, enum_names);
let v_str = stub_go_type_with_context(v, excluded_types, import_alias, enum_names);
format!("map[{k_str}]{v_str}")
}
_ => go_type(ty).into_owned(),
}
}
pub(super) fn method_to_camel(snake: &str) -> String {
snake.to_upper_camel_case()
}
#[allow(clippy::too_many_arguments)]
fn emit_go_stub_method_body(
out: &mut String,
struct_name: &str,
go_method: &str,
method: &crate::core::ir::MethodDef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
excluded_types: &std::collections::HashSet<&str>,
import_alias: &str,
enum_names: &std::collections::HashSet<&str>,
fixture: &crate::e2e::fixture::Fixture,
enums: &[crate::core::ir::EnumDef],
) {
use crate::core::ir::TypeRef;
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let go_param = go_param_name(&p.name);
let type_str = stub_go_type_with_context(&p.ty, excluded_types, import_alias, enum_names);
format!("{go_param} {type_str}")
})
.collect();
let param_str = params.join(", ");
let ret_ty = stub_go_type_with_context(&method.return_type, excluded_types, import_alias, enum_names);
let return_type_str = if method.error_type.is_some() {
match &method.return_type {
TypeRef::Unit => "error".to_string(),
_ => format!("({ret_ty}, error)"),
}
} else {
ret_ty.clone()
};
let return_expr = if method.error_type.is_some() {
match &method.return_type {
TypeRef::Unit => "return nil".to_string(),
_ => {
let default_val = extract_fixture_default(&method.name, fixture).unwrap_or_else(|| {
go_stub_default_with_context(&method.return_type, enum_names, excluded_types, import_alias, enums)
});
format!("return {default_val}, nil")
}
}
} else if matches!(method.return_type, TypeRef::Unit) {
String::new()
} else {
let default_val = extract_fixture_default(&method.name, fixture).unwrap_or_else(|| {
go_stub_default_with_context(&method.return_type, enum_names, excluded_types, import_alias, enums)
});
format!("return {default_val}")
};
let _ = defaults;
let _ = writeln!(
out,
"func ({struct_name}) {go_method}({param_str}) {return_type_str} {{ {return_expr} }}"
);
}
const SUPER_TRAIT_REQUIRED_METHODS: [(&str, bool); 4] = [
("name", false),
("version", false),
("initialize", true),
("shutdown", true),
];
pub(super) fn resolve_test_backend_emission(
fixture: &crate::e2e::fixture::Fixture,
trait_name: &str,
trait_bridge: &crate::core::config::TraitBridgeConfig,
config: &crate::core::config::ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
import_alias: &str,
) -> super::super::TestBackendEmission {
let mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
.iter()
.find(|t| t.name == *trait_name)
.map(|t| t.methods.iter().collect())
.unwrap_or_default();
if let Some(super_trait) = &trait_bridge.super_trait
&& let Some(super_type) = type_defs.iter().find(|t| &t.rust_path == super_trait)
{
for method in &super_type.methods {
if !methods.iter().any(|m| m.name == method.name) {
methods.push(method);
}
}
}
let synthetic_super_trait_methods: Vec<crate::core::ir::MethodDef> = if trait_bridge.super_trait.is_some() {
SUPER_TRAIT_REQUIRED_METHODS
.iter()
.copied()
.filter(|(name, _)| !methods.iter().any(|m| m.name == *name))
.map(|(name, fallible)| crate::core::ir::MethodDef {
name: name.to_string(),
return_type: if fallible {
crate::core::ir::TypeRef::Unit
} else {
crate::core::ir::TypeRef::String
},
error_type: fallible.then(|| "Error".to_string()),
..Default::default()
})
.collect()
} else {
Vec::new()
};
methods.extend(synthetic_super_trait_methods.iter());
let excluded_named = crate::e2e::codegen::recipe::trait_bridge_excluded_type_names(config, type_defs, &methods);
let enum_names: std::collections::HashSet<&str> = enums.iter().map(|e| e.name.as_str()).collect();
emit_test_backend_with_context(
trait_bridge,
&methods,
fixture,
&excluded_named,
import_alias,
&enum_names,
enums,
)
}
#[cfg(test)]
mod trait_bridge_tests;
#[cfg(test)]
#[path = "test_backend/enum_default_tests.rs"]
mod enum_default_tests;