Skip to main content

alef/core/config/resolved/
imports.rs

1//! Core crate import path methods for `ResolvedCrateConfig`.
2
3use super::ResolvedCrateConfig;
4use crate::core::config::extras::Language;
5use crate::core::config::resolve_helpers::find_after_crates_prefix;
6
7impl ResolvedCrateConfig {
8    /// Get the core crate Rust import path (e.g., `"sample_llm"`).
9    ///
10    /// Returns `[crate] core_import` if set, otherwise derives it from the
11    /// crate name by replacing hyphens with underscores.
12    pub fn core_import_name(&self) -> String {
13        self.core_import.clone().unwrap_or_else(|| self.name.replace('-', "_"))
14    }
15
16    /// Get the crate error type name (e.g., `"SampleCrateError"`).
17    ///
18    /// Returns `[crate] error_type` if set, otherwise `"Error"`.
19    pub fn error_type_name(&self) -> String {
20        self.error_type.clone().unwrap_or_else(|| "Error".to_string())
21    }
22
23    /// Get the error constructor pattern. `{msg}` is replaced with the message expression.
24    ///
25    /// Returns `[crate] error_constructor` if set, otherwise generates
26    /// `"{core_import}::{error_type}::from({msg})"`.
27    pub fn error_constructor_expr(&self) -> String {
28        self.error_constructor
29            .clone()
30            .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import_name(), self.error_type_name()))
31    }
32
33    /// Get the directory name of the core crate (derived from sources or falling back to name).
34    ///
35    /// For example, if `sources` contains `"crates/sample-markdown/src/lib.rs"`, this returns
36    /// `"sample-markdown"`. Used by the scaffold to generate correct `path = "../../crates/…"`
37    /// references in binding-crate `Cargo.toml` files.
38    pub fn core_crate_dir(&self) -> String {
39        if let Some(first_source) = self.sources.first() {
40            let path = std::path::Path::new(first_source);
41            let mut current = path.parent();
42            while let Some(dir) = current {
43                if dir.file_name().is_some_and(|n| n == "src") {
44                    if let Some(crate_dir) = dir.parent() {
45                        if let Some(dir_name) = crate_dir.file_name() {
46                            return dir_name.to_string_lossy().into_owned();
47                        }
48                    }
49                    break;
50                }
51                current = dir.parent();
52            }
53        }
54        self.name.clone()
55    }
56
57    /// Resolve the core Cargo dependency name (and matching directory) for a
58    /// language's binding crate.
59    ///
60    /// Returns `[<lang>].core_crate_override` when set (currently honored for
61    /// `wasm`, `dart`, `swift`), otherwise falls back to [`Self::core_crate_dir`].
62    pub fn core_crate_for_language(&self, lang: Language) -> String {
63        let override_name = match lang {
64            Language::Wasm => self.wasm.as_ref().and_then(|c| c.core_crate_override.as_deref()),
65            Language::Dart => self.dart.as_ref().and_then(|c| c.core_crate_override.as_deref()),
66            Language::Swift => self.swift.as_ref().and_then(|c| c.core_crate_override.as_deref()),
67            _ => None,
68        };
69        match override_name {
70            Some(name) => name.to_string(),
71            None => self.core_crate_dir(),
72        }
73    }
74
75    /// Resolve the core crate Rust import path for a language's binding crate.
76    ///
77    /// When `[<lang>].core_crate_override` is set, the override name (with `-`
78    /// translated to `_`) is used so that generated `use` paths and `From`
79    /// impls reference the overridden crate. Otherwise falls back to
80    /// [`Self::core_import_name`].
81    pub fn core_import_for_language(&self, lang: Language) -> String {
82        let override_name = match lang {
83            Language::Wasm => self.wasm.as_ref().and_then(|c| c.core_crate_override.as_deref()),
84            Language::Dart => self.dart.as_ref().and_then(|c| c.core_crate_override.as_deref()),
85            Language::Swift => self.swift.as_ref().and_then(|c| c.core_crate_override.as_deref()),
86            _ => None,
87        };
88        match override_name {
89            Some(name) => name.replace('-', "_"),
90            None => self.core_import_name(),
91        }
92    }
93
94    /// Return the effective path mappings for this crate.
95    ///
96    /// When `auto_path_mappings` is true, automatically derives a mapping from each source
97    /// crate to the configured `core_import` facade. For each source file whose path contains
98    /// `crates/{crate-name}/src/`, a mapping `{crate_name}` → `{core_import}` is added
99    /// (hyphens in the crate name are converted to underscores). Source crates that already
100    /// equal `core_import` are skipped.
101    ///
102    /// Explicit entries in `path_mappings` always override auto-derived ones.
103    pub fn effective_path_mappings(&self) -> std::collections::HashMap<String, String> {
104        let mut mappings = std::collections::HashMap::new();
105
106        if self.auto_path_mappings {
107            let core_import = self.core_import_name();
108
109            for source in &self.sources {
110                let source_str = source.to_string_lossy();
111                if let Some(after_crates) = find_after_crates_prefix(&source_str) {
112                    if let Some(slash_pos) = after_crates.find('/') {
113                        let crate_dir = &after_crates[..slash_pos];
114                        let crate_ident = crate_dir.replace('-', "_");
115                        if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
116                            mappings.insert(crate_ident, core_import.clone());
117                        }
118                    }
119                }
120            }
121        }
122
123        for (from, to) in &self.path_mappings {
124            mappings.insert(from.clone(), to.clone());
125        }
126
127        mappings
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use crate::core::config::new_config::NewAlefConfig;
134
135    fn resolved_one(toml: &str) -> super::super::ResolvedCrateConfig {
136        let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
137        cfg.resolve().unwrap().remove(0)
138    }
139
140    fn minimal() -> super::super::ResolvedCrateConfig {
141        resolved_one(
142            r#"
143[workspace]
144languages = ["python"]
145
146[[crates]]
147name = "test-lib"
148sources = ["src/lib.rs"]
149"#,
150        )
151    }
152
153    #[test]
154    fn core_import_name_defaults_to_snake_case_name() {
155        let r = minimal();
156        assert_eq!(r.core_import_name(), "test_lib");
157    }
158
159    #[test]
160    fn core_import_name_explicit_wins() {
161        let r = resolved_one(
162            r#"
163[workspace]
164languages = ["python"]
165
166[[crates]]
167name = "test-lib"
168sources = ["src/lib.rs"]
169core_import = "custom_core"
170"#,
171        );
172        assert_eq!(r.core_import_name(), "custom_core");
173    }
174
175    #[test]
176    fn error_type_name_defaults_to_error() {
177        let r = minimal();
178        assert_eq!(r.error_type_name(), "Error");
179    }
180
181    #[test]
182    fn error_type_name_explicit_wins() {
183        let r = resolved_one(
184            r#"
185[workspace]
186languages = ["python"]
187
188[[crates]]
189name = "test-lib"
190sources = ["src/lib.rs"]
191error_type = "MyError"
192"#,
193        );
194        assert_eq!(r.error_type_name(), "MyError");
195    }
196
197    #[test]
198    fn error_constructor_expr_defaults_to_from_pattern() {
199        let r = minimal();
200        assert_eq!(r.error_constructor_expr(), "test_lib::Error::from({msg})");
201    }
202
203    #[test]
204    fn error_constructor_expr_explicit_wins() {
205        let r = resolved_one(
206            r#"
207[workspace]
208languages = ["python"]
209
210[[crates]]
211name = "test-lib"
212sources = ["src/lib.rs"]
213error_constructor = "MyError::new({msg})"
214"#,
215        );
216        assert_eq!(r.error_constructor_expr(), "MyError::new({msg})");
217    }
218
219    #[test]
220    fn core_crate_dir_from_source_path() {
221        let r = resolved_one(
222            r#"
223[workspace]
224languages = ["python"]
225
226[[crates]]
227name = "test-lib"
228sources = ["crates/my-core/src/lib.rs"]
229"#,
230        );
231        assert_eq!(r.core_crate_dir(), "my-core");
232    }
233
234    #[test]
235    fn core_crate_dir_falls_back_to_name() {
236        let r = minimal();
237        assert_eq!(r.core_crate_dir(), "test-lib");
238    }
239
240    #[test]
241    fn core_crate_for_language_uses_wasm_override() {
242        use crate::core::config::extras::Language;
243        let r = resolved_one(
244            r#"
245[workspace]
246languages = ["wasm"]
247
248[[crates]]
249name = "test-lib"
250sources = ["src/lib.rs"]
251
252[crates.wasm]
253core_crate_override = "test-lib-wasm-core"
254"#,
255        );
256        assert_eq!(r.core_crate_for_language(Language::Wasm), "test-lib-wasm-core");
257    }
258
259    #[test]
260    fn core_import_for_language_normalizes_override_hyphens() {
261        use crate::core::config::extras::Language;
262        let r = resolved_one(
263            r#"
264[workspace]
265languages = ["wasm"]
266
267[[crates]]
268name = "test-lib"
269sources = ["src/lib.rs"]
270
271[crates.wasm]
272core_crate_override = "test-lib-wasm-core"
273"#,
274        );
275        assert_eq!(r.core_import_for_language(Language::Wasm), "test_lib_wasm_core");
276    }
277
278    #[test]
279    fn resolved_path_mappings_per_crate_only() {
280        let r = resolved_one(
281            r#"
282[workspace]
283languages = ["python"]
284
285[[crates]]
286name = "test-lib"
287sources = ["src/lib.rs"]
288path_mappings = { "old_mod" = "new_mod" }
289"#,
290        );
291        let mappings = r.effective_path_mappings();
292        assert_eq!(mappings.get("old_mod").map(|s| s.as_str()), Some("new_mod"));
293    }
294
295    #[test]
296    fn effective_path_mappings_auto_derives_from_sources() {
297        let r = resolved_one(
298            r#"
299[workspace]
300languages = ["python"]
301
302[[crates]]
303name = "my-lib"
304sources = ["crates/my-dep/src/lib.rs", "crates/my-lib/src/lib.rs"]
305core_import = "my_lib"
306auto_path_mappings = true
307"#,
308        );
309        let mappings = r.effective_path_mappings();
310        assert_eq!(mappings.get("my_dep").map(|s| s.as_str()), Some("my_lib"));
311        assert!(!mappings.contains_key("my_lib"));
312    }
313}