alef 0.65.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::config_marshalling_fixtures::{config_marshalling_api_surface, json_marshalling_api_surface};
use super::{RustlerBackend, test_config};
use crate::core::backend::Backend;
use crate::core::ir::ApiSurface;

fn generated_native(api: &ApiSurface) -> String {
    let files = RustlerBackend
        .generate_bindings(api, &test_config())
        .expect("native binding generation must succeed");
    files
        .iter()
        .find(|file| file.path.ends_with("lib.rs"))
        .expect("expected native lib.rs")
        .content
        .clone()
}

fn generated_public(api: &ApiSurface) -> (String, String) {
    let files = RustlerBackend
        .generate_public_api(api, &test_config())
        .expect("public API generation must succeed");
    let root = files.iter().find(|file| file.path.ends_with("my_lib.ex")).unwrap();
    let builder = files.iter().find(|file| file.path.ends_with("builder.ex")).unwrap();
    (root.content.clone(), builder.content.clone())
}

#[test]
fn native_nifs_should_return_contextual_errors_for_malformed_default_typed_json() {
    let content = generated_native(&config_marshalling_api_surface());
    assert_default_contexts(&content);
    assert_default_signatures(&content);
    assert_default_conversion(&content);
    syn::parse_file(&content).expect("generated Rustler source with Vec<Config> methods must parse as Rust");
}

fn assert_default_contexts(content: &str) {
    for function_name in [
        "builder_configure",
        "builder_configure_later_async",
        "builder_configure_many",
        "builder_configure_many_later_async",
        "registry_configure_many",
        "registry_configure_many_later_async",
        "build",
        "build_async",
    ] {
        let parameter = if function_name.contains("many") {
            "configs"
        } else {
            "config"
        };
        let expected = format!("failed to deserialize parameter `{parameter}` for `{function_name}`");
        assert!(
            content.contains(&expected),
            "missing context for {function_name}:\n{content}"
        );
    }
    for parameter in ["first", "second"] {
        let expected = format!("failed to deserialize parameter `{parameter}` for `merge`");
        assert!(content.contains(&expected), "missing merge context:\n{content}");
    }
}

fn assert_default_signatures(content: &str) {
    for signature in [
        "pub fn builder_configure(resource: rustler::ResourceArc<Builder>, config: Option<String>) -> Result<(), String>",
        "pub fn build(config: Option<String>) -> Result<(), String>",
        "pub fn builder_configure_many(resource: rustler::ResourceArc<Builder>, configs: Option<String>) -> Result<(), String>",
        "pub fn registry_configure_many(obj: Registry, configs: Option<String>) -> Result<(), String>",
    ] {
        assert!(
            content.contains(signature),
            "missing fallible signature `{signature}`:\n{content}"
        );
    }
}

fn assert_default_conversion(content: &str) {
    assert!(
        content.contains("let configs_core: Vec<my_lib::Config> = configs_core_option.unwrap_or_default();"),
        "Vec<Config> JSON must deserialize into the core element type:\n{content}"
    );
    assert!(
        content.contains(".configure_many(configs_core)"),
        "Vec<Config> method call must receive the deserialized core vector:\n{content}"
    );
    assert!(
        !content.contains("serde_json::from_str::<my_lib::Config>(&s).ok()"),
        "malformed JSON must not be discarded:\n{content}"
    );
}

#[test]
fn free_functions_should_pass_mutable_named_vectors_by_mutable_reference() {
    let content = generated_native(&config_marshalling_api_surface());
    for function_name in ["mutate_configs", "mutate_configs_async"] {
        let function_start = content
            .find(&format!("pub fn {function_name}"))
            .expect("expected generated function");
        let function = &content[function_start..];
        assert!(
            function.contains("let mut configs_core: Vec<my_lib::Config> ="),
            "{function_name} must create a mutable core vector:\n{function}"
        );
        assert!(
            function.contains(&format!("my_lib::{function_name}(&mut configs_core)")),
            "{function_name} must pass the core vector by mutable reference:\n{function}"
        );
    }
}

pub(super) fn assert_opaque_methods_json_encode_named_params() {
    let (_, builder) = generated_public(&config_marshalling_api_surface());
    for expected in [
        "Native.builder_configure(obj.ref, (cond do is_nil(config) -> nil; is_binary(config) -> config; true -> Jason.encode!(config) end))",
        "Native.builder_configure_later_async(obj.ref, (cond do is_nil(config) -> nil; is_binary(config) -> config; true -> Jason.encode!(config) end))",
        "Native.builder_configure_many(obj.ref, (cond do is_nil(configs) -> nil; is_binary(configs) -> configs; true -> Jason.encode!(configs) end))",
        "Native.builder_configure_many_later_async(obj.ref, (cond do is_nil(configs) -> nil; is_binary(configs) -> configs; true -> Jason.encode!(configs) end))",
    ] {
        assert!(builder.contains(expected), "generated Builder wrapper:\n{builder}");
    }
}

pub(super) fn assert_public_sync_wrapper_contracts() {
    let (root, builder) = generated_public(&config_marshalling_api_surface());
    assert_sync_root_contract(&root);
    assert_sync_builder_contract(&builder);
}

fn assert_sync_root_contract(root: &str) {
    for expected in [
        "case MyLib.Native.build(",
        "{:ok, value} -> value",
        "{:error, error} -> raise ArgumentError, error",
        "def merge(opts \\\\ []) do",
        "case MyLib.Native.merge(\n      case Keyword.get(opts, :first)",
        "case MyLib.Native.builder_with_config(obj.ref, (cond do is_nil(config)",
        "%MyLib.Builder{ref: ref}",
        "case MyLib.Native.builder_configure_many(obj.ref, (cond do is_nil(configs)",
    ] {
        assert!(root.contains(expected), "root wrapper missing `{expected}`:\n{root}");
    }
}

fn assert_sync_builder_contract(builder: &str) {
    for expected in [
        "ref =\n      case Native.builder_with_config(",
        "{:ok, value} -> value",
        "%__MODULE__{ref: ref}",
        "case Native.builder_configure_many(obj.ref, (cond do is_nil(configs) -> nil; is_binary(configs)",
        "Native.builder_configure_many_later_async(obj.ref, (cond do is_nil(configs)",
    ] {
        assert!(
            builder.contains(expected),
            "Builder wrapper missing `{expected}`:\n{builder}"
        );
    }
    assert!(
        !builder.contains("case Native.builder_configure_many_later_async(obj.ref"),
        "async Vec<Config> public result contract must remain unchanged:\n{builder}"
    );
}

pub(super) fn assert_returns_self_result_shapes() {
    let (root, builder) = generated_public(&config_marshalling_api_surface());
    for call in [
        "Native.builder_with_config_later_async(obj.ref",
        "Native.builder_try_with_config(obj.ref",
    ] {
        assert_result_wrapper(&builder, call, "%__MODULE__{ref: ref}");
    }
    for call in [
        "MyLib.Native.builder_with_config_later_async(obj.ref",
        "MyLib.Native.builder_try_with_config(obj.ref",
    ] {
        assert_result_wrapper(&root, call, "%MyLib.Builder{ref: ref}");
    }
}

fn assert_result_wrapper(content: &str, nif_call: &str, wrapper: &str) {
    let start = content.find(nif_call).expect("expected generated NIF call");
    let function = &content[start..];
    let success = format!("{{:ok, ref}} -> {{:ok, {wrapper}}}");
    assert!(
        function.contains(&success),
        "successful ref must be Result-wrapped:\n{function}"
    );
    assert!(
        function.contains("{:error, error} -> {:error, error}"),
        "error tuples must pass through unchanged:\n{function}"
    );
    let trailing = format!("{{:error, error}} -> {{:error, error}}\n    end\n    {wrapper}");
    assert!(
        !function.contains(&trailing),
        "must not append a second wrapper:\n{function}"
    );
}

pub(super) fn assert_json_deserialization_contracts() {
    let api = json_marshalling_api_surface();
    let native = generated_native(&api);
    assert_json_contexts(&native);
    assert_json_native_contract(&native);
    syn::parse_file(&native).expect("generated Rustler source must parse as Rust");
    let (root, builder) = generated_public(&api);
    assert_json_public_contract(&root, &builder);
}

fn assert_json_contexts(native: &str) {
    for function_name in [
        "builder_set_metadata",
        "builder_set_metadata_later_async",
        "render",
        "render_async",
    ] {
        let expected = format!("failed to deserialize parameter `metadata` for `{function_name}`");
        assert!(
            native.contains(&expected),
            "missing JSON context for {function_name}:\n{native}"
        );
    }
}

fn assert_json_native_contract(native: &str) {
    for expected in [
        "pub fn render(metadata: String) -> Result<(), String>",
        "pub fn nondelegated_json(metadata: String) -> ()",
        "pub fn builder_sanitized_metadata(resource: rustler::ResourceArc<Builder>, metadata: String) -> ()",
        "pub fn nondelegated_json_async(metadata: String) -> Result<(), String>",
        "pub fn builder_sanitized_metadata_later_async(resource: rustler::ResourceArc<Builder>, metadata: String) -> Result<(), String>",
        "Err(String::from(\"Not implemented: nondelegated_json_async\"))",
        "Err(String::from(\"Not implemented: builder_sanitized_metadata_later_async\"))",
    ] {
        assert!(
            native.contains(expected),
            "native JSON contract missing `{expected}`:\n{native}"
        );
    }
}

fn assert_json_public_contract(root: &str, builder: &str) {
    for expected in [
        "case MyLib.Native.render(metadata) do",
        "MyLib.Native.render_async(metadata)",
        "case MyLib.Native.builder_set_metadata(obj.ref, metadata) do",
    ] {
        assert!(
            root.contains(expected),
            "root JSON wrapper missing `{expected}`:\n{root}"
        );
    }
    assert!(!root.contains("case MyLib.Native.render_async(metadata)"));
    for expected in [
        "case Native.builder_set_metadata(obj.ref, metadata) do",
        "Native.builder_set_metadata_later_async(obj.ref, metadata)",
    ] {
        assert!(
            builder.contains(expected),
            "Builder JSON wrapper missing `{expected}`:\n{builder}"
        );
    }
}