alef 0.103.17

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::Pyo3Backend;
use crate::core::backend::Backend;
use crate::core::config::{ResolvedCrateConfig, new_config::NewAlefConfig};
use crate::core::ir::{ApiSurface, EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use std::path::Path;

const CORE_SOURCE: &str = r#"
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub enum Policy {
    #[default]
    None,
    Analyze { label: String },
}
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Request { pub policy: Policy }
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Prepared { pub request: Request }
#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Batch { pub items: Vec<Prepared> }
"#;

const ROUND_TRIP_TEST: &str = r#"
#[test]
fn nested_enum_payload_survives_core_to_binding_and_serde_round_trips() {
    let expected = serde_json::json!({"items": [
        {"request": {"policy": {"Analyze": {"label": "retain me"}}}}
    ]});
    let core: test_lib::Batch = serde_json::from_value(expected.clone()).unwrap();
    let binding: Batch = core.into();
    assert_eq!(serde_json::to_value(&binding).unwrap(), expected);
    let restored: Batch = serde_json::from_value(expected.clone()).unwrap();
    assert_eq!(serde_json::to_value(restored).unwrap(), expected);
}
"#;

fn record(name: &str, field: &str, ty: TypeRef) -> TypeDef {
    TypeDef {
        name: name.into(),
        rust_path: format!("test_lib::{name}"),
        has_serde: true,
        has_default: true,
        fields: vec![FieldDef {
            name: field.into(),
            ty,
            ..Default::default()
        }],
        ..Default::default()
    }
}

fn api_surface() -> ApiSurface {
    ApiSurface {
        crate_name: "test-lib".into(),
        version: "0.1.0".into(),
        enums: vec![EnumDef {
            name: "Policy".into(),
            rust_path: "test_lib::Policy".into(),
            has_serde: true,
            has_default: true,
            variants: vec![
                EnumVariant {
                    name: "None".into(),
                    ..Default::default()
                },
                EnumVariant {
                    name: "Analyze".into(),
                    fields: vec![FieldDef {
                        name: "label".into(),
                        ty: TypeRef::String,
                        ..Default::default()
                    }],
                    ..Default::default()
                },
            ],
            ..Default::default()
        }],
        types: vec![
            record("Request", "policy", TypeRef::Named("Policy".into())),
            record("Prepared", "request", TypeRef::Named("Request".into())),
            record(
                "Batch",
                "items",
                TypeRef::Vec(Box::new(TypeRef::Named("Prepared".into()))),
            ),
        ],
        ..Default::default()
    }
}

fn fixture_config(binding_directory: &Path) -> ResolvedCrateConfig {
    let config: NewAlefConfig = toml::from_str(
        r#"
[workspace]
languages = ["python"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.python]
module_name = "_test_lib"
"#,
    )
    .unwrap();
    let mut config = config.resolve().unwrap().remove(0);
    config
        .output_paths
        .insert("python".into(), binding_directory.join("src"));
    config
}

fn write_fixture(root: &Path) -> std::path::PathBuf {
    let core = root.join("test-lib");
    let binding = root.join("test-lib-py");
    std::fs::create_dir_all(core.join("src")).unwrap();
    std::fs::create_dir_all(binding.join("src")).unwrap();
    std::fs::write(core.join("src/lib.rs"), CORE_SOURCE).unwrap();
    std::fs::write(
        core.join("Cargo.toml"),
        r#"
[package]
name = "test-lib"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", features = ["derive"] }
"#,
    )
    .unwrap();
    std::fs::write(
        binding.join("Cargo.toml"),
        r#"
[package]
name = "test-lib-py"
version = "0.1.0"
edition = "2024"
[dependencies]
pyo3 = "=0.29.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
test-lib = { path = "../test-lib" }
"#,
    )
    .unwrap();
    binding
}

#[test]
fn nested_data_enum_dtos_compile_and_round_trip() {
    let temporary_directory = tempfile::tempdir().unwrap();
    let binding = write_fixture(temporary_directory.path());
    let generated = Pyo3Backend
        .generate_bindings(&api_surface(), &fixture_config(&binding))
        .unwrap();
    let mut source = generated[0].content.clone();
    source.push_str(ROUND_TRIP_TEST);
    std::fs::write(binding.join("src/lib.rs"), &source).unwrap();
    let output = std::process::Command::new("cargo")
        .args(["test", "--quiet", "--manifest-path"])
        .arg(binding.join("Cargo.toml"))
        .env("CARGO_TARGET_DIR", temporary_directory.path().join("target"))
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "generated nested DTO bindings must compile and round-trip:\n{}\n{}\n{source}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr),
    );
}