use alef::core::config::{BridgeBinding, TraitBridgeConfig};
use alef::core::ir::*;
fn make_field(name: &str, ty: TypeRef) -> FieldDef {
FieldDef {
name: name.to_string(),
ty,
..Default::default()
}
}
fn make_param(name: &str, ty: TypeRef, is_ref: bool) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
is_ref,
..Default::default()
}
}
fn async_method(name: &str, return_type: TypeRef) -> MethodDef {
MethodDef {
name: name.to_string(),
params: vec![make_param("texts", TypeRef::Vec(Box::new(TypeRef::String)), false)],
return_type,
receiver: Some(ReceiverKind::Ref),
error_type: Some("Error".to_string()),
is_async: true,
..Default::default()
}
}
fn embedder_trait() -> TypeDef {
TypeDef {
name: "Embedder".to_string(),
rust_path: "test_lib::Embedder".to_string(),
is_trait: true,
is_opaque: true,
methods: vec![
async_method(
"embed",
TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::F32))))),
),
async_method("tag", TypeRef::Vec(Box::new(TypeRef::String))),
async_method("describe", TypeRef::Named("Doc".to_string())),
],
..Default::default()
}
}
fn embedder_api() -> ApiSurface {
let mut doc = TypeDef {
name: "Doc".to_string(),
rust_path: "test_lib::Doc".to_string(),
has_serde: true,
fields: vec![make_field("text", TypeRef::String)],
..Default::default()
};
doc.is_return_type = true;
ApiSurface {
crate_name: "test_lib".to_string(),
version: "0.1.0".to_string(),
types: vec![embedder_trait(), doc],
..Default::default()
}
}
fn embedder_bridge_cfg() -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: "Embedder".to_string(),
register_fn: Some("register_embedder".to_string()),
registry_getter: Some("test_lib::registry::get".to_string()),
super_trait: Some("Plugin".to_string()),
bind_via: BridgeBinding::FunctionParam,
..Default::default()
}
}
fn method_source<'a>(code: &'a str, name: &str) -> &'a str {
let marker = format!("async fn {name}(");
let start = code
.find(&marker)
.unwrap_or_else(|| panic!("method `{name}` not found in:\n{code}"));
let rest = &code[start..];
match rest[marker.len()..].find("async fn ") {
Some(next) => &rest[..marker.len() + next],
None => rest,
}
}
fn gen_bridge_code() -> String {
let trait_def = embedder_trait();
let api = embedder_api();
alef::backends::php::trait_bridge::gen_trait_bridge(
&trait_def,
&embedder_bridge_cfg(),
"test_lib",
"TestLibError",
"TestLibError::from({msg})",
&api,
)
.code
}
#[test]
fn vec_string_param_is_passed_as_native_php_array_for_every_method() {
let code = gen_bridge_code();
for name in ["embed", "tag", "describe"] {
let m = method_source(&code, name);
assert!(
m.contains("ext_php_rs::convert::IntoZval::into_zval(texts.clone(), false)"),
"`{name}`'s `texts: Vec<String>` arg must be passed as a native PHP array:\n{m}"
);
assert!(
!m.contains("format!(\"{:?}\", texts)"),
"`{name}`'s `texts` arg must NOT be Debug-string encoded:\n{m}"
);
}
}
#[test]
fn native_decodable_vec_string_return_type_decodes_via_from_zval() {
let code = gen_bridge_code();
let tag = method_source(&code, "tag");
assert!(
tag.contains("<Vec<String> as ext_php_rs::convert::FromZval>::from_zval(&val)"),
"`tag`'s `Vec<String>` return is fully php-native and must decode via FromZval:\n{tag}"
);
assert!(
!tag.contains("val.string()") && !tag.contains("serde_json::from_str"),
"`tag`'s native return must NOT go through the JSON string fallback:\n{tag}"
);
}
#[test]
fn native_decodable_nested_f32_return_type_also_decodes_via_from_zval() {
let code = gen_bridge_code();
let embed = method_source(&code, "embed");
assert!(
embed.contains("<Vec<Vec<f32>> as ext_php_rs::convert::FromZval>::from_zval(&val)"),
"`embed`'s `Vec<Vec<f32>>` return must decode natively via FromZval (ext-php-rs supports \
f32 decoding):\n{embed}"
);
assert!(
!embed.contains("val.string()") && !embed.contains("serde_json::from_str"),
"`embed` must NOT fall back to the JSON string path:\n{embed}"
);
}
#[test]
fn named_struct_return_type_keeps_native_struct_or_json_fallback_no_regression() {
let code = gen_bridge_code();
let describe = method_source(&code, "describe");
assert!(
describe.contains("<&Doc as ext_php_rs::convert::FromZval>::from_zval(&val)"),
"`describe`'s `Doc` struct return must keep the native-struct extraction path:\n{describe}"
);
assert!(
describe.contains("val.string()") && describe.contains("serde_json::from_str"),
"`describe` must still fall back to JSON when the PHP value isn't the native object:\n{describe}"
);
assert!(
!describe.contains("<test_lib::Doc as ext_php_rs::convert::FromZval>::from_zval(&val)\n")
&& !describe.contains(".ok_or_else("),
"`describe` must NOT go through the new generic native-decode branch (that branch is only \
for String/primitive/Vec returns, not Named struct returns):\n{describe}"
);
}