alef 0.25.39

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::methods::emit_trait_bridge_method;
use super::*;
use crate::core::ir::{ApiSurface, MethodDef, ReceiverKind, TypeDef, TypeRef};

fn empty_type_def(name: &str, is_trait: bool) -> TypeDef {
    TypeDef {
        name: name.to_string(),
        rust_path: format!("demo::{name}"),
        original_rust_path: String::new(),
        fields: vec![],
        methods: vec![],
        is_opaque: false,
        is_clone: false,
        is_copy: false,
        doc: String::new(),
        cfg: None,
        is_trait,
        has_default: false,
        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,
        version: Default::default(),
    }
}

fn api_surface(types: Vec<TypeDef>, excluded_paths: Vec<(&str, &str)>, excluded_traits: Vec<&str>) -> ApiSurface {
    ApiSurface {
        types,
        excluded_type_paths: excluded_paths
            .into_iter()
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect(),
        excluded_trait_names: excluded_traits.into_iter().map(String::from).collect(),
        services: vec![],
        handler_contracts: vec![],
        ..ApiSurface::default()
    }
}

#[test]
fn return_type_references_in_surface_trait() {
    // Sanity check: pre-existing behaviour — traits present in api.types are detected.
    let api = api_surface(vec![empty_type_def("MyTrait", true)], vec![], vec![]);
    let ret = TypeRef::Optional(Box::new(TypeRef::Named("MyTrait".into())));
    assert!(return_type_references_trait(&ret, &api));
}

#[test]
fn return_type_references_excluded_trait_is_detected() {
    // Regression: a trait stripped from api.types via `alef(skip)` must still be
    // detected via `excluded_trait_names`, otherwise the trait-bridge field is emitted
    // and the generated `Box<dyn Fn() -> DartFnFuture<Option<demo::SyncExtractor>>>`
    // fails to compile with E0782 (`SyncExtractor` is a trait, not a type).
    let api = api_surface(
        vec![],
        vec![("SyncExtractor", "demo::extractors::SyncExtractor")],
        vec!["SyncExtractor"],
    );
    let ret = TypeRef::Optional(Box::new(TypeRef::Named("SyncExtractor".into())));
    assert!(return_type_references_trait(&ret, &api));
}

#[test]
fn return_type_with_excluded_struct_is_not_detected() {
    // Regression: excluded structs appear by qualified path
    // in surviving method signatures (`load -> Result<HiddenDocument>`) and
    // ARE bridgeable — they must NOT be filtered out, or the trait impl ends up missing
    // a required method (`error[E0046]: not all trait items implemented`).
    let api = api_surface(
        vec![],
        vec![("HiddenDocument", "demo::types::hidden::HiddenDocument")],
        vec![],
    );
    let ret = TypeRef::Named("HiddenDocument".into());
    assert!(!return_type_references_trait(&ret, &api));
}

#[test]
fn return_type_with_unrelated_named_is_not_detected() {
    let api = api_surface(vec![empty_type_def("MyStruct", false)], vec![], vec![]);
    let ret = TypeRef::Optional(Box::new(TypeRef::Named("MyStruct".into())));
    assert!(!return_type_references_trait(&ret, &api));
}

#[test]
fn excluded_named_result_return_deserializes_with_error_mapping() {
    let method = MethodDef {
        name: "extract".to_string(),
        params: vec![],
        return_type: TypeRef::Named("HiddenDocument".to_string()),
        is_async: true,
        is_static: false,
        error_type: Some("Error".to_string()),
        doc: String::new(),
        receiver: Some(ReceiverKind::Ref),
        sanitized: false,
        trait_source: None,
        returns_ref: false,
        returns_cow: false,
        return_newtype_wrapper: None,
        has_default_impl: false,
        binding_excluded: false,
        binding_exclusion_reason: None,
        version: Default::default(),
    };
    let mut out = String::new();
    let type_paths = std::collections::HashMap::from([(
        "HiddenDocument".to_string(),
        "demo::types::hidden::HiddenDocument".to_string(),
    )]);
    let excluded_type_paths = type_paths.clone();

    emit_trait_bridge_method(
        &mut out,
        &method,
        "demo",
        &type_paths,
        &excluded_type_paths,
        &std::collections::HashSet::new(),
    );

    assert!(
        out.contains("serde_json::from_str(&__ret_bridge.json)?;"),
        "Result-returning excluded types must propagate JSON decode errors, got:\n{out}",
    );
    assert!(
        !out.contains("expect(\"deserialize excluded Dart trait bridge value\")"),
        "Result-returning excluded types must not panic on JSON decode, got:\n{out}",
    );
}

#[test]
fn excluded_named_result_param_serializes_with_error_mapping() {
    let method = MethodDef {
        name: "render".to_string(),
        params: vec![crate::core::ir::ParamDef {
            name: "document".to_string(),
            ty: TypeRef::Named("HiddenDocument".to_string()),
            optional: false,
            is_ref: true,
            ..Default::default()
        }],
        return_type: TypeRef::String,
        is_async: true,
        is_static: false,
        error_type: Some("Error".to_string()),
        doc: String::new(),
        receiver: Some(ReceiverKind::Ref),
        sanitized: false,
        trait_source: None,
        returns_ref: false,
        returns_cow: false,
        return_newtype_wrapper: None,
        has_default_impl: false,
        binding_excluded: false,
        binding_exclusion_reason: None,
        version: Default::default(),
    };
    let mut out = String::new();
    let type_paths = std::collections::HashMap::from([(
        "HiddenDocument".to_string(),
        "demo::types::hidden::HiddenDocument".to_string(),
    )]);
    let excluded_type_paths = type_paths.clone();

    emit_trait_bridge_method(
        &mut out,
        &method,
        "demo",
        &type_paths,
        &excluded_type_paths,
        &std::collections::HashSet::new(),
    );

    assert!(
        out.contains("serde_json::to_string(&document)?"),
        "Result-returning excluded params must propagate JSON encode errors, got:\n{out}",
    );
    assert!(
        !out.contains("expect(\"serialize excluded Dart trait bridge value\")"),
        "Result-returning excluded params must not panic on JSON encode, got:\n{out}",
    );
}