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, "", "", &[])
}
fn native_struct_php_return(
ret: &crate::core::ir::TypeRef,
type_defs: &[crate::core::ir::TypeDef],
binding_namespace: &str,
) -> Option<String> {
let (leaf, optional) = match ret {
TypeRef::Named(n) => (n.as_str(), false),
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Named(n) => (n.as_str(), true),
_ => return None,
},
_ => return None,
};
let is_native = type_defs
.iter()
.any(|t| t.name == leaf && !t.is_trait && !t.is_opaque && t.has_serde && !t.binding_excluded);
if !is_native {
return None;
}
let qualified = if binding_namespace.is_empty() {
leaf.to_string()
} else {
format!("\\{binding_namespace}\\{leaf}")
};
Some(if optional { format!("?{qualified}") } else { qualified })
}
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,
type_defs: &[crate::core::ir::TypeDef],
) -> 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(", ");
let php_return_type: String = native_struct_php_return(&method.return_type, type_defs, binding_namespace)
.unwrap_or_else(|| {
match &method.return_type {
TypeRef::String => "string",
TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool) => "bool",
TypeRef::Primitive(
crate::core::ir::PrimitiveType::I32
| crate::core::ir::PrimitiveType::I64
| crate::core::ir::PrimitiveType::U32
| crate::core::ir::PrimitiveType::U64
| crate::core::ir::PrimitiveType::Usize,
) => "int",
TypeRef::Primitive(crate::core::ir::PrimitiveType::F32 | crate::core::ir::PrimitiveType::F64) => {
"float"
}
_ => "mixed",
}
.to_string()
});
if matches!(method.return_type, TypeRef::Unit) {
let _ = writeln!(
setup,
" public function {php_name}({param_str}): {php_return_type} {{ return null; }}"
);
} else {
let _ = writeln!(
setup,
" public function {php_name}({param_str}): {php_return_type} {{ 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,
}
}