alef 0.36.2

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

fn make_param(name: &str, ty: TypeRef, is_ref: bool, is_mut: bool, optional: bool) -> ParamDef {
    ParamDef {
        name: name.to_string(),
        ty,
        optional,
        default: None,
        sanitized: false,
        typed_default: None,
        is_ref,
        is_mut,
        newtype_wrapper: None,
        original_type: None,
        map_is_ahash: false,
        map_key_is_cow: false,
        vec_inner_is_ref: false,
        map_is_btree: false,
        core_wrapper: crate::core::ir::CoreWrapper::None,
    }
}

#[test]
fn is_mut_named_opaque_emits_mut_inner() {
    let p = make_param(
        "result",
        TypeRef::Named("ExtractionResult".to_string()),
        false,
        true,
        false,
    );
    let mut opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    opaque.insert("ExtractionResult".to_string());
    let needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
    let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();

    let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
    assert_eq!(expr, "&mut result.inner", "is_mut opaque param must use &mut: {expr}");
}

#[test]
fn is_mut_named_from_emits_mut_borrow() {
    let p = make_param(
        "cfg",
        TypeRef::Named("TranslationConfig".to_string()),
        false,
        true,
        false,
    );
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let mut needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
    needs_from.insert("TranslationConfig".to_string());
    let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();

    let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
    assert!(
        expr.contains("&mut"),
        "is_mut From-converted Named param must emit &mut borrow: {expr}"
    );
}

#[test]
fn is_mut_named_transmute_emits_mut_transmute() {
    let p = make_param("config", TypeRef::Named("MyConfig".to_string()), false, true, false);
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
    let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();

    let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
    assert!(
        expr.contains("&mut"),
        "is_mut transmute Named param must emit &mut transmute: {expr}"
    );
    assert!(
        expr.contains("transmute"),
        "is_mut transmute Named param must emit transmute: {expr}"
    );
}

#[test]
fn vec_named_is_ref_emits_slice_not_raw_pointer() {
    let p = make_param(
        "categories",
        TypeRef::Vec(Box::new(TypeRef::Named("PiiCategory".to_string()))),
        true,
        false,
        false,
    );
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let needs_from: std::collections::HashSet<String> = std::collections::HashSet::new();
    let type_paths: std::collections::HashMap<String, String> = std::collections::HashMap::new();

    let expr = dart_call_arg_with_mirror_transmute(&p, "mylib", &type_paths, &needs_from, &opaque);
    assert!(
        expr.contains("from_raw_parts"),
        "Vec<Named> is_ref must use slice::from_raw_parts, got: {expr}"
    );
    assert!(
        expr.contains(".len()"),
        "Vec<Named> is_ref must include .len() for slice bounds, got: {expr}"
    );
}

#[test]
fn collect_in_return_transmute_vec_has_type_annotation() {
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let ty = TypeRef::Vec(Box::new(TypeRef::Named("QrCode".to_string())));
    let transform = return_transform(&ty, "mylib", &std::collections::HashMap::new(), &opaque, false);
    let suffix = match &transform {
        RetTransform::Suffix(s) => s.clone(),
        other => panic!("expected Suffix, got {other:?}"),
    };
    assert!(
        suffix.contains("collect::<Vec<_>>()"),
        "Vec<Named> collect must have type annotation: {suffix}"
    );
}

#[test]
fn vec_named_return_transform_is_suffix_not_closure_call() {
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let ty = TypeRef::Vec(Box::new(TypeRef::Named("QrCode".to_string())));
    let transform = return_transform(&ty, "mylib", &std::collections::HashMap::new(), &opaque, false);
    match &transform {
        RetTransform::Suffix(s) => {
            assert!(
                s.starts_with(".into_iter()"),
                "expected suffix starting with .into_iter(), got {s}"
            );
            assert!(s.contains("QrCode::from"), "expected QrCode::from in suffix, got {s}");
            assert!(!s.contains("|v"), "suffix must not contain a closure literal, got {s}");
        }
        other => panic!("expected Suffix, got {other:?}"),
    }
    let body = build_body("sample_crate::detect(&x)", "", &transform, false, false, false);
    assert!(
        !body.contains("|v: Vec<_>|"),
        "body must not emit closure-literal wrap: {body}"
    );
    assert!(
        body.contains("sample_crate::detect(&x).into_iter().map(QrCode::from).collect::<Vec<_>>()"),
        "body must apply suffix directly to call: {body}"
    );
}

#[test]
fn vec_named_returns_ref_emits_iter_not_into_iter() {
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let ty = TypeRef::Vec(Box::new(TypeRef::Named("Foo".to_string())));
    let transform = return_transform(&ty, "mylib", &std::collections::HashMap::new(), &opaque, true);
    match transform {
        RetTransform::Suffix(s) => {
            assert!(
                s.starts_with(".iter()"),
                "ref-return Vec<Named> must start with .iter(): {s}"
            );
            assert!(!s.contains(".into_iter()"), "ref-return must not use .into_iter(): {s}");
        }
        other => panic!("expected Suffix, got {other:?}"),
    }
}

#[test]
fn option_named_return_transform_is_suffix_not_closure_call() {
    let opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let ty = TypeRef::Optional(Box::new(TypeRef::Named("EmbeddingPreset".to_string())));
    let transform = return_transform(&ty, "mylib", &std::collections::HashMap::new(), &opaque, false);
    match &transform {
        RetTransform::Suffix(s) => {
            assert_eq!(s, ".map(EmbeddingPreset::from)");
        }
        other => panic!("expected Suffix, got {other:?}"),
    }
    let body = build_body("sample_crate::get(&n)", "", &transform, false, false, false);
    assert!(
        !body.contains("|v: Option<_>|"),
        "body must not emit closure-literal wrap: {body}"
    );
    assert!(
        body.contains("sample_crate::get(&n).map(EmbeddingPreset::from)"),
        "body must apply suffix directly to call: {body}"
    );
}

#[test]
fn scalar_named_return_transform_does_not_emit_closure_call() {
    let mut opaque: std::collections::HashSet<String> = std::collections::HashSet::new();
    let ty_named = TypeRef::Named("Foo".to_string());

    let t = return_transform(&ty_named, "mylib", &std::collections::HashMap::new(), &opaque, false);
    let body = build_body("sample_crate::foo()", "", &t, false, false, false);
    assert!(
        body.contains("Foo::from(sample_crate::foo())"),
        "sync scalar Named must emit direct call, got: {body}"
    );
    assert!(!body.contains("(Foo::from)("), "must not use (path)(expr) wrap: {body}");

    opaque.insert("Foo".to_string());
    let t = return_transform(&ty_named, "mylib", &std::collections::HashMap::new(), &opaque, false);
    let body = build_body("sample_crate::foo()", "", &t, false, false, false);
    assert!(
        body.contains("Foo { inner: sample_crate::foo() }"),
        "sync scalar opaque Named must emit struct literal, got: {body}"
    );
    assert!(!body.contains("|inner|"), "must not emit closure: {body}");
}

#[test]
fn path_vec_result_cast_uses_to_string_lossy() {
    let ty = TypeRef::Vec(Box::new(TypeRef::Path));
    let cast = build_primitive_result_cast(&ty, false);
    assert!(
        cast.contains("to_string_lossy"),
        "Vec<Path> cast must use to_string_lossy: {cast}"
    );
    assert!(
        !cast.contains(".to_string()"),
        "Vec<Path> must NOT use .to_string(): {cast}"
    );
}

#[test]
fn vec_string_returns_ref_result_cast_uses_iter_not_into_iter() {
    let ty = TypeRef::Vec(Box::new(TypeRef::String));
    let cast_owned = build_primitive_result_cast(&ty, false);
    let cast_ref = build_primitive_result_cast(&ty, true);
    assert!(
        cast_owned.starts_with(".into_iter()"),
        "owned must use .into_iter(): {cast_owned}"
    );
    assert!(cast_ref.starts_with(".iter()"), "ref must use .iter(): {cast_ref}");
    assert!(
        !cast_ref.contains(".into_iter()"),
        "ref must not use .into_iter(): {cast_ref}"
    );
}

#[test]
fn scalar_path_result_cast_uses_display_not_to_string() {
    let ty = TypeRef::Path;
    let cast = build_primitive_result_cast(&ty, false);
    assert!(cast.contains("display()"), "Path cast must use .display(): {cast}");
    assert!(cast.contains("to_string()"), "Path cast must use to_string(): {cast}");
}

#[test]
fn unit_return_no_error_emits_statement_not_expression() {
    let transform = RetTransform::None;
    let body_sync = build_body("sample_crate::clear()", "", &transform, false, false, true);
    let body_async = build_body("sample_crate::clear()", "", &transform, false, true, true);

    assert!(
        body_sync.contains("sample_crate::clear();"),
        "unit return without error must emit semicolon in sync fn: {body_sync}"
    );
    assert!(
        !body_sync.contains("sample_crate::clear()\n"),
        "unit return must NOT have semicolon-less expression: {body_sync}"
    );

    assert!(
        body_async.contains("sample_crate::clear().await;"),
        "unit return without error must emit semicolon in async fn: {body_async}"
    );
}

#[test]
fn bool_to_bool_cast_skipped_redundant() {
    let ty = TypeRef::Primitive(crate::core::ir::PrimitiveType::Bool);
    let cast = build_primitive_result_cast(&ty, false);

    assert_eq!(cast, "", "bool->bool cast must be empty (redundant), got: '{cast}'");
}

#[test]
fn non_matching_primitive_cast_preserved() {
    let ty_i64 = TypeRef::Primitive(crate::core::ir::PrimitiveType::I64);
    let cast_i64 = build_primitive_result_cast(&ty_i64, false);

    assert_eq!(
        cast_i64, "",
        "i64->i64 cast must be empty (redundant), got: '{cast_i64}'"
    );

    let ty_f64 = TypeRef::Primitive(crate::core::ir::PrimitiveType::F64);
    let cast_f64 = build_primitive_result_cast(&ty_f64, false);
    assert_eq!(
        cast_f64, "",
        "f64->f64 cast must be empty (redundant), got: '{cast_f64}'"
    );
}