Skip to main content

alef_core/config/resolved/
imports.rs

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