alef 0.107.2

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::*;
use crate::core::config::languages::RubyConfig;
use crate::core::ir::TypeDef;

fn config_with_core_manifest(features: &[&str]) -> (tempfile::TempDir, ResolvedCrateConfig) {
    let dir = tempfile::tempdir().expect("tempdir");
    let mut config = test_config();
    config.workspace_root = Some(dir.path().to_path_buf());
    config.sources = vec![PathBuf::from("crates/my-lib/src/lib.rs")];
    let crate_dir = dir.path().join("crates/my-lib");
    std::fs::create_dir_all(&crate_dir).expect("core crate directory");
    let feature_rows = features
        .iter()
        .map(|feature| format!("{feature} = []"))
        .collect::<Vec<_>>()
        .join("\n");
    std::fs::write(
        crate_dir.join("Cargo.toml"),
        format!("[package]\nname = \"my-lib\"\nversion = \"0.1.0\"\n\n[features]\n{feature_rows}\n"),
    )
    .expect("core Cargo.toml");
    (dir, config)
}

#[test]
fn native_wrappers_preserve_configured_defaults_without_restoring_hidden_api() {
    let (_dir, mut config) = config_with_core_manifest(&["mcp-http", "hidden-api"]);
    config.languages = vec![
        Language::Python,
        Language::Node,
        Language::Ruby,
        Language::Php,
        Language::Elixir,
        Language::Ffi,
    ];
    config.wrapper_default_features = vec!["mcp-http".to_string()];
    let api = ApiSurface {
        crate_name: config.name.clone(),
        version: "0.1.0".to_string(),
        types: vec![TypeDef {
            name: "RustOnlyFormulaOptions".to_string(),
            rust_path: "my_lib::RustOnlyFormulaOptions".to_string(),
            cfg: Some(r#"feature = "hidden-api""#.to_string()),
            binding_excluded: true,
            ..Default::default()
        }],
        ..Default::default()
    };

    let files = scaffold(&api, &config, &config.languages).expect("native wrapper scaffold");
    let cargo_manifests: Vec<_> = files
        .iter()
        .filter(|file| file.path.file_name().is_some_and(|name| name == "Cargo.toml"))
        .filter(|file| {
            let path = crate::test_support::portable_path_string(&file.path);
            path.contains("-py/")
                || path.contains("-node/")
                || path.contains("-rb/")
                || path.contains("-php/")
                || path.contains("/native/")
                || path.contains("-ffi/")
        })
        .collect();
    assert_eq!(
        cargo_manifests.len(),
        6,
        "all six native wrapper manifests must be emitted"
    );

    for manifest in cargo_manifests {
        assert!(
            manifest.content.contains(r#"mcp-http = ["my-lib/mcp-http"]"#),
            "{} must forward the configured wrapper default:\n{}",
            manifest.path.display(),
            manifest.content
        );
        let default_line = manifest
            .content
            .lines()
            .find(|line| line.starts_with("default = ["))
            .expect("wrapper manifest default feature array");
        assert!(
            default_line.contains(r#""mcp-http""#),
            "{} must enable the configured wrapper default: {default_line}",
            manifest.path.display()
        );
        assert!(
            !manifest.content.contains("hidden-api"),
            "{} must not restore a projected-out API feature:\n{}",
            manifest.path.display(),
            manifest.content
        );
    }

    assert!(
        files
            .iter()
            .filter(|file| file.path.file_name().is_none_or(|name| name != "Cargo.toml"))
            .all(|file| !file.content.contains("RustOnlyFormulaOptions")),
        "the manifest-only feature declaration must not restore a binding-excluded Rust API"
    );
}

#[test]
fn fresh_scaffold_rejects_wrapper_default_excluded_by_a_native_language() {
    let (_dir, mut config) = config_with_core_manifest(&["formula-recognition"]);
    config.languages = vec![Language::Ruby];
    config.wrapper_default_features = vec!["formula-recognition".to_string()];
    config.ruby = Some(
        toml::from_str::<RubyConfig>("gem_name = \"my_lib\"\nexcluded_default_features = [\"formula-recognition\"]\n")
            .expect("Ruby config"),
    );

    let error = scaffold(&ApiSurface::default(), &config, &config.languages)
        .expect_err("conflicting wrapper defaults must fail before manifest emission");

    assert!(
        error.to_string().contains(
            "wrapper_default_features feature `formula-recognition` conflicts with \
             `[crates.ruby].excluded_default_features`"
        ),
        "the error must identify both conflicting settings: {error:#}"
    );
}

#[test]
fn fresh_scaffold_rejects_invalid_or_unknown_wrapper_default_features() {
    for (feature, expected) in [
        ("", "must not be empty"),
        ("dep:escape", "may only contain ASCII letters"),
        ("evil\"]\nforged = [\"x", "may only contain ASCII letters"),
        (
            "unknown-feature",
            "is not declared in the core crate's `[features]` table",
        ),
    ] {
        let (_dir, mut config) = config_with_core_manifest(&["formula-recognition"]);
        config.languages = vec![Language::Python];
        config.wrapper_default_features = vec![feature.to_string()];

        let error = scaffold(&ApiSurface::default(), &config, &config.languages)
            .expect_err("an invalid wrapper default must fail before manifest emission");
        assert!(
            error.to_string().contains(expected),
            "`{feature}` must fail with `{expected}`, got: {error:#}"
        );
    }
}

#[test]
fn fresh_scaffold_rejects_reserved_wrapper_default_features() {
    for feature in ["default", "extension-module"] {
        let (_dir, mut config) = config_with_core_manifest(&[feature]);
        config.languages = vec![Language::Python];
        config.wrapper_default_features = vec![feature.to_string()];

        let error = scaffold(&ApiSurface::default(), &config, &config.languages)
            .expect_err("reserved wrapper defaults must fail before manifest emission");
        assert!(
            error
                .to_string()
                .contains("is reserved by generated native wrapper manifests"),
            "`{feature}` must be rejected as reserved, got: {error:#}"
        );
    }
}

#[test]
fn fresh_scaffold_ignores_exclusions_for_inactive_languages() {
    let (_dir, mut config) = config_with_core_manifest(&["formula-recognition"]);
    config.languages = vec![Language::Python];
    config.wrapper_default_features = vec!["formula-recognition".to_string()];
    config.ruby = Some(
        toml::from_str::<RubyConfig>("gem_name = \"my_lib\"\nexcluded_default_features = [\"formula-recognition\"]\n")
            .expect("Ruby config"),
    );

    let files = scaffold(&ApiSurface::default(), &config, &config.languages)
        .expect("an inactive Ruby exclusion must not invalidate Python scaffolding");
    let python_manifest = files
        .iter()
        .find(|file| crate::test_support::portable_path_string(&file.path).ends_with("my-lib-py/Cargo.toml"))
        .expect("Python Cargo.toml");
    assert!(
        python_manifest
            .content
            .contains(r#"formula-recognition = ["my-lib/formula-recognition"]"#),
        "Python must still forward the configured default:\n{}",
        python_manifest.content
    );
}