Skip to main content

alef_core/config/resolved/
ffi.rs

1//! FFI-related methods for `ResolvedCrateConfig`.
2
3use super::ResolvedCrateConfig;
4
5impl ResolvedCrateConfig {
6    /// Get the FFI prefix (e.g., `"kreuzberg"`). Used by FFI, Go, Java, C# backends.
7    ///
8    /// Returns `[ffi] prefix` if set, otherwise derives from the crate name by
9    /// replacing hyphens with underscores.
10    pub fn ffi_prefix(&self) -> String {
11        self.ffi
12            .as_ref()
13            .and_then(|f| f.prefix.as_ref())
14            .cloned()
15            .unwrap_or_else(|| self.name.replace('-', "_"))
16    }
17
18    /// Get the FFI native library name (for Go cgo, Java Panama, C# P/Invoke).
19    ///
20    /// Resolution order:
21    /// 1. `[ffi] lib_name` explicit override
22    /// 2. Directory name of the user-supplied `[crates.output] ffi` path with
23    ///    hyphens replaced by underscores (e.g. `crates/html-to-markdown-ffi/src/`
24    ///    → `html_to_markdown_ffi`). Walks components from the end and skips
25    ///    `src`/`lib`/`include` to find the crate directory.
26    /// 3. `{ffi_prefix}_ffi` fallback
27    pub fn ffi_lib_name(&self) -> String {
28        // 1. Explicit override in [ffi] section.
29        if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
30            return name.clone();
31        }
32
33        // 2. Derive from the user-supplied `[crates.output] ffi` path. We use
34        //    `explicit_output` (the raw user input) — NOT `output_paths` — so a
35        //    template-derived FFI dir does not accidentally drive the lib name.
36        if let Some(ffi_path) = self.explicit_output.ffi.as_ref() {
37            let crate_dir = ffi_path
38                .components()
39                .filter_map(|c| {
40                    if let std::path::Component::Normal(s) = c {
41                        s.to_str()
42                    } else {
43                        None
44                    }
45                })
46                .rev()
47                .find(|&s| s != "src" && s != "lib" && s != "include");
48            if let Some(dir) = crate_dir {
49                return dir.replace('-', "_");
50            }
51        }
52
53        // 3. Default fallback.
54        format!("{}_ffi", self.ffi_prefix())
55    }
56
57    /// Get the FFI header name.
58    ///
59    /// Returns `[ffi] header_name` if set, otherwise `"{ffi_prefix}.h"`.
60    pub fn ffi_header_name(&self) -> String {
61        self.ffi
62            .as_ref()
63            .and_then(|f| f.header_name.as_ref())
64            .cloned()
65            .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use crate::config::new_config::NewAlefConfig;
72
73    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
74        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
75        cfg.resolve().unwrap().remove(0)
76    }
77
78    fn minimal_ffi() -> super::super::ResolvedCrateConfig {
79        resolved_one(
80            r#"
81[workspace]
82languages = ["ffi"]
83
84[[crates]]
85name = "my-lib"
86sources = ["src/lib.rs"]
87"#,
88        )
89    }
90
91    #[test]
92    fn ffi_prefix_defaults_to_snake_case_name() {
93        let r = minimal_ffi();
94        assert_eq!(r.ffi_prefix(), "my_lib");
95    }
96
97    #[test]
98    fn ffi_prefix_explicit_wins() {
99        let r = resolved_one(
100            r#"
101[workspace]
102languages = ["ffi"]
103
104[[crates]]
105name = "my-lib"
106sources = ["src/lib.rs"]
107
108[crates.ffi]
109prefix = "custom_prefix"
110"#,
111        );
112        assert_eq!(r.ffi_prefix(), "custom_prefix");
113    }
114
115    #[test]
116    fn ffi_lib_name_falls_back_to_prefix_ffi() {
117        let r = minimal_ffi();
118        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
119    }
120
121    #[test]
122    fn ffi_lib_name_explicit_wins() {
123        let r = resolved_one(
124            r#"
125[workspace]
126languages = ["ffi"]
127
128[[crates]]
129name = "my-lib"
130sources = ["src/lib.rs"]
131
132[crates.ffi]
133lib_name = "libmy_custom"
134"#,
135        );
136        assert_eq!(r.ffi_lib_name(), "libmy_custom");
137    }
138
139    #[test]
140    fn ffi_header_name_defaults_to_prefix_h() {
141        let r = minimal_ffi();
142        assert_eq!(r.ffi_header_name(), "my_lib.h");
143    }
144
145    #[test]
146    fn ffi_lib_name_derives_from_explicit_output_path() {
147        let r = resolved_one(
148            r#"
149[workspace]
150languages = ["ffi"]
151
152[[crates]]
153name = "my-lib"
154sources = ["src/lib.rs"]
155
156[crates.output]
157ffi = "crates/html-to-markdown-ffi/src/"
158"#,
159        );
160        // Step 2 of resolution: derive from `[crates.output] ffi` path,
161        // skipping `src`/`lib`/`include` and replacing hyphens with underscores.
162        assert_eq!(r.ffi_lib_name(), "html_to_markdown_ffi");
163    }
164
165    #[test]
166    fn ffi_lib_name_explicit_lib_name_overrides_output_path_derivation() {
167        let r = resolved_one(
168            r#"
169[workspace]
170languages = ["ffi"]
171
172[[crates]]
173name = "my-lib"
174sources = ["src/lib.rs"]
175
176[crates.ffi]
177lib_name = "explicit_wins"
178
179[crates.output]
180ffi = "crates/html-to-markdown-ffi/src/"
181"#,
182        );
183        // Step 1 (explicit lib_name) takes precedence over step 2 (output path).
184        assert_eq!(r.ffi_lib_name(), "explicit_wins");
185    }
186
187    #[test]
188    fn ffi_lib_name_template_derived_output_does_not_drive_lib_name() {
189        // No explicit `[crates.output] ffi`. The template-derived path
190        // (e.g. `packages/ffi/my-lib/`) must NOT drive the lib_name —
191        // it falls through to step 3 (`{ffi_prefix}_ffi`).
192        let r = minimal_ffi();
193        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
194    }
195
196    #[test]
197    fn ffi_header_name_explicit_wins() {
198        let r = resolved_one(
199            r#"
200[workspace]
201languages = ["ffi"]
202
203[[crates]]
204name = "my-lib"
205sources = ["src/lib.rs"]
206
207[crates.ffi]
208header_name = "custom.h"
209"#,
210        );
211        assert_eq!(r.ffi_header_name(), "custom.h");
212    }
213}