use super::*;
#[test]
fn test_type_stubs_documented_field_emits_var_phpdoc_with_description() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![TypeDef {
name: "ClientConfig".to_string(),
rust_path: "test_lib::ClientConfig".to_string(),
original_rust_path: String::new(),
fields: vec![
make_field_with_doc(
"base_url",
TypeRef::Optional(Box::new(TypeRef::String)),
true,
"Base URL of the remote API endpoint. Defaults to OpenAI's.",
),
make_field_with_doc(
"timeout_secs",
TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::I32))),
true,
"Request timeout in seconds.\nDefaults to 30.",
),
],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: true,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = make_config();
let files = backend.generate_type_stubs(&api, &config).unwrap();
let stubs = files.first().unwrap();
let content = &stubs.content;
assert!(
content.contains("@var ?string Base URL of the remote API endpoint. Defaults to OpenAI's."),
"Documented optional string field should have @var ?string with description;\ncontent:\n{content}"
);
assert!(
content.contains("@var ?int"),
"Documented optional int field should have @var ?int tag;\ncontent:\n{content}"
);
assert!(
content.contains("Request timeout in seconds."),
"Multi-line doc first line should appear in PHPDoc;\ncontent:\n{content}"
);
assert!(
content.contains("Defaults to 30."),
"Multi-line doc second line should appear in PHPDoc;\ncontent:\n{content}"
);
}
#[test]
fn test_type_stubs_undocumented_field_emits_var_phpdoc_type_only() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![TypeDef {
name: "Options".to_string(),
rust_path: "test_lib::Options".to_string(),
original_rust_path: String::new(),
fields: vec![
make_field("enabled", TypeRef::Primitive(PrimitiveType::Bool), false),
make_field(
"max_retries",
TypeRef::Optional(Box::new(TypeRef::Primitive(PrimitiveType::I32))),
true,
),
],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = make_config();
let files = backend.generate_type_stubs(&api, &config).unwrap();
let stubs = files.first().unwrap();
let content = &stubs.content;
assert!(
content.contains("/** @var bool */"),
"Undocumented bool field should have type-only /** @var bool */;\ncontent:\n{content}"
);
assert!(
content.contains("/** @var ?int */"),
"Undocumented optional int field should have type-only /** @var ?int */;\ncontent:\n{content}"
);
}
#[test]
fn test_public_api_sanitizes_rust_syntax_from_docstrings() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![FunctionDef {
name: "convert".to_string(),
rust_path: "test_lib::convert".to_string(),
original_rust_path: String::new(),
params: vec![ParamDef {
name: "html".to_string(),
ty: TypeRef::String,
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: alef::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::String,
error_type: None,
is_async: false,
doc: "Convert markup conversion, returning a result.\n\n# Arguments\n\n* `html` - The HTML string to convert.\n\n# Example\n\n```rust\nuse test_lib::convert;\nlet result = convert(html, None).unwrap();\n```"
.to_string(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = make_config();
let files = backend.generate_public_api(&api, &config).unwrap();
let facade = files.first().unwrap();
let content = &facade.content;
assert!(
!content.contains("use test_lib::convert;"),
"Rust 'use' statement must not leak into PHPDoc"
);
assert!(!content.contains(".unwrap()"), ".unwrap() must not leak into PHPDoc");
assert!(!content.contains("```rust"), "Raw Rust fence must not appear in PHPDoc");
assert!(
content.contains("Convert markup conversion"),
"Summary must be preserved in PHPDoc"
);
assert!(
content.contains("@param"),
"@param tag must be present for documented parameters"
);
assert!(content.contains("@return"), "@return tag must be present");
}
#[test]
fn test_duration_field_on_default_struct_getter_returns_option() {
let backend = PhpBackend;
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![TypeDef {
name: "CacheConfig".to_string(),
rust_path: "test_lib::CacheConfig".to_string(),
original_rust_path: String::new(),
fields: vec![
make_field("max_entries", TypeRef::Primitive(PrimitiveType::I64), false),
make_field("ttl", TypeRef::Duration, false),
],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: true,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: true,
super_traits: vec![],
doc: "Cache configuration".to_string(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = make_config();
let result = backend.generate_bindings(&api, &config);
assert!(result.is_ok(), "generation must succeed: {:?}", result.err());
let files = result.unwrap();
let lib_rs = files
.iter()
.find(|f| f.path.to_string_lossy().contains("lib.rs"))
.unwrap();
let content = &lib_rs.content;
assert!(
content.contains("pub ttl: Option<i64>"),
"Duration field on Default struct must be stored as Option<i64>; got:\n{content}"
);
assert!(
content.contains("fn get_ttl") && content.contains("-> Option<i64>"),
"getter for Duration field on Default struct must return Option<i64>; got:\n{content}"
);
assert!(
!content.contains("fn get_ttl(&self) -> i64"),
"getter must not return bare i64 for a Duration-on-Default field; got:\n{content}"
);
}
#[test]
fn has_default_struct_emits_delegating_impl_not_derived_default() {
use alef::codegen::generators::{AsyncPattern, RustBindingConfig, gen_struct_with_per_field_attrs};
use alef::core::ir::FieldDef;
let typ = TypeDef {
name: "CrawlConfig".to_string(),
rust_path: "test_lib::CrawlConfig".to_string(),
original_rust_path: String::new(),
fields: vec![
make_field("max_redirects", TypeRef::Primitive(PrimitiveType::I64), false),
make_field("respect_robots_txt", TypeRef::Primitive(PrimitiveType::Bool), false),
],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: true,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: true,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
struct StubMapper;
impl alef::codegen::type_mapper::TypeMapper for StubMapper {
fn error_wrapper(&self) -> &str {
"Result"
}
}
let mapper = StubMapper;
let struct_attrs: &[&str] = &["php_class", "serde(default, rename_all = \"camelCase\")"];
let struct_derives: &[&str] = &["Clone", "serde::Serialize", "serde::Deserialize"];
let cfg = RustBindingConfig {
struct_attrs,
field_attrs: &[],
struct_derives,
method_block_attr: Some("php_impl"),
constructor_attr: "",
static_attr: None,
function_attr: "#[php_function]",
enum_attrs: &[],
enum_derives: &[],
needs_signature: false,
signature_prefix: "",
signature_suffix: "",
core_import: "test_lib",
async_pattern: AsyncPattern::TokioBlockOn,
has_serde: true,
type_name_prefix: "",
option_duration_on_defaults: true,
opaque_type_names: &[],
skip_impl_constructor: false,
cast_uints_to_i32: false,
cast_large_ints_to_f64: false,
named_non_opaque_params_by_ref: false,
lossy_skip_types: &[],
serializable_opaque_type_names: &[],
never_skip_cfg_field_names: &[],
emit_delegating_default_impl: true,
skip_methods_when_not_delegatable: false,
};
let content = gen_struct_with_per_field_attrs(&typ, &mapper, &cfg, |_: &FieldDef| vec![]);
let struct_start = content
.find("pub struct CrawlConfig")
.expect("CrawlConfig struct must be emitted");
let derive_window = &content[..struct_start];
assert!(
!derive_window.contains("Default"),
"CrawlConfig must NOT derive Default — that would emit zeros instead of \
delegating to the core's custom Default. Derive block:\n{derive_window}"
);
assert!(
content.contains("impl Default for CrawlConfig"),
"delegating impl Default must be emitted for has_default types; got:\n{content}"
);
assert!(
content.contains("<test_lib::CrawlConfig as Default>::default().into()"),
"impl Default must delegate to the core type's Default via `.into()`; got:\n{content}"
);
assert!(
content.contains("serde(default"),
"struct must still carry struct-level `#[serde(default)]`; got:\n{content}"
);
assert!(
content.contains("serde::Serialize"),
"struct must still derive serde::Serialize; got:\n{content}"
);
assert!(
content.contains("serde::Deserialize"),
"struct must still derive serde::Deserialize; got:\n{content}"
);
}
#[test]
fn has_default_struct_keeps_derived_default_when_delegation_disabled() {
use alef::codegen::generators::{AsyncPattern, RustBindingConfig, gen_struct_with_per_field_attrs};
use alef::core::ir::FieldDef;
let typ = TypeDef {
name: "PlainConfig".to_string(),
rust_path: "test_lib::PlainConfig".to_string(),
original_rust_path: String::new(),
fields: vec![make_field("count", TypeRef::Primitive(PrimitiveType::I64), false)],
methods: vec![],
is_opaque: false,
is_clone: true,
is_copy: false,
is_trait: false,
has_default: true,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
struct StubMapper;
impl alef::codegen::type_mapper::TypeMapper for StubMapper {
fn error_wrapper(&self) -> &str {
"Result"
}
}
let mapper = StubMapper;
let cfg = RustBindingConfig {
struct_attrs: &[],
field_attrs: &[],
struct_derives: &["Clone"],
method_block_attr: None,
constructor_attr: "",
static_attr: None,
function_attr: "",
enum_attrs: &[],
enum_derives: &[],
needs_signature: false,
signature_prefix: "",
signature_suffix: "",
core_import: "test_lib",
async_pattern: AsyncPattern::None,
has_serde: false,
type_name_prefix: "",
option_duration_on_defaults: false,
opaque_type_names: &[],
skip_impl_constructor: false,
cast_uints_to_i32: false,
cast_large_ints_to_f64: false,
named_non_opaque_params_by_ref: false,
lossy_skip_types: &[],
serializable_opaque_type_names: &[],
never_skip_cfg_field_names: &[],
emit_delegating_default_impl: false,
skip_methods_when_not_delegatable: false,
};
let content = gen_struct_with_per_field_attrs(&typ, &mapper, &cfg, |_: &FieldDef| vec![]);
assert!(
content.contains("Default"),
"Default must still be derived when emit_delegating_default_impl is false; got:\n{content}"
);
assert!(
!content.contains("impl Default for PlainConfig"),
"no delegating impl Default should be emitted when the flag is disabled; got:\n{content}"
);
}