use heck::{ToLowerCamelCase, ToUpperCamelCase};
use crate::e2e::codegen::TestBackendEmission;
pub(super) fn java_type_fqn(ty: &crate::core::ir::TypeRef) -> String {
use crate::backends::java::type_map::java_type;
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(_) => "Object".to_string(),
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => "Object".to_string(),
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => "java.util.List<Object>".to_string(),
TypeRef::Vec(_) => {
format!("java.util.{}", java_type(ty).into_owned())
}
TypeRef::Map(_, _) => {
format!("java.util.{}", java_type(ty).into_owned())
}
_ => {
let t = java_type(ty).into_owned();
match t.as_str() {
"List" | "ArrayList" => format!("java.util.{}", t),
"Map" | "HashMap" => format!("java.util.{}", t),
_ => t,
}
}
}
}
pub(super) fn java_stub_type_fqn(ty: &crate::core::ir::TypeRef, binding_pkg: &str) -> String {
use crate::core::ir::TypeRef;
let pkg_prefix = if binding_pkg.is_empty() {
String::new()
} else {
format!("{binding_pkg}.")
};
match ty {
TypeRef::Named(name) => {
format!("{pkg_prefix}{name}")
}
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) => format!("{pkg_prefix}{name}"),
other => java_stub_type_fqn(other, binding_pkg),
},
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(name) => format!("java.util.List<{pkg_prefix}{name}>"),
other => format!("java.util.List<{}>", java_stub_type_fqn(other, binding_pkg)),
},
TypeRef::Map(k, v) => {
let key_type = java_stub_type_fqn(k, binding_pkg);
let val_type = java_stub_type_fqn(v, binding_pkg);
format!("java.util.Map<{}, {}>", key_type, val_type)
}
_ => java_type_fqn(ty),
}
}
pub(super) fn box_java_type_for_generic(ty: &str) -> String {
match ty {
"boolean" => "Boolean".to_string(),
"byte" => "Byte".to_string(),
"short" => "Short".to_string(),
"int" => "Integer".to_string(),
"long" => "Long".to_string(),
"float" => "Float".to_string(),
"double" => "Double".to_string(),
"char" => "Character".to_string(),
other => other.to_string(),
}
}
pub(super) fn java_stub_type_with_context(
ty: &crate::core::ir::TypeRef,
binding_pkg: &str,
excluded_types: &std::collections::HashSet<&str>,
) -> String {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) if !excluded_types.is_empty() && excluded_types.contains(name.as_str()) => {
"String".to_string()
}
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(name) if !excluded_types.is_empty() && excluded_types.contains(name.as_str()) => {
"String".to_string()
}
other => java_stub_type_with_context(other, binding_pkg, excluded_types),
},
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::Named(name) if !excluded_types.is_empty() && excluded_types.contains(name.as_str()) => {
"java.util.List<String>".to_string()
}
other => {
let inner_type = java_stub_type_with_context(other, binding_pkg, excluded_types);
let boxed_inner = box_java_type_for_generic(&inner_type);
format!("java.util.List<{boxed_inner}>")
}
},
TypeRef::Map(k, v) => {
let key_type = java_stub_type_with_context(k, binding_pkg, excluded_types);
let val_type = java_stub_type_with_context(v, binding_pkg, excluded_types);
let boxed_key = box_java_type_for_generic(&key_type);
let boxed_val = box_java_type_for_generic(&val_type);
format!("java.util.Map<{}, {}>", boxed_key, boxed_val)
}
_ => java_stub_type_fqn(ty, binding_pkg),
}
}
#[allow(dead_code)]
pub(super) fn java_boxed_stub_type_with_context(
ty: &crate::core::ir::TypeRef,
binding_pkg: &str,
excluded_types: &std::collections::HashSet<&str>,
) -> String {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Unit => "Void".to_string(),
_ => {
let t = java_stub_type_with_context(ty, binding_pkg, excluded_types);
match t.as_str() {
"boolean" => "Boolean".to_string(),
"byte" => "Byte".to_string(),
"short" => "Short".to_string(),
"int" => "Integer".to_string(),
"long" => "Long".to_string(),
"float" => "Float".to_string(),
"double" => "Double".to_string(),
"byte[]" => "byte[]".to_string(), _ => t,
}
}
}
}
pub(super) fn java_stub_default_with_context(
ty: &crate::core::ir::TypeRef,
excluded_types: &std::collections::HashSet<&str>,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
) -> String {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) if !excluded_types.is_empty() && excluded_types.contains(name.as_str()) => {
"\"null\"".to_string()
}
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if !excluded_types.is_empty() && excluded_types.contains(n.as_str())) =>
{
"\"null\"".to_string()
}
TypeRef::Named(_) => "null".to_string(),
_ => {
let def = defaults.emit_default(ty);
if def == "0" { "1".to_string() } else { def }
}
}
}
pub(super) fn java_stub_default_from_fixture(
method: &crate::core::ir::MethodDef,
ty: &crate::core::ir::TypeRef,
excluded_types: &std::collections::HashSet<&str>,
backend_input: Option<&serde_json::Map<String, serde_json::Value>>,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
) -> String {
use heck::ToLowerCamelCase;
let fixture_val = backend_input
.and_then(|b| b.get(&method.name.to_lowercase()))
.or_else(|| backend_input.and_then(|b| b.get(&method.name.to_lower_camel_case())));
if let Some(val) = fixture_val {
match val {
serde_json::Value::Number(n) => return n.to_string(),
serde_json::Value::String(s) => return format!("\"{}\"", s),
serde_json::Value::Bool(b) => return b.to_string(),
_ => {
}
}
}
java_stub_default_with_context(ty, excluded_types, defaults)
}
pub(super) fn emit_java_stub_method_with_context(
out: &mut String,
method_java: &str,
method: &crate::core::ir::MethodDef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
binding_pkg: &str,
excluded_types: &std::collections::HashSet<&str>,
backend_input: Option<&serde_json::Map<String, serde_json::Value>>,
) {
use std::fmt::Write as _;
let ret_java = java_stub_type_with_context(&method.return_type, binding_pkg, excluded_types);
let default_val =
java_stub_default_from_fixture(method, &method.return_type, excluded_types, backend_input, defaults);
let params: Vec<String> = method
.params
.iter()
.map(|p| {
format!(
"{} {}",
java_stub_type_with_context(&p.ty, binding_pkg, excluded_types),
p.name.to_lower_camel_case()
)
})
.collect();
let params_str = params.join(", ");
let _ = writeln!(out, " @Override");
if ret_java == "void" {
let _ = writeln!(out, " public void {method_java}({params_str}) {{}}");
} else {
let _ = writeln!(out, " public {ret_java} {method_java}({params_str}) {{");
let _ = writeln!(out, " return {default_val};");
let _ = writeln!(out, " }}");
}
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
binding_pkg: &str,
) -> TestBackendEmission {
emit_test_backend_with_context(trait_bridge, methods, fixture, binding_pkg, &Default::default(), "")
}
pub(super) fn emit_test_backend_with_context(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
binding_pkg: &str,
excluded_types: &std::collections::HashSet<&str>,
binding_class: &str,
) -> TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use crate::e2e::escape::escape_java;
use std::fmt::Write as _;
let pascal_id = fixture.id.to_upper_camel_case();
let class_name = format!("TestStub{pascal_id}");
let interface_name = if binding_pkg.is_empty() {
format!("I{}", trait_bridge.trait_name)
} else {
format!("{binding_pkg}.I{}", trait_bridge.trait_name)
};
let plugin_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
let backend_name = plugin_name.clone();
let backend_input = fixture.input.get("backend").and_then(|v| v.as_object());
let defaults = language_defaults("java");
let mut setup = String::new();
let _ = writeln!(setup, "class {class_name} implements {interface_name} {{");
if let Some(super_trait) = trait_bridge.super_trait.as_deref() {
for method in methods
.iter()
.filter(|m| m.trait_source.as_deref() == Some(super_trait))
{
let method_java = &method.name; if method.name == "name" {
let _ = writeln!(setup, " @Override");
let _ = writeln!(
setup,
" public String {method_java}() {{ return \"{plugin_name}\"; }}"
);
} else {
emit_java_stub_method_with_context(
&mut setup,
method_java,
method,
&*defaults,
binding_pkg,
excluded_types,
backend_input,
);
}
}
}
for method in methods {
if trait_bridge
.super_trait
.as_deref()
.is_some_and(|st| method.trait_source.as_deref() == Some(st))
{
continue;
}
let method_java = &method.name; if method.name == "name" {
let _ = writeln!(setup, " @Override");
let _ = writeln!(
setup,
" public String {method_java}() {{ return \"{plugin_name}\"; }}"
);
} else {
emit_java_stub_method_with_context(
&mut setup,
method_java,
method,
&*defaults,
binding_pkg,
excluded_types,
backend_input,
);
}
}
let _ = writeln!(setup, "}}");
let teardown_block = if binding_class.is_empty() {
String::new()
} else {
trait_bridge
.unregister_fn
.as_deref()
.map(|unregister_fn| {
let escaped = escape_java(&backend_name);
let camel_case_fn = unregister_fn.to_lower_camel_case();
format!(" {binding_class}.{camel_case_fn}(\"{escaped}\");\n")
})
.unwrap_or_default()
};
TestBackendEmission {
setup_block: setup,
arg_expr: format!("new {class_name}()"),
type_imports: Vec::new(),
teardown_block,
}
}
pub(super) fn extract_backend_name_from_input(input: &serde_json::Value, fallback: &str) -> String {
if let Some(obj) = input.as_object() {
if let Some(s) = obj.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
for v in obj.values() {
if let Some(inner) = v.as_object() {
if let Some(s) = inner.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
}
}
for v in obj.values() {
if let Some(s) = v.as_str() {
return s.to_string();
}
}
}
fallback.to_string()
}
#[cfg(test)]
mod test_backend_tests {
use super::emit_test_backend;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{MethodDef, PrimitiveType, TypeRef};
use crate::e2e::fixture::Fixture;
fn make_trait_bridge(trait_name: &str) -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: trait_name.to_string(),
super_trait: Some("Plugin".to_string()),
register_fn: Some(format!("register_{}", trait_name.to_lowercase())),
..Default::default()
}
}
fn make_method(name: &str, required: bool) -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![],
return_type: TypeRef::Primitive(PrimitiveType::Bool),
is_async: false,
is_static: false,
error_type: None,
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: !required,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
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: None,
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
}
}
#[test]
fn java_stub_contains_no_sample_crate_domain_names() {
let bridge = make_trait_bridge("TestTrait");
let required_method = make_method("process_item", true);
let methods = [&required_method];
let fixture = make_fixture("my_test_fixture");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
let output = format!("{}\n{}", emission.setup_block, emission.arg_expr);
assert!(
!output.contains("SampleCrate"),
"must not contain literal 'SampleCrate', got:\n{output}"
);
assert!(
!output.contains("sample_crate::"),
"must not contain 'sample_crate::', got:\n{output}"
);
assert!(
!output.contains("dev.sample_crate"),
"must not contain hardcoded 'dev.sample_crate', got:\n{output}"
);
assert!(
!output.contains("SampleCrateBridge"),
"must not contain 'SampleCrateBridge', got:\n{output}"
);
assert!(
output.contains("TestStubMyTestFixture"),
"class name must be derived from fixture id, got:\n{output}"
);
assert!(
output.contains("implements ITestTrait"),
"class must implement interface with binding_pkg prefix, got:\n{output}"
);
assert!(
output.contains("process_item"),
"required method must be emitted in snake_case to match interface, got:\n{output}"
);
}
#[test]
fn java_stub_uses_binding_pkg_for_interface_and_type_qualification() {
let bridge = make_trait_bridge("DocumentExtractor");
let method = MethodDef {
name: "extract_bytes".to_string(),
params: vec![],
return_type: TypeRef::Named("OperationOutput".to_string()),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: Some("DocumentExtractor".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 methods = [&method];
let fixture = make_fixture("extract_bytes_test");
let emission = emit_test_backend(&bridge, &methods, &fixture, "dev.example");
let output = &emission.setup_block;
assert!(
output.contains("implements dev.example.IDocumentExtractor"),
"class must implement dev.example.IDocumentExtractor, got:\n{output}"
);
assert!(
output.contains("dev.example.OperationOutput"),
"return type must use dev.example.OperationOutput, got:\n{output}"
);
assert!(
!output.contains("dev.sample_crate"),
"must not contain hardcoded dev.sample_crate, got:\n{output}"
);
}
#[test]
fn java_stub_plugin_name_extracted_from_input_name_field() {
let bridge = make_trait_bridge("DocumentExtractor");
let mut name_method = make_method("name", true);
name_method.trait_source = Some("Plugin".to_string());
let methods = [&name_method];
let fixture = Fixture {
id: "register_document_extractor_trait_bridge".to_string(),
category: None,
description: "test".to_string(),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::json!({
"extractor": {
"type": "test",
"name": "test-extractor"
}
}),
mock_response: None,
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
};
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
let output = &emission.setup_block;
assert!(
output.contains("public String name() { return \"test-extractor\"; }"),
"name() method must return extracted name 'test-extractor', got:\n{output}"
);
}
#[test]
fn java_stub_method_uses_fqn_for_domain_types_no_pkg() {
let bridge = make_trait_bridge("DocumentExtractor");
let method = MethodDef {
name: "extract_bytes".to_string(),
params: vec![],
return_type: TypeRef::Named("OperationOutput".to_string()),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: Some("DocumentExtractor".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 methods = [&method];
let fixture = make_fixture("extract_bytes_test");
let emission = emit_test_backend(&bridge, &methods, &fixture, "");
let output = &emission.setup_block;
assert!(
output.contains("public OperationOutput extract_bytes"),
"return type must use OperationOutput (unqualified, empty pkg) with snake_case method name, got:\n{output}"
);
assert!(
!output.contains("dev.sample_crate"),
"must not contain hardcoded dev.sample_crate, got:\n{output}"
);
}
#[test]
fn java_stub_named_return_type_emits_json_valid_default() {
let bridge = make_trait_bridge("PostProcessor");
let method = MethodDef {
name: "process".to_string(),
params: vec![],
return_type: TypeRef::Named("ProcessingConfig".to_string()),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: Some("PostProcessor".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 methods = [&method];
let fixture = make_fixture("register_post_processor");
let mut excluded = std::collections::HashSet::new();
excluded.insert("ProcessingConfig");
let emission = super::emit_test_backend_with_context(&bridge, &methods, &fixture, "", &excluded, "");
let output = &emission.setup_block;
assert!(
output.contains("return \"null\""),
"named return type default must be JSON-valid (\\\"null\\\"), got:\n{output}"
);
assert!(
!output.contains("return \"\""),
"must not return empty string for excluded types (causes serde_json parse panic), got:\n{output}"
);
}
#[test]
fn java_stub_numeric_return_type_emits_one_not_zero() {
let bridge = make_trait_bridge("EmbeddingBackend");
let method = MethodDef {
name: "dimensions".to_string(),
params: vec![],
return_type: TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: Some("EmbeddingBackend".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 methods = [&method];
let fixture = make_fixture("register_embedding_backend");
let excluded = std::collections::HashSet::new();
let emission = super::emit_test_backend_with_context(&bridge, &methods, &fixture, "", &excluded, "");
let output = &emission.setup_block;
assert!(
output.contains("return 1"),
"numeric return type default must be 1 (not 0), got:\n{output}"
);
assert!(
!output.contains("return 0"),
"must not return 0 for numeric types (fails downstream validation), got:\n{output}"
);
}
#[test]
fn java_stub_extracts_method_defaults_from_fixture_input() {
let bridge = make_trait_bridge("EmbeddingBackend");
let method = MethodDef {
name: "dimensions".to_string(),
params: vec![],
return_type: TypeRef::Primitive(crate::core::ir::PrimitiveType::Usize),
is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: Some("EmbeddingBackend".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 methods = [&method];
let fixture = Fixture {
id: "register_embedding_backend_with_input".to_string(),
category: None,
description: "test".to_string(),
tags: vec![],
skip: None,
env: None,
setup: Vec::new(),
call: None,
input: serde_json::json!({
"backend": {
"dimensions": 768
}
}),
mock_response: None,
source: String::new(),
http: None,
assertions: vec![],
visitor: None,
args: vec![],
assertion_recipes: vec![],
};
let excluded = std::collections::HashSet::new();
let emission = super::emit_test_backend_with_context(&bridge, &methods, &fixture, "", &excluded, "");
let output = &emission.setup_block;
assert!(
output.contains("return 768"),
"stub method must extract and use fixture.input.backend.dimensions (768), got:\n{output}"
);
assert!(
!output.contains("return 1"),
"must not use fallback default (1) when fixture value is present, got:\n{output}"
);
}
}