alef 0.115.1

Opinionated polyglot binding generator for Rust libraries
Documentation
use anyhow::Context;

pub(crate) fn receiver_call(
    core_path: &str,
    receiver: &str,
    borrowed_receiver: &str,
    args: &str,
) -> anyhow::Result<String> {
    let path: syn::ExprPath =
        syn::parse_str(core_path).with_context(|| format!("parsing adapter Rust callable `{core_path}`"))?;
    let qualified = path.qself.is_some() || path.path.leading_colon.is_some() || path.path.segments.len() > 1;
    let call = if qualified {
        let separator = if args.is_empty() { "" } else { ", " };
        format!("{core_path}({borrowed_receiver}{separator}{args})")
    } else {
        format!("{receiver}.{core_path}({args})")
    };
    syn::parse_str::<syn::Expr>(&call)
        .with_context(|| format!("constructing adapter Rust receiver call for `{core_path}`"))?;
    Ok(call)
}

#[cfg(test)]
mod tests {
    use super::receiver_call;

    #[test]
    fn receiver_calls_preserve_rust_path_syntax_and_reject_expressions() {
        let cases = [
            ("events", "inner.events(request)"),
            ("events::<Vec<u8>>", "inner.events::<Vec<u8>>(request)"),
            ("r#type", "inner.r#type(request)"),
            ("sample::Engine::events", "sample::Engine::events(&inner, request)"),
            (
                "<Engine as Events>::events",
                "<Engine as Events>::events(&inner, request)",
            ),
            (
                "::sample::events::<Vec<u8>>",
                "::sample::events::<Vec<u8>>(&inner, request)",
            ),
        ];
        assert_eq!(cases.len(), 6);
        for (path, expected) in cases {
            let call = receiver_call(path, "inner", "&inner", "request").unwrap();
            assert_eq!(call, expected);
            syn::parse_str::<syn::Expr>(&call).expect("actual call is valid Rust syntax");
        }
        assert_eq!(
            receiver_call("sample::events", "inner", "inner", "").unwrap(),
            "sample::events(inner)"
        );
        for path in ["", "events()", "events; panic!()", "sample::"] {
            let error = receiver_call(path, "inner", "&inner", "").unwrap_err();
            assert!(error.to_string().contains("parsing adapter Rust callable"));
            assert!(error.root_cause().is::<syn::Error>());
        }
    }

    #[test]
    fn receiver_call_rejects_bare_path_keywords_and_accepts_qualified_paths() {
        let invalid = ["self", "Self", "super", "crate"];
        assert_eq!(invalid.len(), 4);
        for path in invalid {
            syn::parse_str::<syn::ExprPath>(path).expect("the input is a path, but not a method name");
            let error = receiver_call(path, "inner", "&inner", "").unwrap_err();
            assert!(error.to_string().contains("constructing adapter Rust receiver call"));
            assert!(error.root_cause().is::<syn::Error>());
        }
        for path in [
            "Self::events",
            "super::events",
            "crate::events",
            "r#type",
            "events::<Vec<u8>>",
        ] {
            let call = receiver_call(path, "inner", "&inner", "").unwrap();
            syn::parse_str::<syn::Expr>(&call).expect("qualified paths and legal method names remain valid");
        }
    }

    #[test]
    fn receiver_call_native_dispatch_preserves_trait_helpers_and_generics() {
        let calls = [
            ("events", "inner", "&inner", "", 1),
            ("Engine::events", "inner", "&inner", "", 1),
            ("<Engine as Events>::events", "inner", "&inner", "", 2),
            ("helper", "inner", "&inner", "", 3),
            ("helpers::events", "inner", "&inner", "", 4),
            ("generic::<Vec<u8>>", "inner", "&inner", "", 5),
            ("Engine::generic::<Vec<u8>>", "inner", "&inner", "", 5),
            ("r#type", "inner", "&inner", "", 6),
            ("Engine::events", "borrowed", "borrowed", "", 1),
        ];
        assert_eq!(calls.len(), 9);
        let assertions = calls
            .into_iter()
            .map(|(path, receiver, borrowed, args, expected)| {
                let call = receiver_call(path, receiver, borrowed, args).unwrap();
                format!("assert_eq!({call}, {expected});")
            })
            .collect::<Vec<_>>()
            .join("\n");
        let source = include_str!("rust_callable_probe.rs.txt").replace("__ASSERTIONS__", &assertions);
        check_dispatch_controls(&source);
    }

    fn check_dispatch_controls(source: &str) {
        let directory = tempfile::tempdir().unwrap();
        let executable = format!("probe{}", std::env::consts::EXE_SUFFIX);
        std::fs::write(directory.path().join("Taskfile.yml"), format!("version: '3'\ntasks:\n  proof:\n    cmds:\n      - rustc --edition=2024 probe.rs -o {executable}\n      - ./{executable}\n")).unwrap();
        for valid in [false, true] {
            let candidate = if valid {
                source.to_string()
            } else {
                source.replace("<Engine as Events>::events(&inner)", "inner.events()")
            };
            std::fs::write(directory.path().join("probe.rs"), candidate).unwrap();
            let output = std::process::Command::new("task")
                .args(["proof"])
                .current_dir(directory.path())
                .env("RUSTC_WRAPPER", "")
                .env("RUSTC_WORKSPACE_WRAPPER", "")
                .output()
                .unwrap();
            let stderr = String::from_utf8_lossy(&output.stderr);
            assert_eq!(output.status.success(), valid, "{stderr}");
            if !valid {
                assert!(stderr.contains("left: 1") && stderr.contains("right: 2"), "{stderr}");
            }
        }
    }
}