alef 0.103.13

Opinionated polyglot binding generator for Rust libraries
Documentation
use alef::backends::php::PhpBackend;
use alef::core::backend::Backend;
use alef::core::config::{Language, NewAlefConfig, ReadmeConfig};
use alef::core::ir::{ApiSurface, TypeDef};
use std::collections::HashMap;

fn excluded_visitor_config() -> alef::core::config::ResolvedCrateConfig {
    let raw: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["php"]

[[crates]]
name = "sample"
sources = ["src/lib.rs"]

[crates.php]
extension_name = "sample"
exclude_functions = ["visitor"]

[[crates.trait_bridges]]
trait_name = "HtmlVisitor"
type_alias = "VisitorHandle"
param_name = "visitor"
bind_via = "options_field"
options_type = "ConversionOptions"
context_type = "NodeContext"
result_type = "VisitResult"
exclude_languages = ["php"]
"#,
    )
    .expect("PHP visitor exclusion config must parse");
    raw.resolve()
        .expect("PHP visitor exclusion config must resolve")
        .remove(0)
}

fn visitor_api() -> ApiSurface {
    ApiSurface {
        crate_name: "sample".to_string(),
        version: "1.0.0".to_string(),
        types: vec![
            TypeDef {
                name: "HtmlVisitor".to_string(),
                rust_path: "sample::HtmlVisitor".to_string(),
                is_trait: true,
                ..TypeDef::default()
            },
            TypeDef {
                name: "VisitorHandle".to_string(),
                rust_path: "sample::VisitorHandle".to_string(),
                is_opaque: true,
                ..TypeDef::default()
            },
            TypeDef {
                name: "ConversionResult".to_string(),
                rust_path: "sample::ConversionResult".to_string(),
                is_opaque: true,
                ..TypeDef::default()
            },
        ],
        ..ApiSurface::default()
    }
}

#[test]
fn php_visitor_exclusion_removes_runtime_stub_and_reference_surfaces() {
    let config = excluded_visitor_config();
    let api = visitor_api();
    let backend = PhpBackend;
    let mut files = backend.generate_bindings(&api, &config).expect("generate PHP runtime");
    files.extend(backend.generate_public_api(&api, &config).expect("generate PHP facade"));
    files.extend(backend.generate_type_stubs(&api, &config).expect("generate PHP stubs"));
    let docs = alef::docs::generate_docs(&api, &config, &[Language::Php], "docs").expect("generate PHP docs");
    files.extend(docs.into_iter().filter(|file| file.path.ends_with("api-php.md")));

    let output = files.iter().map(|file| file.content.as_str()).collect::<String>();
    assert!(
        output.contains("ConversionResult"),
        "the negative control must prove PHP output was generated"
    );
    for excluded in ["HtmlVisitor", "VisitorHandle"] {
        assert!(
            !output.contains(excluded),
            "PHP's visitor pseudo-function exclusion must remove `{excluded}` from runtime registration, stubs, and docs:\n{output}"
        );
    }
}

#[test]
fn php_visitor_exclusion_overrides_readme_visitor_feature_flag() {
    let temp = tempfile::tempdir().expect("create README template directory");
    std::fs::write(
        temp.path().join("php.md"),
        "visitor={{ features.visitor_pattern }}\n{% if features.visitor_pattern %}visitor API{% endif %}\n",
    )
    .expect("write README template");

    let mut config = excluded_visitor_config();
    config.workspace_root = Some(temp.path().to_path_buf());
    config.readme = Some(ReadmeConfig {
        template_dir: Some(".".into()),
        snippets_dir: None,
        config: None,
        output_pattern: None,
        discord_url: None,
        banner_url: None,
        languages: HashMap::from([(
            "php".to_string(),
            serde_json::json!({
                "template": "php.md",
                "features": { "visitor_pattern": true }
            }),
        )]),
        targets: HashMap::new(),
    });

    let files = alef::readme::generate_readmes(&visitor_api(), &config, &[Language::Php]).expect("generate PHP README");
    assert_eq!(files.len(), 1, "one PHP README must be generated");
    assert!(
        files[0].content.contains("visitor=False"),
        "the configured feature must be overridden:\n{}",
        files[0].content
    );
    assert!(
        !files[0].content.contains("visitor API"),
        "the visitor feature block must be omitted"
    );
}