alef-core 0.14.26

Core types, config schema, and backend trait for the alef polyglot binding generator
Documentation
//! FFI-related methods for `ResolvedCrateConfig`.

use super::ResolvedCrateConfig;

impl ResolvedCrateConfig {
    /// Get the FFI prefix (e.g., `"kreuzberg"`). Used by FFI, Go, Java, C# backends.
    ///
    /// Returns `[ffi] prefix` if set, otherwise derives from the crate name by
    /// replacing hyphens with underscores.
    pub fn ffi_prefix(&self) -> String {
        self.ffi
            .as_ref()
            .and_then(|f| f.prefix.as_ref())
            .cloned()
            .unwrap_or_else(|| self.name.replace('-', "_"))
    }

    /// Get the FFI native library name (for Go cgo, Java Panama, C# P/Invoke).
    ///
    /// Resolution order:
    /// 1. `[ffi] lib_name` explicit override
    /// 2. Directory name of the user-supplied `[crates.output] ffi` path with
    ///    hyphens replaced by underscores (e.g. `crates/html-to-markdown-ffi/src/`
    ///    → `html_to_markdown_ffi`). Walks components from the end and skips
    ///    `src`/`lib`/`include` to find the crate directory.
    /// 3. `{ffi_prefix}_ffi` fallback
    pub fn ffi_lib_name(&self) -> String {
        // 1. Explicit override in [ffi] section.
        if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
            return name.clone();
        }

        // 2. Derive from the user-supplied `[crates.output] ffi` path. We use
        //    `explicit_output` (the raw user input) — NOT `output_paths` — so a
        //    template-derived FFI dir does not accidentally drive the lib name.
        if let Some(ffi_path) = self.explicit_output.ffi.as_ref() {
            let crate_dir = ffi_path
                .components()
                .filter_map(|c| {
                    if let std::path::Component::Normal(s) = c {
                        s.to_str()
                    } else {
                        None
                    }
                })
                .rev()
                .find(|&s| s != "src" && s != "lib" && s != "include");
            if let Some(dir) = crate_dir {
                return dir.replace('-', "_");
            }
        }

        // 3. Default fallback.
        format!("{}_ffi", self.ffi_prefix())
    }

    /// Get the FFI header name.
    ///
    /// Returns `[ffi] header_name` if set, otherwise `"{ffi_prefix}.h"`.
    pub fn ffi_header_name(&self) -> String {
        self.ffi
            .as_ref()
            .and_then(|f| f.header_name.as_ref())
            .cloned()
            .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
    }
}

#[cfg(test)]
mod tests {
    use crate::config::new_config::NewAlefConfig;

    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
        cfg.resolve().unwrap().remove(0)
    }

    fn minimal_ffi() -> super::super::ResolvedCrateConfig {
        resolved_one(
            r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
"#,
        )
    }

    #[test]
    fn ffi_prefix_defaults_to_snake_case_name() {
        let r = minimal_ffi();
        assert_eq!(r.ffi_prefix(), "my_lib");
    }

    #[test]
    fn ffi_prefix_explicit_wins() {
        let r = resolved_one(
            r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]

[crates.ffi]
prefix = "custom_prefix"
"#,
        );
        assert_eq!(r.ffi_prefix(), "custom_prefix");
    }

    #[test]
    fn ffi_lib_name_falls_back_to_prefix_ffi() {
        let r = minimal_ffi();
        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
    }

    #[test]
    fn ffi_lib_name_explicit_wins() {
        let r = resolved_one(
            r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]

[crates.ffi]
lib_name = "libmy_custom"
"#,
        );
        assert_eq!(r.ffi_lib_name(), "libmy_custom");
    }

    #[test]
    fn ffi_header_name_defaults_to_prefix_h() {
        let r = minimal_ffi();
        assert_eq!(r.ffi_header_name(), "my_lib.h");
    }

    #[test]
    fn ffi_lib_name_derives_from_explicit_output_path() {
        let r = resolved_one(
            r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]

[crates.output]
ffi = "crates/html-to-markdown-ffi/src/"
"#,
        );
        // Step 2 of resolution: derive from `[crates.output] ffi` path,
        // skipping `src`/`lib`/`include` and replacing hyphens with underscores.
        assert_eq!(r.ffi_lib_name(), "html_to_markdown_ffi");
    }

    #[test]
    fn ffi_lib_name_explicit_lib_name_overrides_output_path_derivation() {
        let r = resolved_one(
            r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]

[crates.ffi]
lib_name = "explicit_wins"

[crates.output]
ffi = "crates/html-to-markdown-ffi/src/"
"#,
        );
        // Step 1 (explicit lib_name) takes precedence over step 2 (output path).
        assert_eq!(r.ffi_lib_name(), "explicit_wins");
    }

    #[test]
    fn ffi_lib_name_template_derived_output_does_not_drive_lib_name() {
        // No explicit `[crates.output] ffi`. The template-derived path
        // (e.g. `packages/ffi/my-lib/`) must NOT drive the lib_name —
        // it falls through to step 3 (`{ffi_prefix}_ffi`).
        let r = minimal_ffi();
        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
    }

    #[test]
    fn ffi_header_name_explicit_wins() {
        let r = resolved_one(
            r#"
[workspace]
languages = ["ffi"]

[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]

[crates.ffi]
header_name = "custom.h"
"#,
        );
        assert_eq!(r.ffi_header_name(), "custom.h");
    }
}