alef 0.85.15

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::core::config::{NewAlefConfig, ResolvedCrateConfig};
use crate::core::ir::{ApiSurface, EnumDef, EnumVariant, FieldDef, FunctionDef, ParamDef, TypeRef};
use std::path::Path;

pub(super) fn surface(shared_function: bool) -> ApiSurface {
    ApiSurface {
        crate_name: "core-lib".into(),
        version: "0.1.0".into(),
        enums: vec![
            EnumDef {
                name: "Backend".into(),
                rust_path: "core_lib::Backend".into(),
                serde_tag: Some("kind".into()),
                variants: vec![
                    EnumVariant {
                        name: "Memory".into(),
                        is_default: true,
                        ..Default::default()
                    },
                    EnumVariant {
                        name: "Remote".into(),
                        cfg: Some(r#"feature = "remote""#.into()),
                        fields: vec![FieldDef {
                            name: "endpoint".into(),
                            ty: TypeRef::String,
                            ..Default::default()
                        }],
                        ..Default::default()
                    },
                    EnumVariant {
                        name: "RemoteUnit".into(),
                        cfg: Some(r#"feature = "remote""#.into()),
                        ..Default::default()
                    },
                ],
                ..Default::default()
            },
            EnumDef {
                name: "Mode".into(),
                rust_path: "core_lib::Mode".into(),
                variants: vec![
                    EnumVariant {
                        name: "Local".into(),
                        is_default: true,
                        ..Default::default()
                    },
                    EnumVariant {
                        name: "Remote".into(),
                        cfg: Some(r#"feature = "remote""#.into()),
                        ..Default::default()
                    },
                ],
                ..Default::default()
            },
        ],
        functions: if shared_function {
            vec![FunctionDef {
                params: vec![ParamDef {
                    name: "mode".into(),
                    ty: TypeRef::Named("Mode".into()),
                    ..Default::default()
                }],
                name: "remote_enabled".into(),
                rust_path: "core_lib::remote_enabled".into(),
                cfg: Some(r#"feature = "remote""#.into()),
                ..Default::default()
            }]
        } else {
            vec![]
        },
        ..Default::default()
    }
}

pub(super) fn config(root: &Path, enabled: bool) -> ResolvedCrateConfig {
    let options = if enabled {
        "features = [\"full\"]"
    } else {
        "excluded_default_features = [\"remote\"]"
    };
    let source = format!(
        "[workspace]\nlanguages=[\"php\"]\n[[crates]]\nname=\"core-lib\"\nsources=[\"crates/core-lib/src/lib.rs\"]\n[crates.php]\n{options}\n"
    );
    let cfg: NewAlefConfig = toml::from_str(&source).expect("valid config");
    let mut config = cfg.resolve().expect("resolve config").remove(0);
    config.workspace_root = Some(root.to_path_buf());
    config
}

#[test]
fn shared_function_feature_remains_unconditionally_core_owned() {
    let features = crate::scaffold::languages::php::php_declared_features(&surface(true), &[]);
    assert!(features.is_empty(), "{features:?}");
}

#[test]
fn function_only_feature_is_not_made_toggleable() {
    let mut api = surface(true);
    api.enums.clear();
    assert!(crate::scaffold::languages::php::php_declared_features(&api, &[]).is_empty());
}

pub(super) fn write_fixture(root: &Path, enabled: bool, target: Option<bool>, opt_in: bool, aggregate: bool) {
    let core = root.join("crates/core-lib");
    let binding = root.join("crates/core-lib-php");
    std::fs::create_dir_all(core.join("src")).expect("core directory");
    std::fs::create_dir_all(binding.join("src")).expect("binding directory");
    std::fs::write(
        core.join("Cargo.toml"),
        "[package]\nname=\"core-lib\"\nversion=\"0.1.0\"\nedition=\"2024\"\n[features]\nfull=[\"remote\"]\nremote=[]\nextra=[\"remote\"]\n",
    )
    .expect("core manifest");
    std::fs::write(
        core.join("src/lib.rs"),
        r#"
#[derive(Debug, Default)]
pub enum Backend { #[default] Memory, #[cfg(feature="remote")] Remote { endpoint: String }, #[cfg(feature="remote")] RemoteUnit, #[cfg(feature="extra")] Aggregate }
"#,
    )
    .expect("core source");
    let mut api = surface((enabled && target.is_none()) || aggregate);
    if aggregate {
        api.enums[0].variants.push(EnumVariant {
            name: "Aggregate".into(),
            cfg: Some(r#"feature = "extra""#.into()),
            ..Default::default()
        });
    }
    let mut config = config(root, enabled);
    if let Some(target_enabled) = target {
        config
            .php
            .as_mut()
            .expect("PHP config")
            .target_dep_overrides
            .push(crate::core::config::FfiTargetDepOverride {
                cfg: format!("target_os = \"{}\"", std::env::consts::OS),
                features: if target_enabled { vec!["remote".into()] } else { vec![] },
                default_features: false,
            });
    }
    let files = crate::scaffold::languages::php::scaffold_php_cargo(&api, &config).expect("scaffold");
    let manifest = files
        .iter()
        .find(|file| file.path.ends_with("Cargo.toml"))
        .expect("manifest");
    let mut manifest: toml::Value = toml::from_str(&manifest.content).expect("generated TOML");
    manifest["dependencies"]
        .as_table_mut()
        .expect("dependencies")
        .retain(|name, _| name == "core-lib");
    manifest.as_table_mut().expect("manifest table").remove("lib");
    std::fs::write(
        binding.join("Cargo.toml"),
        toml::to_string(&manifest).expect("fixture manifest"),
    )
    .expect("write manifest");
    use crate::core::backend::Backend;
    let generated = super::PhpBackend.generate_bindings(&api, &config).expect("bindings");
    let source = &generated
        .iter()
        .find(|file| file.path.ends_with("lib.rs"))
        .expect("lib.rs")
        .content;
    let mut conversion = String::new();
    for marker in [
        "impl From<core_lib::Backend> for Backend",
        "impl From<Backend> for core_lib::Backend",
    ] {
        let start = source.find(marker).expect("conversion start");
        let end = source[start..].find("\n}").expect("conversion end") + start + 2;
        conversion.push_str(&source[start..end]);
        conversion.push('\n');
    }
    let exercise = if (aggregate && opt_in) || target.unwrap_or(enabled || opt_in) {
        r#"let unit = Backend::from(core_lib::Backend::RemoteUnit); assert_eq!(unit.kind_tag, "RemoteUnit"); assert!(matches!(core_lib::Backend::from(unit), core_lib::Backend::RemoteUnit)); let value = Backend::from(core_lib::Backend::Remote { endpoint: "identity".into() }); assert_eq!(value.endpoint.as_deref(), Some("identity")); assert!(matches!(core_lib::Backend::from(value), core_lib::Backend::Remote { endpoint } if endpoint == "identity"));"#
    } else {
        r#"let value = Backend::from(core_lib::Backend::Memory); assert_eq!(value.kind_tag, "Memory"); assert!(matches!(core_lib::Backend::from(value), core_lib::Backend::Memory));"#
    };
    let source = format!(
        "#[derive(Default)]\nstruct Backend {{ kind_tag: String, endpoint: Option<String> }}\n{conversion}\nfn main() {{ {exercise} }}\n"
    );
    std::fs::write(binding.join("src/main.rs"), source).expect("binding source");
}

fn compile_fixture(enabled: bool, no_defaults: bool, target: Option<bool>, opt_in: bool, aggregate: bool) {
    let root = tempfile::tempdir().expect("fixture directory");
    write_fixture(root.path(), enabled, target, opt_in, aggregate);
    let mut command = std::process::Command::new("cargo");
    command
        .args(["run", "--offline", "--quiet", "--manifest-path"])
        .arg(root.path().join("crates/core-lib-php/Cargo.toml"))
        .env("CARGO_TARGET_DIR", root.path().join("target"))
        .env("CARGO_BUILD_JOBS", "1")
        .env("RUSTFLAGS", "-Dunexpected_cfgs");
    if no_defaults {
        command.arg("--no-default-features");
    }
    if opt_in {
        command.args(["--features", if aggregate { "extra" } else { "remote" }]);
    }
    let (success, output) = crate::snippets::validators::run_command(&mut command, 60)
        .expect("compile and execute generated conversion within deadline");
    assert!(success, "{output}");
}

#[test]
fn emitted_conversion_compiles_with_transitive_full_feature() {
    compile_fixture(true, false, None, false, false);
}

#[test]
fn emitted_conversion_compiles_with_optional_feature_disabled() {
    compile_fixture(false, true, None, false, false);
}

#[test]
fn emitted_conversion_preserves_forced_core_feature_without_binding_defaults() {
    compile_fixture(true, true, None, false, false);
}

#[test]
fn emitted_conversion_supports_optional_feature_opt_in() {
    compile_fixture(false, true, None, true, false);
}

#[test]
fn emitted_conversion_respects_target_override_disabling_core_feature() {
    compile_fixture(true, true, Some(false), false, false);
}

#[test]
fn emitted_conversion_respects_target_override_enabling_core_feature() {
    compile_fixture(false, true, Some(true), false, false);
}

#[test]
fn aggregate_passthrough_enables_transitive_core_variant_on_overridden_target() {
    compile_fixture(true, true, Some(false), true, true);
}