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);
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()) => {
if let Some(enum_def) = enums.iter().find(|e| e.name == *name) {
if let Some(first_variant) = enum_def.variants.first() {
let go_name = crate::codegen::naming::go_type_name(name);
let variant_name = crate::codegen::naming::go_type_name(&first_variant.name);
if !import_alias.is_empty() {
format!("{import_alias}.{go_name}{variant_name}")
} else {
format!("{go_name}{variant_name}")
}
} else {
"nil".to_string()
}
} else {
"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()) => {
if let Some(enum_def) = enums.iter().find(|e| e.name == *name) {
if let Some(first_variant) = enum_def.variants.first() {
let go_name = crate::codegen::naming::go_type_name(name);
let variant_name = crate::codegen::naming::go_type_name(&first_variant.name);
if !import_alias.is_empty() {
format!("{import_alias}.{go_name}{variant_name}")
} else {
format!("{go_name}{variant_name}")
}
} else {
"\"\"".to_string()
}
} else {
"\"\"".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 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} }}"
);
}
#[cfg(test)]
mod trait_bridge_tests {
use super::{emit_test_backend, emit_test_backend_with_context};
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, ParamDef, TypeRef};
use crate::e2e::fixture::Fixture;
fn make_fixture(id: &str) -> Fixture {
Fixture {
id: id.to_string(),
category: None,
description: "test".to_string(),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::Value::Null,
mock_response: Some(crate::e2e::fixture::MockResponse {
status: 200,
body: Some(serde_json::Value::Null),
stream_chunks: None,
headers: std::collections::BTreeMap::new(),
}),
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
}
}
fn make_param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}
}
fn make_method(name: &str, params: Vec<(&str, TypeRef)>, ret: TypeRef, is_async: bool) -> MethodDef {
MethodDef {
name: name.to_string(),
params: params.into_iter().map(|(n, ty)| make_param(n, ty)).collect(),
return_type: ret,
is_async,
is_static: false,
error_type: Some("Error".to_string()),
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn test_backend_emission_is_generic() {
let trait_bridge = TraitBridgeConfig {
trait_name: "TestTrait".to_string(),
super_trait: Some("SomeSuperTrait".to_string()),
register_fn: Some("register_test_trait".to_string()),
..TraitBridgeConfig::default()
};
let do_thing = make_method(
"do_thing",
vec![("x", TypeRef::Primitive(crate::core::ir::PrimitiveType::I32))],
TypeRef::String,
false,
);
let fixture = make_fixture("my_test_fixture");
let methods = vec![&do_thing];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
assert!(
!emission.setup_block.contains("ImageBackend"),
"setup_block must not hardcode domain trait names, got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("ProcessImage"),
"setup_block must not hardcode domain method names, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("DoThing"),
"setup_block must contain Go PascalCase method 'DoThing', got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("type testStub_my_test_fixture struct"),
"setup_block must contain struct declaration, got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("Initialize"),
"setup_block must not contain hardcoded 'Initialize', got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("Shutdown"),
"setup_block must not contain hardcoded 'Shutdown', got:\n{}",
emission.setup_block
);
assert!(
emission.arg_expr.contains("testStub_my_test_fixture"),
"arg_expr must reference struct name, got: {}",
emission.arg_expr
);
assert!(
emission.arg_expr.ends_with("{}"),
"arg_expr must be a struct literal, got: {}",
emission.arg_expr
);
}
#[test]
fn test_go_super_trait_methods_driven_from_ir_not_hardcoded() {
let make_super_method = |name: &str, ret: TypeRef| -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![],
return_type: ret,
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: Some("Plugin".to_string()),
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
};
let name_method = make_super_method("name", TypeRef::String);
let version_method = make_super_method("version", TypeRef::String);
let init_method = make_super_method("init", TypeRef::Unit);
let trait_bridge = TraitBridgeConfig {
trait_name: "TestPlugin".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_test_plugin".to_string()),
..TraitBridgeConfig::default()
};
let fixture = make_fixture("my_plugin_fixture");
let methods = vec![&name_method, &version_method, &init_method];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("Init("),
"setup_block must contain 'Init(' (from IR), got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("Initialize"),
"setup_block must NOT contain hardcoded 'Initialize', got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("Version("),
"setup_block must contain 'Version(' (from IR), got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("Shutdown"),
"setup_block must NOT contain hardcoded 'Shutdown', got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("Name()"),
"setup_block must contain Name() from IR name method, got:\n{}",
emission.setup_block
);
}
#[test]
fn test_go_stub_named_types_use_proper_go_names() {
let backend_type_method = make_method("backend_type", vec![], TypeRef::Named("BackendKind".to_string()), false);
let trait_bridge = TraitBridgeConfig {
trait_name: "SampleBackend".to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some("register_sample_backend".to_string()),
..TraitBridgeConfig::default()
};
let fixture = make_fixture("backend_type_test");
let methods = vec![&backend_type_method];
let emission = emit_test_backend(&trait_bridge, &methods, &fixture);
assert!(
emission.setup_block.contains("BackendType()") && emission.setup_block.contains("BackendKind"),
"setup_block must use BackendKind in BackendType() method signature, got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("json.RawMessage(nil)"),
"setup_block must not use json.RawMessage for BackendKind, got:\n{}",
emission.setup_block
);
}
#[test]
fn test_go_stub_skips_excluded_return_types() {
let excluded_return_method = make_method(
"get_internal_record",
vec![],
TypeRef::Named("InternalRecord".to_string()),
false,
);
let result_return_method = make_method(
"extract_bytes",
vec![("content", TypeRef::Bytes)],
TypeRef::Named("InternalRecord".to_string()), true, );
let normal_method = make_method("get_config", vec![], TypeRef::Named("ParseConfig".to_string()), false);
let trait_bridge = TraitBridgeConfig {
trait_name: "RecordProvider".to_string(),
super_trait: None,
register_fn: Some("register_document_extractor".to_string()),
..TraitBridgeConfig::default()
};
let fixture = make_fixture("extractor_test");
let methods = vec![&excluded_return_method, &result_return_method, &normal_method];
let mut excluded = std::collections::HashSet::new();
excluded.insert("InternalRecord");
let enum_names = std::collections::HashSet::new();
let emission = emit_test_backend_with_context(
&trait_bridge,
&methods,
&fixture,
&excluded,
"myproject",
&enum_names,
&[],
);
assert!(
!emission.setup_block.contains("get_internal_record"),
"method with directly excluded return type must be skipped, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("ExtractBytes"),
"method with Result<ExcludedType> should be emitted (binding handles conversion), got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("GetConfig"),
"normal method must be emitted, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("myproject.ParseConfig"),
"named type ParseConfig must be qualified as myproject.ParseConfig, got:\n{}",
emission.setup_block
);
}
#[test]
fn test_go_stub_emits_methods_returning_named_excluded_types() {
let diagnose_method = make_method("diagnose", vec![], TypeRef::Named("DiagnosticLevel".to_string()), false);
let trait_bridge = TraitBridgeConfig {
trait_name: "MyService".to_string(),
super_trait: None,
register_fn: Some("register_my_service".to_string()),
..TraitBridgeConfig::default()
};
let fixture = make_fixture("service_diagnose");
let methods = vec![&diagnose_method];
let mut excluded = std::collections::HashSet::new();
excluded.insert("DiagnosticLevel");
let enum_names = std::collections::HashSet::new();
let emission =
emit_test_backend_with_context(&trait_bridge, &methods, &fixture, &excluded, "", &enum_names, &[]);
assert!(
emission.setup_block.contains("Diagnose()"),
"method returning excluded named type must be emitted, got:\n{}",
emission.setup_block
);
assert!(
emission.setup_block.contains("json.RawMessage") || emission.setup_block.contains("nil"),
"method must emit a zero-value that matches the excluded type handling, got:\n{}",
emission.setup_block
);
}
#[test]
fn test_go_stub_skips_optional_excluded_return_types() {
let optional_excluded_method = make_method(
"as_internal_provider",
vec![],
TypeRef::Optional(Box::new(TypeRef::Named("InternalProvider".to_string()))),
false,
);
let trait_bridge = TraitBridgeConfig {
trait_name: "RecordProvider".to_string(),
super_trait: None,
register_fn: Some("register_document_extractor".to_string()),
..TraitBridgeConfig::default()
};
let fixture = make_fixture("extractor_test");
let methods = vec![&optional_excluded_method];
let mut excluded = std::collections::HashSet::new();
excluded.insert("InternalProvider");
let enum_names = std::collections::HashSet::new();
let emission =
emit_test_backend_with_context(&trait_bridge, &methods, &fixture, &excluded, "mylib", &enum_names, &[]);
assert!(
!emission.setup_block.contains("as_internal_provider")
&& !emission.setup_block.contains("AsInternalProvider"),
"method with Option<ExcludedType> return must be skipped, got:\n{}",
emission.setup_block
);
assert!(
!emission.setup_block.contains("InternalProvider"),
"excluded type InternalProvider must not appear in stub, got:\n{}",
emission.setup_block
);
}
}