alef 0.25.37

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::*;
use crate::core::ir::{
    EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, RegistrationDef, ServiceDef, TypeRef,
};

fn make_fixture_surface() -> ApiSurface {
    let constructor = MethodDef {
        name: "new".to_owned(),
        params: vec![],
        return_type: TypeRef::Unit,
        is_async: false,
        is_static: true,
        error_type: None,
        doc: "Create a new service owner.".to_owned(),
        receiver: None,
        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 get_variant = crate::core::ir::RegistrationVariant {
        name: "get".to_owned(),
        overrides: vec![crate::core::ir::RegistrationVariantOverride {
            param_name: "method".to_owned(),
            value_expr: "\"GET\"".to_owned(),
        }],
        wrapper_call: None,
        signature_params: vec![ParamDef {
            name: "path".to_owned(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            ..ParamDef::default()
        }],
        doc: Some("Register a GET handler.".to_owned()),
        style: Default::default(),
        ..Default::default()
    };

    let registration = RegistrationDef {
        method: "add_handler".to_owned(),
        callback_param: "handler".to_owned(),
        callback_contract: "RequestHandler".to_owned(),
        metadata_params: vec![
            ParamDef {
                name: "method".to_owned(),
                ty: TypeRef::String,
                optional: false,
                default: None,
                ..ParamDef::default()
            },
            ParamDef {
                name: "path".to_owned(),
                ty: TypeRef::String,
                optional: false,
                default: None,
                ..ParamDef::default()
            },
        ],
        receiver: Some(crate::core::ir::ReceiverKind::RefMut),
        return_type: TypeRef::Unit,
        error_type: Some("HandlerError".to_owned()),
        doc: "Register a request handler.".to_owned(),
        variants: vec![get_variant],
        ..Default::default()
    };

    let run_entrypoint = EntrypointDef {
        method: "run".to_owned(),
        kind: EntrypointKind::Run,
        is_async: true,
        params: vec![ParamDef {
            name: "addr".to_owned(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            ..ParamDef::default()
        }],
        return_type: TypeRef::Unit,
        error_type: Some("IoError".to_owned()),
        doc: "Start the service.".to_owned(),
    };

    let handler_contract = HandlerContractDef {
        trait_name: "RequestHandler".to_owned(),
        rust_path: "my_crate::RequestHandler".to_owned(),
        dispatch: MethodDef {
            name: "handle".to_owned(),
            params: vec![ParamDef {
                name: "req".to_owned(),
                ty: TypeRef::Named("RequestData".to_owned()),
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            return_type: TypeRef::Named("Response".to_owned()),
            is_async: true,
            is_static: false,
            error_type: None,
            doc: "Handle a request.".to_owned(),
            receiver: Some(crate::core::ir::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(),
        },
        optional_methods: vec![],
        wire_request_type: Some("RequestData".to_owned()),
        wire_response_type: Some("Response".to_owned()),
        dispatch_extra_params: vec![],
        wire_param_name: None,
        dispatch_return_type: None,
        response_adapter: None,
        doc: "Handler contract.".to_owned(),
    };

    ApiSurface {
        crate_name: "test_crate".to_owned(),
        version: "1.0.0".to_owned(),
        services: vec![ServiceDef {
            name: "TestService".to_owned(),
            rust_path: "my_crate::TestService".to_owned(),
            constructor,
            configurators: vec![],
            registrations: vec![registration],
            entrypoints: vec![run_entrypoint],
            doc: "Test service.".to_owned(),
            cfg: None,
        }],
        handler_contracts: vec![handler_contract],
        ..ApiSurface::default()
    }
}

#[test]
fn test_gen_service_go_produces_valid_go() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "TEST_CRATE");

    // Verify that the generated Go contains expected markers
    assert!(go.contains("package binding"));
    assert!(go.contains("TestService"));
    assert!(go.contains("NewTestService"));
    assert!(go.contains("RegisterAddHandler"));
    assert!(go.contains("Run"));
    assert!(go.contains("HandlerFunc"));
    assert!(go.contains("handlerRegistry"));
    assert!(go.contains("service_handler_callback"));
    // Verify cgo preamble
    assert!(go.contains("/*\n#include <string.h>"));
    assert!(go.contains("#include \"test_crate.h\""));
    assert!(go.contains("//export service_handler_callback"));
    assert!(go.contains("import \"C\""));
    // Verify prefixed struct names (uppercase prefix)
    assert!(go.contains("*TEST_CRATETestServiceOpaque"));
}

#[test]
fn test_service_struct_is_generated() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "TEST_CRATE");

    // The service struct must be present with prefixed opaque type
    assert!(go.contains("type TestService struct"));
    assert!(go.contains("owner unsafe.Pointer"));
    assert!(go.contains("*TEST_CRATETestServiceOpaque"));
    assert!(go.contains("mu    sync.Mutex"));
}

#[test]
fn test_constructor_is_generated() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // Constructor should be present with lowercase prefix
    assert!(go.contains("func NewTestService()"));
    assert!(go.contains("test_crate_test_service_new"));
}

#[test]
fn test_registration_method_exists() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // Registration method should be present with correct callback passing
    assert!(go.contains("RegisterAddHandler"));
    assert!(go.contains("handler HandlerFunc"));
    assert!(go.contains("registerHandler(handler)"));
    // Verify callback is obtained from public C helper function.
    assert!(go.contains("C.get_service_handler_callback(),"));
    // Verify prefixed struct names
    assert!(go.contains("(*C.TEST_CRATETestServiceOpaque)"));
}

#[test]
fn test_entrypoint_method_exists() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // Entrypoint method should be present with prefixed struct names
    assert!(go.contains("func (s *TestService) Run("));
    assert!(go.contains("test_crate_test_service_ep_run"));
    assert!(go.contains("(*C.TEST_CRATETestServiceOpaque)"));
}

#[test]
fn test_handler_registry_and_trampoline() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // Handler registry and trampoline must be present
    assert!(go.contains("handlerRegistry"));
    assert!(go.contains("service_handler_callback"));
    assert!(go.contains("invokeHandler"));
    assert!(go.contains("registerHandler"));
    assert!(go.contains("//export service_handler_callback"));
}

#[test]
fn test_c_ffi_imports_generated() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // C FFI imports should be present in comments
    assert!(go.contains("test_crate_test_service_new"));
    assert!(go.contains("test_crate_test_service_free"));
    assert!(go.contains("test_crate_test_service_register_add_handler"));
}

#[test]
fn test_registration_variant_method_exists() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // Variant method should be present with TitleCase name
    assert!(go.contains("func (s *TestService) Get("));
    assert!(go.contains("handler HandlerFunc"));
    assert!(go.contains("path string"));
    // Verify it calls the variant C function: symbol is {prefix}_{service}_{variant},
    // WITHOUT the registration method name in between.
    assert!(go.contains("C.test_crate_test_service_get"));
    assert!(!go.contains("C.test_crate_test_service_add_handler_get"));
    // Verify that the free wrapper-call arg (path) is marshaled with CString.
    assert!(go.contains("C.CString(path)"));
}

#[test]
fn test_start_background_method_exists() {
    let api = make_fixture_surface();
    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };

    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // StartBackground method and ServerHandle must be present
    assert!(go.contains("func (s *TestService) StartBackground("));
    assert!(go.contains("type ServerHandle struct"));
    assert!(go.contains("func (h *ServerHandle) Stop()"));
    assert!(go.contains("host string, port uint16"));
    assert!(go.contains("*ServerHandle, error"));
}

#[test]
fn test_registration_variant_wrapper_call_emits_free_args() {
    use crate::core::ir::{WrapperConstructorArg, WrapperConstructorCall};

    // Build a surface where the variant uses wrapper_call so free args come from wc.args.
    let mut api = make_fixture_surface();
    let svc = &mut api.services[0];
    let reg = &mut svc.registrations[0];

    // Replace the variant with one that has wrapper_call set.
    reg.variants[0] = crate::core::ir::RegistrationVariant {
        name: "get".to_owned(),
        overrides: vec![crate::core::ir::RegistrationVariantOverride {
            param_name: "method".to_owned(),
            value_expr: "\"GET\"".to_owned(),
        }],
        wrapper_call: Some(WrapperConstructorCall {
            metadata_param: "builder".to_owned(),
            wrapper_type_path: "test_crate::RouteBuilder".to_owned(),
            wrapper_type_name: "RouteBuilder".to_owned(),
            constructor_method: "new".to_owned(),
            args: vec![
                WrapperConstructorArg::Fixed {
                    param_name: "method".to_owned(),
                    value_expr: "\"GET\"".to_owned(),
                },
                WrapperConstructorArg::Free {
                    param: ParamDef {
                        name: "path".to_owned(),
                        ty: TypeRef::String,
                        optional: false,
                        default: None,
                        ..ParamDef::default()
                    },
                },
            ],
        }),
        signature_params: vec![ParamDef {
            name: "path".to_owned(),
            ty: TypeRef::String,
            optional: false,
            default: None,
            ..ParamDef::default()
        }],
        doc: Some("Register a GET handler.".to_owned()),
        style: Default::default(),
        ..Default::default()
    };

    let config = ResolvedCrateConfig {
        name: "test_crate".to_owned(),
        ..ResolvedCrateConfig::default()
    };
    let go = gen_service_go(&api, &config, "binding", "test_crate");

    // Free arg from wrapper_call must be emitted as a C arg.
    assert!(go.contains("C.CString(path)"), "missing CString(path) in:\n{go}");
    // Fixed args must NOT be emitted separately (baked into the FFI function).
    assert!(!go.contains("\"GET\""), "fixed arg must not be re-emitted:\n{go}");
}