use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::TypeRef;
use crate::e2e::codegen::TestBackendEmission;
use crate::e2e::escape::{escape_php, sanitize_ident};
use heck::ToUpperCamelCase;
use std::fmt::Write as FmtWrite;
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()
}
pub(super) fn trait_bridge_options_type(config: &ResolvedCrateConfig) -> Option<&str> {
crate::e2e::codegen::recipe::trait_bridge_options_type(config)
}
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_ns(trait_bridge, methods, fixture, "", "")
}
pub fn emit_test_backend_with_ns(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &crate::e2e::fixture::Fixture,
binding_namespace: &str,
binding_class: &str,
) -> TestBackendEmission {
use crate::codegen::defaults::language_defaults;
let defaults = language_defaults("php");
let backend_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
let mut setup = String::new();
let interface_name = trait_bridge.trait_name.to_upper_camel_case();
let qualified_interface = if binding_namespace.is_empty() {
interface_name.clone()
} else {
format!("\\{binding_namespace}\\{interface_name}")
};
let _ = writeln!(setup, "$stub = new class implements {qualified_interface} {{");
if trait_bridge.super_trait.is_some() {
let escaped_name = escape_php(&backend_name);
let _ = writeln!(
setup,
" public function name(): string {{ return '{escaped_name}'; }}"
);
}
for method in methods
.iter()
.filter(|m| !(trait_bridge.super_trait.is_some() && m.name == "name"))
{
let php_name = method.name.clone();
let default_val = match &method.return_type {
TypeRef::Named(_) => "'{}'".to_string(),
TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool) => "false".to_string(),
TypeRef::Primitive(_) => "1".to_string(), other => defaults.emit_default(other),
};
let params: Vec<String> = method
.params
.iter()
.map(|p| format!("${}", sanitize_ident(&p.name)))
.collect();
let param_str = params.join(", ");
if matches!(method.return_type, TypeRef::Unit) {
let _ = writeln!(
setup,
" public function {php_name}({param_str}): mixed {{ return null; }}"
);
} else {
let _ = writeln!(
setup,
" public function {php_name}({param_str}): mixed {{ return {default_val}; }}"
);
}
}
let _ = writeln!(setup, "}};");
let (teardown_block, type_imports) = if binding_class.is_empty() {
(String::new(), Vec::new())
} else {
trait_bridge
.unregister_fn
.as_deref()
.map(|unregister_fn| {
let escaped = escape_php(&backend_name);
let parts: Vec<&str> = unregister_fn.split('_').collect();
let mut method_name = String::new();
for (i, part) in parts.iter().enumerate() {
if i == 0 {
method_name.push_str(part);
} else if let Some(first) = part.chars().next() {
method_name.push_str(&first.to_uppercase().to_string());
method_name.push_str(&part[1..]);
}
}
let teardown = format!(" {binding_class}::{method_name}(\"{escaped}\");\n");
(teardown, vec![])
})
.unwrap_or_else(|| (String::new(), Vec::new()))
};
TestBackendEmission {
setup_block: setup,
arg_expr: "$stub".to_string(),
type_imports,
teardown_block,
}
}