alef 0.25.39

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::*;
use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{
    ApiSurface, EnumDef, EnumVariant, FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef,
};

fn param(name: &str, ty: TypeRef, is_ref: bool) -> ParamDef {
    ParamDef {
        name: name.to_string(),
        ty,
        is_ref,
        ..ParamDef::default()
    }
}

fn method(name: &str, params: Vec<ParamDef>, return_type: TypeRef) -> MethodDef {
    MethodDef {
        name: name.to_string(),
        params,
        return_type,
        doc: "Callback method.".to_string(),
        receiver: Some(ReceiverKind::RefMut),
        ..MethodDef::default()
    }
}

fn visitor_trait(name: &str, methods: Vec<MethodDef>) -> TypeDef {
    TypeDef {
        name: name.to_string(),
        rust_path: format!("my_lib::visitor::{name}"),
        methods,
        is_trait: true,
        ..TypeDef::default()
    }
}

fn bridge_config(
    trait_name: &str,
    options_type: &str,
    context_type: Option<&str>,
    result_type: Option<&str>,
) -> TraitBridgeConfig {
    TraitBridgeConfig {
        trait_name: trait_name.to_string(),
        type_alias: Some("VisitorHandle".to_string()),
        param_name: Some("visitor".to_string()),
        options_type: Some(options_type.to_string()),
        context_type: context_type.map(str::to_string),
        result_type: result_type.map(str::to_string),
        ..TraitBridgeConfig::default()
    }
}

fn protocol_api(context_name: &str, result_name: &str, default_variant: &str) -> ApiSurface {
    ApiSurface {
        crate_name: "my-lib".to_string(),
        version: "1.0.0".to_string(),
        types: vec![TypeDef {
            name: context_name.to_string(),
            rust_path: format!("my_lib::visitor::{context_name}"),
            fields: vec![
                FieldDef {
                    name: "tag_name".to_string(),
                    ty: TypeRef::String,
                    ..FieldDef::default()
                },
                FieldDef {
                    name: "depth".to_string(),
                    ty: TypeRef::Primitive(PrimitiveType::Usize),
                    ..FieldDef::default()
                },
            ],
            ..TypeDef::default()
        }],
        functions: vec![],
        enums: vec![EnumDef {
            name: result_name.to_string(),
            rust_path: format!("my_lib::visitor::{result_name}"),
            variants: vec![
                EnumVariant {
                    name: "Continue".to_string(),
                    is_default: default_variant == "Continue",
                    ..EnumVariant::default()
                },
                EnumVariant {
                    name: "Proceed".to_string(),
                    is_default: default_variant == "Proceed",
                    ..EnumVariant::default()
                },
                EnumVariant {
                    name: "Custom".to_string(),
                    fields: vec![FieldDef {
                        name: "value".to_string(),
                        ty: TypeRef::String,
                        ..FieldDef::default()
                    }],
                    ..EnumVariant::default()
                },
            ],
            ..EnumDef::default()
        }],
        errors: vec![],
        excluded_type_paths: Default::default(),
        excluded_trait_names: Default::default(),
        services: vec![],
        handler_contracts: vec![],
        unsupported_public_items: Vec::new(),
    }
}

#[test]
fn visitor_bindings_use_trait_name_and_callback_count_from_ir() {
    let trait_def = visitor_trait(
        "MarkdownVisitor",
        vec![method(
            "visit_text",
            vec![
                param("ctx", TypeRef::Named("NodeContext".to_string()), true),
                param("text", TypeRef::String, true),
            ],
            TypeRef::Named("VisitResult".to_string()),
        )],
    );

    let bridge_cfg = bridge_config(
        "MarkdownVisitor",
        "RenderOptions",
        Some("NodeContext"),
        Some("VisitResult"),
    );
    let api = protocol_api("NodeContext", "VisitResult", "Continue");
    let code = gen_visitor_bindings_with_api(
        "md",
        "my_lib",
        false,
        &trait_def,
        Some(&bridge_cfg),
        None,
        Some(&api),
        true,
    );

    assert!(code.contains("// Visitor / callback FFI — 1 MarkdownVisitor methods"));
    assert!(code.contains("dyn MarkdownVisitor + Send"));
    assert!(code.contains("options: *mut my_lib::RenderOptions"));
    assert!(code.contains("MD_VISIT_CUSTOM"));
    assert!(!code.contains("fn md_convert_with_visitor"));
    assert!(!code.contains("all 42 HtmlVisitor methods"));
    assert!(!code.contains("dyn HtmlVisitor + Send"));
    assert!(!code.contains("`HTM_VISIT_CUSTOM`"));
}

#[test]
fn visitor_bindings_skip_traits_without_node_context_visit_result_protocol() {
    let trait_def = visitor_trait(
        "PlainVisitor",
        vec![method(
            "visit_text",
            vec![
                param("context", TypeRef::Named("OtherContext".to_string()), true),
                param("text", TypeRef::String, true),
            ],
            TypeRef::String,
        )],
    );

    let bridge_cfg = bridge_config("PlainVisitor", "PlainOptions", None, None);
    let code = gen_visitor_bindings("pln", "my_lib", false, &trait_def, Some(&bridge_cfg), None);

    assert!(code.is_empty());
}

#[test]
fn visitor_bindings_use_configured_context_and_result_type_names() {
    let trait_def = visitor_trait(
        "RenderVisitor",
        vec![method(
            "visit_text",
            vec![
                param("context", TypeRef::Named("RenderContext".to_string()), true),
                param("text", TypeRef::String, true),
            ],
            TypeRef::Named("RenderDecision".to_string()),
        )],
    );
    let bridge_cfg = bridge_config(
        "RenderVisitor",
        "RenderOptions",
        Some("RenderContext"),
        Some("RenderDecision"),
    );
    let api = protocol_api("RenderContext", "RenderDecision", "Continue");

    let code = gen_visitor_bindings_with_api(
        "doc",
        "my_lib",
        false,
        &trait_def,
        Some(&bridge_cfg),
        None,
        Some(&api),
        true,
    );

    assert!(code.contains("ctx: &my_lib::visitor::RenderContext"));
    assert!(code.contains(") -> my_lib::visitor::RenderDecision"));
    assert!(code.contains("use my_lib::visitor::RenderDecision as VisitorResult"));
    assert!(code.contains("return my_lib::visitor::RenderDecision::Continue"));
    assert!(!code.contains("ctx: &my_lib::visitor::NodeContext"));
    assert!(!code.contains(") -> my_lib::visitor::VisitResult"));
}

#[test]
fn visitor_bindings_use_derived_default_result_variant() {
    let trait_def = visitor_trait(
        "RenderVisitor",
        vec![method(
            "visit_text",
            vec![param("context", TypeRef::Named("RenderContext".to_string()), true)],
            TypeRef::Named("RenderDecision".to_string()),
        )],
    );
    let bridge_cfg = bridge_config(
        "RenderVisitor",
        "RenderOptions",
        Some("RenderContext"),
        Some("RenderDecision"),
    );
    let api = ApiSurface {
        crate_name: "my-lib".to_string(),
        version: "1.0.0".to_string(),
        types: vec![TypeDef {
            name: "RenderContext".to_string(),
            rust_path: "my_lib::visitor::RenderContext".to_string(),
            fields: vec![FieldDef {
                name: "tag_name".to_string(),
                ty: TypeRef::String,
                ..FieldDef::default()
            }],
            ..TypeDef::default()
        }],
        functions: vec![],
        enums: vec![EnumDef {
            name: "RenderDecision".to_string(),
            rust_path: "my_lib::visitor::RenderDecision".to_string(),
            variants: vec![
                EnumVariant {
                    name: "Proceed".to_string(),
                    is_default: true,
                    ..EnumVariant::default()
                },
                EnumVariant {
                    name: "ReplaceWith".to_string(),
                    fields: vec![FieldDef {
                        name: "value".to_string(),
                        ty: TypeRef::String,
                        ..FieldDef::default()
                    }],
                    is_tuple: true,
                    ..EnumVariant::default()
                },
            ],
            ..EnumDef::default()
        }],
        errors: vec![],
        excluded_type_paths: Default::default(),
        excluded_trait_names: Default::default(),
        services: vec![],
        handler_contracts: vec![],
        unsupported_public_items: Vec::new(),
    };

    let code = gen_visitor_bindings_with_api(
        "doc",
        "my_lib",
        false,
        &trait_def,
        Some(&bridge_cfg),
        None,
        Some(&api),
        true,
    );

    assert!(code.contains("return my_lib::visitor::RenderDecision::Proceed"));
    assert!(code.contains("_ => my_lib::visitor::RenderDecision::Proceed"));
    assert!(!code.contains("RenderDecision::Continue"));
}