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    /// Resolve the Rust expression used by FFI plugin shims
69    /// (`plugin_impl_initialize`, `plugin_impl_shutdown`) to construct an
70    /// error value from a runtime `String` named `msg`.
71    ///
72    /// Returns `[ffi] plugin_error_constructor` verbatim when set; otherwise
73    /// `None` so callers can fall back to a generic constructor that doesn't
74    /// depend on a specific error variant shape.
75    pub fn ffi_plugin_error_constructor(&self) -> Option<String> {
76        self.ffi
77            .as_ref()
78            .and_then(|f| f.plugin_error_constructor.as_ref())
79            .cloned()
80    }
81
82    /// Get the relative path to the FFI crate from the e2e test directory.
83    ///
84    /// Used by C e2e tests to locate the compiled FFI library when building
85    /// against a local checkout rather than a downloaded release.
86    ///
87    /// Resolution order:
88    /// 1. Directory name of the user-supplied `[crates.output] ffi` path,
89    ///    skipping trailing `src`/`lib`/`include` components, prefixed with
90    ///    `../../` so the path resolves from `e2e/c/` back to the repo root.
91    ///    E.g. `crates/my-lib-ffi/src/` → `../../crates/my-lib-ffi`.
92    /// 2. `../../crates/{name}-ffi` fallback derived from the crate name.
93    pub fn ffi_crate_path(&self) -> String {
94        if let Some(ffi_path) = self.explicit_output.ffi.as_ref() {
95            // Walk path components from the end, skipping src/lib/include.
96            let components: Vec<&str> = ffi_path
97                .components()
98                .filter_map(|c| {
99                    if let std::path::Component::Normal(s) = c {
100                        s.to_str()
101                    } else {
102                        None
103                    }
104                })
105                .collect();
106            // Find the crate directory component (first non-leaf from the right
107            // after skipping src/lib/include).
108            if let Some(idx) = components
109                .iter()
110                .rposition(|&s| s != "src" && s != "lib" && s != "include")
111            {
112                // Reconstruct the path up to and including this component.
113                let meaningful = &components[..=idx];
114                return format!("../../{}", meaningful.join("/"));
115            }
116        }
117        format!("../../crates/{}-ffi", self.name)
118    }
119
120    /// Get the relative path to the WASM crate's `pkg/` directory from the
121    /// e2e test directory.
122    ///
123    /// Used by WASM e2e tests to import the wasm-pack build output when
124    /// working against a local checkout rather than a published npm package.
125    ///
126    /// Resolution order:
127    /// 1. Directory name of the user-supplied `[crates.output] wasm` path,
128    ///    skipping trailing `src`/`lib`/`include` components, prefixed with
129    ///    `../../` and suffixed with `/pkg`.
130    ///    E.g. `crates/my-lib-wasm/src/` → `../../crates/my-lib-wasm/pkg`.
131    /// 2. `../../crates/{name}-wasm/pkg` fallback derived from the crate name.
132    pub fn wasm_crate_path(&self) -> String {
133        if let Some(wasm_path) = self.explicit_output.wasm.as_ref() {
134            let components: Vec<&str> = wasm_path
135                .components()
136                .filter_map(|c| {
137                    if let std::path::Component::Normal(s) = c {
138                        s.to_str()
139                    } else {
140                        None
141                    }
142                })
143                .collect();
144            if let Some(idx) = components
145                .iter()
146                .rposition(|&s| s != "src" && s != "lib" && s != "include")
147            {
148                let meaningful = &components[..=idx];
149                return format!("../../{}/pkg", meaningful.join("/"));
150            }
151        }
152        format!("../../crates/{}-wasm/pkg", self.name)
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::config::new_config::NewAlefConfig;
159
160    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
161        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
162        cfg.resolve().unwrap().remove(0)
163    }
164
165    fn minimal_ffi() -> super::super::ResolvedCrateConfig {
166        resolved_one(
167            r#"
168[workspace]
169languages = ["ffi"]
170
171[[crates]]
172name = "my-lib"
173sources = ["src/lib.rs"]
174"#,
175        )
176    }
177
178    #[test]
179    fn ffi_prefix_defaults_to_snake_case_name() {
180        let r = minimal_ffi();
181        assert_eq!(r.ffi_prefix(), "my_lib");
182    }
183
184    #[test]
185    fn ffi_prefix_explicit_wins() {
186        let r = resolved_one(
187            r#"
188[workspace]
189languages = ["ffi"]
190
191[[crates]]
192name = "my-lib"
193sources = ["src/lib.rs"]
194
195[crates.ffi]
196prefix = "custom_prefix"
197"#,
198        );
199        assert_eq!(r.ffi_prefix(), "custom_prefix");
200    }
201
202    #[test]
203    fn ffi_lib_name_falls_back_to_prefix_ffi() {
204        let r = minimal_ffi();
205        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
206    }
207
208    #[test]
209    fn ffi_lib_name_explicit_wins() {
210        let r = resolved_one(
211            r#"
212[workspace]
213languages = ["ffi"]
214
215[[crates]]
216name = "my-lib"
217sources = ["src/lib.rs"]
218
219[crates.ffi]
220lib_name = "libmy_custom"
221"#,
222        );
223        assert_eq!(r.ffi_lib_name(), "libmy_custom");
224    }
225
226    #[test]
227    fn ffi_header_name_defaults_to_prefix_h() {
228        let r = minimal_ffi();
229        assert_eq!(r.ffi_header_name(), "my_lib.h");
230    }
231
232    #[test]
233    fn ffi_lib_name_derives_from_explicit_output_path() {
234        let r = resolved_one(
235            r#"
236[workspace]
237languages = ["ffi"]
238
239[[crates]]
240name = "my-lib"
241sources = ["src/lib.rs"]
242
243[crates.output]
244ffi = "crates/html-to-markdown-ffi/src/"
245"#,
246        );
247        // Step 2 of resolution: derive from `[crates.output] ffi` path,
248        // skipping `src`/`lib`/`include` and replacing hyphens with underscores.
249        assert_eq!(r.ffi_lib_name(), "html_to_markdown_ffi");
250    }
251
252    #[test]
253    fn ffi_lib_name_explicit_lib_name_overrides_output_path_derivation() {
254        let r = resolved_one(
255            r#"
256[workspace]
257languages = ["ffi"]
258
259[[crates]]
260name = "my-lib"
261sources = ["src/lib.rs"]
262
263[crates.ffi]
264lib_name = "explicit_wins"
265
266[crates.output]
267ffi = "crates/html-to-markdown-ffi/src/"
268"#,
269        );
270        // Step 1 (explicit lib_name) takes precedence over step 2 (output path).
271        assert_eq!(r.ffi_lib_name(), "explicit_wins");
272    }
273
274    #[test]
275    fn ffi_lib_name_template_derived_output_does_not_drive_lib_name() {
276        // No explicit `[crates.output] ffi`. The template-derived path
277        // (e.g. `packages/ffi/my-lib/`) must NOT drive the lib_name —
278        // it falls through to step 3 (`{ffi_prefix}_ffi`).
279        let r = minimal_ffi();
280        assert_eq!(r.ffi_lib_name(), "my_lib_ffi");
281    }
282
283    #[test]
284    fn ffi_header_name_explicit_wins() {
285        let r = resolved_one(
286            r#"
287[workspace]
288languages = ["ffi"]
289
290[[crates]]
291name = "my-lib"
292sources = ["src/lib.rs"]
293
294[crates.ffi]
295header_name = "custom.h"
296"#,
297        );
298        assert_eq!(r.ffi_header_name(), "custom.h");
299    }
300}