alef 0.36.2

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::*;

#[test]
fn bridge_handle_path_uses_alias_typedef_rust_path() {
    let mut api = ApiSurface::default();
    api.types.push(make_type_def(
        "RendererHandle",
        "mylib::callbacks::RendererHandle",
        vec![],
    ));
    let bridge = make_bridge(
        Some("RendererHandle"),
        Some("renderer"),
        BridgeBinding::FunctionParam,
        None,
        None,
        None,
        None,
    );

    assert_eq!(
        bridge_handle_path(&api, &bridge, "mylib"),
        "mylib::callbacks::RendererHandle"
    );
}

#[test]
fn bridge_handle_path_uses_excluded_alias_path() {
    let mut api = ApiSurface::default();
    api.excluded_type_paths.insert(
        "RendererHandle".to_string(),
        "mylib::callbacks::RendererHandle".to_string(),
    );
    let bridge = make_bridge(
        Some("RendererHandle"),
        Some("renderer"),
        BridgeBinding::FunctionParam,
        None,
        None,
        None,
        None,
    );

    assert_eq!(
        bridge_handle_path(&api, &bridge, "mylib"),
        "mylib::callbacks::RendererHandle"
    );
}

#[test]
fn managed_function_lookup_matches_trait_bridge_lifecycle_names() {
    let bridge = TraitBridgeConfig {
        register_fn: Some("register_renderer".to_string()),
        unregister_fn: Some("unregister_renderer".to_string()),
        clear_fn: Some("clear_renderers".to_string()),
        ..TraitBridgeConfig::default()
    };
    let bridges = vec![bridge];

    assert!(is_trait_bridge_managed_fn("register_renderer", &bridges));
    assert!(is_trait_bridge_managed_fn("unregister_renderer", &bridges));
    assert!(is_trait_bridge_managed_fn("clear_renderers", &bridges));
    assert!(!is_trait_bridge_managed_fn("list_renderers", &bridges));
}

fn make_bridge(
    type_alias: Option<&str>,
    param_name: Option<&str>,
    bind_via: BridgeBinding,
    options_type: Option<&str>,
    options_field: Option<&str>,
    context_type: Option<&str>,
    result_type: Option<&str>,
) -> TraitBridgeConfig {
    TraitBridgeConfig {
        trait_name: "HtmlVisitor".to_string(),
        super_trait: None,
        registry_getter: None,
        register_fn: None,
        unregister_fn: None,
        clear_fn: None,
        type_alias: type_alias.map(str::to_string),
        param_name: param_name.map(str::to_string),
        register_extra_args: None,
        exclude_languages: vec![],
        ffi_skip_methods: Vec::new(),
        bind_via,
        options_type: options_type.map(str::to_string),
        options_field: options_field.map(str::to_string),
        context_type: context_type.map(str::to_string),
        result_type: result_type.map(str::to_string),
    }
}

#[test]
fn find_bridge_param_returns_first_param_match_in_function_param_mode() {
    let func = make_func(
        "convert",
        vec![
            make_param("html", TypeRef::String, true),
            make_param("visitor", TypeRef::Named("VisitorHandle".to_string()), false),
        ],
    );
    let bridges = vec![make_bridge(
        Some("VisitorHandle"),
        Some("visitor"),
        BridgeBinding::FunctionParam,
        None,
        None,
        None,
        None,
    )];
    let result = find_bridge_param(&func, &bridges).expect("bridge match");
    assert_eq!(result.0, 1);
}

#[test]
fn find_bridge_param_skips_options_field_bridges() {
    let func = make_func(
        "convert",
        vec![
            make_param("html", TypeRef::String, true),
            make_param("visitor", TypeRef::Named("VisitorHandle".to_string()), false),
        ],
    );
    let bridges = vec![make_bridge(
        Some("VisitorHandle"),
        Some("visitor"),
        BridgeBinding::OptionsField,
        Some("ConversionOptions"),
        Some("visitor"),
        None,
        None,
    )];
    assert!(
        find_bridge_param(&func, &bridges).is_none(),
        "bridges configured with bind_via=options_field must not be returned by find_bridge_param"
    );
}

#[test]
fn find_bridge_field_detects_field_via_alias() {
    let opts_type = TypeDef {
        name: "ConversionOptions".to_string(),
        rust_path: "mylib::ConversionOptions".to_string(),
        original_rust_path: String::new(),
        fields: vec![
            make_field("debug", TypeRef::Primitive(PrimitiveType::Bool)),
            make_field(
                "visitor",
                TypeRef::Optional(Box::new(TypeRef::Named("VisitorHandle".to_string()))),
            ),
        ],
        methods: vec![],
        is_opaque: false,
        is_clone: true,
        is_copy: false,
        doc: String::new(),
        cfg: None,
        is_trait: false,
        has_default: true,
        has_stripped_cfg_fields: false,
        is_return_type: false,
        serde_rename_all: None,
        has_serde: false,
        super_traits: vec![],
        binding_excluded: false,
        binding_exclusion_reason: None,
        is_variant_wrapper: false,
        has_lifetime_params: false,
        has_private_fields: false,
        version: Default::default(),
    };
    let func = make_func(
        "convert",
        vec![
            make_param("html", TypeRef::String, true),
            make_param(
                "options",
                TypeRef::Optional(Box::new(TypeRef::Named("ConversionOptions".to_string()))),
                false,
            ),
        ],
    );
    let bridges = vec![make_bridge(
        Some("VisitorHandle"),
        Some("visitor"),
        BridgeBinding::OptionsField,
        Some("ConversionOptions"),
        None,
        None,
        None,
    )];
    let m = find_bridge_field(&func, std::slice::from_ref(&opts_type), &bridges).expect("bridge field match");
    assert_eq!(m.param_index, 1);
    assert_eq!(m.param_name, "options");
    assert_eq!(m.options_type, "ConversionOptions");
    assert!(m.param_is_optional);
    assert_eq!(m.field_name, "visitor");
}

#[test]
fn find_bridge_field_returns_none_for_function_param_bridge() {
    let opts_type = TypeDef {
        name: "ConversionOptions".to_string(),
        rust_path: "mylib::ConversionOptions".to_string(),
        original_rust_path: String::new(),
        fields: vec![make_field(
            "visitor",
            TypeRef::Optional(Box::new(TypeRef::Named("VisitorHandle".to_string()))),
        )],
        methods: vec![],
        is_opaque: false,
        is_clone: true,
        is_copy: false,
        doc: String::new(),
        cfg: None,
        is_trait: false,
        has_default: true,
        has_stripped_cfg_fields: false,
        is_return_type: false,
        serde_rename_all: None,
        has_serde: false,
        super_traits: vec![],
        binding_excluded: false,
        binding_exclusion_reason: None,
        is_variant_wrapper: false,
        has_lifetime_params: false,
        has_private_fields: false,
        version: Default::default(),
    };
    let func = make_func(
        "convert",
        vec![make_param(
            "options",
            TypeRef::Named("ConversionOptions".to_string()),
            false,
        )],
    );
    let bridges = vec![make_bridge(
        Some("VisitorHandle"),
        Some("visitor"),
        BridgeBinding::FunctionParam,
        None,
        None,
        None,
        None,
    )];
    assert!(find_bridge_field(&func, std::slice::from_ref(&opts_type), &bridges).is_none());
}

/// Plain data-struct TypeDef (not a trait, not opaque) with configurable serde / exclusion.
fn make_struct_def(name: &str, has_serde: bool, is_opaque: bool, binding_excluded: bool) -> TypeDef {
    TypeDef {
        name: name.to_string(),
        rust_path: format!("mylib::{name}"),
        is_trait: false,
        is_opaque,
        has_serde,
        binding_excluded,
        ..TypeDef::default()
    }
}

#[test]
fn native_marshalled_struct_params_allowlists_only_known_serde_structs() {
    let greet = MethodDef {
        name: "greet".to_string(),
        params: vec![
            make_param("opts", TypeRef::Named("Opts".to_string()), true),
            make_param("mood", TypeRef::Named("Mood".to_string()), true),
            make_param("handle", TypeRef::Named("Handle".to_string()), true),
            make_param("hidden", TypeRef::Named("Hidden".to_string()), true),
            make_param("plain", TypeRef::Named("Plain".to_string()), true),
            make_param("name", TypeRef::String, true),
            make_param("unknown", TypeRef::Named("Unknown".to_string()), true),
        ],
        return_type: TypeRef::Named("Doc".to_string()),
        receiver: Some(ReceiverKind::Ref),
        ..MethodDef::default()
    };
    let trait_def = TypeDef {
        name: "Greeter".to_string(),
        rust_path: "mylib::Greeter".to_string(),
        is_trait: true,
        methods: vec![greet],
        ..TypeDef::default()
    };

    let api = ApiSurface {
        types: vec![
            make_struct_def("Opts", true, false, false),
            make_struct_def("Handle", true, true, false),
            make_struct_def("Hidden", true, false, true),
            make_struct_def("Plain", false, false, false),
            make_struct_def("Doc", true, false, false),
        ],
        enums: vec![EnumDef {
            name: "Mood".to_string(),
            ..EnumDef::default()
        }],
        ..ApiSurface::default()
    };

    let structs = native_marshalled_struct_params(&trait_def, &api);
    assert!(
        structs.contains("Opts"),
        "known serde struct must be allowlisted: {structs:?}"
    );
    assert_eq!(structs.len(), 1, "only `Opts` qualifies, got {structs:?}");
    for excluded in ["Mood", "Handle", "Hidden", "Plain", "Unknown", "Doc"] {
        assert!(
            !structs.contains(excluded),
            "{excluded} must NOT be native-marshalled: {structs:?}"
        );
    }

    assert!(is_native_marshalled_struct("Opts", &api));
    assert!(!is_native_marshalled_struct("Handle", &api));
    assert!(!is_native_marshalled_struct("Hidden", &api));
    assert!(!is_native_marshalled_struct("Plain", &api));
    assert!(!is_native_marshalled_struct("Unknown", &api));
}

#[test]
fn find_trait_def_locates_bridge_trait_by_name() {
    let trait_def = TypeDef {
        name: "Greeter".to_string(),
        rust_path: "mylib::Greeter".to_string(),
        is_trait: true,
        methods: vec![MethodDef {
            name: "process".to_string(),
            ..MethodDef::default()
        }],
        ..TypeDef::default()
    };
    let api = ApiSurface {
        types: vec![
            TypeDef {
                name: "Greeter".to_string(),
                is_trait: false,
                ..TypeDef::default()
            },
            trait_def,
            TypeDef {
                name: "Other".to_string(),
                is_trait: true,
                ..TypeDef::default()
            },
        ],
        ..ApiSurface::default()
    };
    let bridge = TraitBridgeConfig {
        trait_name: "Greeter".to_string(),
        register_fn: Some("register_greeter".to_string()),
        bind_via: BridgeBinding::FunctionParam,
        ..TraitBridgeConfig::default()
    };

    let found = find_trait_def(&bridge, &api).expect("trait def must be found");
    assert!(
        found.is_trait,
        "must resolve the trait, not the struct of the same name"
    );
    assert_eq!(found.methods.len(), 1);
    assert_eq!(found.methods[0].name, "process");

    let missing = TraitBridgeConfig {
        trait_name: "Absent".to_string(),
        ..TraitBridgeConfig::default()
    };
    assert!(find_trait_def(&missing, &api).is_none());
}